From ea3f36e49243ab851d97f3bf088250d9ccbd0343 Mon Sep 17 00:00:00 2001 From: "Tim L. White" Date: Tue, 3 Mar 2026 21:43:22 -0700 Subject: [PATCH 1/6] add plan doc --- docs/dev/BIRTH_AUGUR_AUTOMATION.md | 137 +++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 docs/dev/BIRTH_AUGUR_AUTOMATION.md diff --git a/docs/dev/BIRTH_AUGUR_AUTOMATION.md b/docs/dev/BIRTH_AUGUR_AUTOMATION.md new file mode 100644 index 00000000..e86adf46 --- /dev/null +++ b/docs/dev/BIRTH_AUGUR_AUTOMATION.md @@ -0,0 +1,137 @@ +# Birth Augur / Lucky Roll Automation + +## Context + +Issues [#684](https://github.com/foundryvtt-dcc/dcc/issues/684) and [#470](https://github.com/foundryvtt-dcc/dcc/issues/470) request automation of birth augur effects. Currently, the "Lucky Roll" field is a free-text textarea with no mechanical effect — users must manually track and apply their birth augur modifier. + +DCC RAW: the birth augur modifier is locked at character creation (level-1 luck mod). Some groups house-rule it to float with current luck. Both modes need support. + +The system already applies luck mod to crits, fumbles, and turn unholy for ALL characters (regardless of birth augur). Phase 1 leaves that behavior unchanged. + +## Approach: `_getBirthAugurBonusFor()` Helper + +A helper method on `DCCActor` that returns the effective birth augur modifier for a given effect type. Each compute function calls this helper for its relevant effect types and adds the result. This avoids mutating persisted fields and handles the double-compute pattern cleanly (same inputs → same outputs). + +```javascript +_getBirthAugurBonusFor (...effectTypes) { + if (!this.isPC) return 0 + const augurIndex = this.system.details.birthAugurIndex + if (augurIndex == null) return 0 + const augur = BIRTH_AUGURS[augurIndex - 1] + if (!augur || !effectTypes.includes(augur.effect)) return 0 + return this.system.config.birthAugurMode === 'floating' + ? this.system.abilities.lck.mod + : this.system.details.birthAugurLuckMod +} +``` + +## Phase 1 (MVP): Data Model + UI + 14 "Easy" Augurs + +### Files to Create + +**`module/birth-augurs.mjs`** — Canonical table of all 30 birth augurs: +```javascript +export const BIRTH_AUGURS = [ + { index: 1, key: 'harshWinter', effect: 'allAttack' }, + { index: 2, key: 'theBull', effect: 'meleeAttack' }, + { index: 3, key: 'fortunateDate', effect: 'missileAttack' }, + // ...etc for all 30, with i18n keys for name and description +] +``` + +Effect types for Phase 1 automation: + +| # | Augur | Effect Type | Target | +|---|-------|-------------|--------| +| 1 | Harsh winter | `allAttack` | melee + missile attack adjustment | +| 2 | The bull | `meleeAttack` | melee attack adjustment | +| 3 | Fortunate date | `missileAttack` | missile attack adjustment | +| 6 | Born on the battlefield | `allDamage` | melee + missile damage adjustment | +| 7 | Path of the bear | `meleeDamage` | melee damage adjustment | +| 8 | Hawkeye | `missileDamage` | missile damage adjustment | +| 13 | Seventh son | `spellCheck` | spellCheckOtherMod | +| 17 | Lucky sign | `allSaves` | all three save otherBonus | +| 20 | Struck by lightning | `reflexSave` | ref save otherBonus | +| 21 | Lived through famine | `fortSave` | frt save otherBonus | +| 22 | Resisted temptation | `willSave` | wil save otherBonus | +| 23 | Charmed house | `armorClass` | AC computation | +| 24 | Speed of the cobra | `initiative` | init computation | +| 30 | Wild child | `speed` | speed (×5 per +1/-1) | + +Remaining augurs (#4, 5, 9, 10–12, 14–16, 18–19, 25–29) are defined in the table but their effect is not yet automated — selecting them stores the data but won't auto-apply until later phases. + +### Files to Modify + +**`module/data/actor/base-actor.mjs`** +- Add `birthAugurIndex: new NumberField({ initial: null, nullable: true, integer: true, min: 1, max: 30 })` to `details` +- Add migration in `migrateData()` to auto-populate `birthAugurIndex` from existing `birthAugur` text (best-effort pattern matching) + +**`module/data/actor/player-data.mjs`** +- Add `birthAugurMode: new StringField({ initial: 'static' })` to `config` SchemaField + +**`module/actor.js`** +- Add `import { BIRTH_AUGURS } from './birth-augurs.mjs'` +- Add `_getBirthAugurBonusFor(...effectTypes)` helper method +- `computeMeleeAndMissileAttackAndDamage()`: add birth augur bonus for `allAttack`/`meleeAttack`/`missileAttack`/`allDamage`/`meleeDamage`/`missileDamage` +- `computeSavingThrows()`: add birth augur bonus for `allSaves`/`reflexSave`/`fortSave`/`willSave` +- `computeSpellCheck()`: add birth augur bonus for `spellCheck` +- `computeInitiative()`: add birth augur bonus for `initiative` +- `prepareDerivedData()` AC section: add birth augur bonus for `armorClass` +- `prepareDerivedData()` speed section: add birth augur bonus for `speed` (×5) +- Store `_computedBirthAugurMod` and `_computedBirthAugurEffect` for UI display + +**`templates/actor-partial-pc-common.html`** +- Replace "Lucky Roll" textarea section with: dropdown selector (30 augurs + "Custom") + textarea (kept for display/custom text) +- Show computed birth augur modifier value + +**`templates/dialog-actor-config.html`** +- Add Birth Augur Mode selector (Static / Floating) with tooltip explaining RAW vs house rule + +**`module/actor-sheets-dcc.js`** (or whichever sheet class provides template data) +- Pass `birthAugurs` array with selection state to template context + +**`lang/en.json`** (and other lang files) +- Add i18n keys for all 30 augur names, descriptions, and UI labels + +**`module/pc-parser.js`** +- When parsing `luckySign`, also set `birthAugurIndex` by matching against the augur table + +### Tests + +**`module/__tests__/birth-augur.test.js`** (new) +- `_getBirthAugurBonusFor()` returns correct values per effect type +- Static mode uses `birthAugurLuckMod`, floating mode uses `lck.mod` +- Null index → no bonus +- Each automated augur modifies the correct computed values + +**`module/__tests__/actor.test.js`** (extend) +- Compute functions incorporate birth augur bonus correctly +- Birth augur interacts correctly with Active Effects (they stack) + +## Phase 2: Skills + HP Augurs + +**Skill augurs (#10, #11, #12)** — Apply at roll time in `rollSkillCheck()`: +- Born under the loom (#10): Add bonus to all skill rolls +- Fox's cunning (#11): Add bonus to findTrap/disableTrap rolls +- Four-leafed clover (#12): Add bonus to detectSecretDoors rolls + +**HP augur (#25 Bountiful harvest)** — In `prepareDerivedData()`, add `birthAugurMod × level` to `hp.max` + +## Phase 3: Context-Dependent + Gated Augurs + +**Context-dependent** (#4 unarmed, #5 mounted, #9 pack hunter, #14 spell damage, #16 magical healing, #18 trap saves, #19 poison saves, #27 corruption): Require roll-time context flags. Some may remain informational if context can't be determined. + +**Gated luck** (#15 turn unholy, #26 crits, #28 fumbles): Add opt-in world setting to only apply luck to these when birth augur matches. Default OFF to preserve current behavior. + +**Informational** (#29 birdsong/languages): No automation planned. + +## Verification + +1. `npm test` — all existing + new tests pass +2. `npm run format` — code passes lint +3. Manual testing in Foundry: + - Create character, select each of the 14 automated augurs, verify computed values change + - Toggle static/floating mode, verify modifier source changes + - Apply Active Effects to same fields, verify they stack correctly + - Import character via PC parser, verify augur auto-detected + - Existing characters with text-only augurs continue working unchanged From 03b843106d93da7e6cc288728cef1b396d58f88b Mon Sep 17 00:00:00 2001 From: "Tim L. White" Date: Wed, 4 Mar 2026 09:12:09 -0700 Subject: [PATCH 2/6] Add birth augur data model and canonical augur table Create module/birth-augurs.mjs with all 30 DCC birth augurs, their effect types, and a matchAugurFromText() helper for migration/parsing. Add birthAugurIndex field to base-actor.mjs with auto-migration from existing birthAugur text. Add birthAugurMode config to player-data.mjs. Refs: #684 #470 Co-Authored-By: Claude Opus 4.6 --- docs/user-guide/Birth-Augur.md | 67 ++++++ lang/en.json | 39 ++++ module/__mocks__/foundry.js | 9 +- module/__tests__/birth-augur.test.js | 296 +++++++++++++++++++++++++ module/actor-sheet.js | 16 ++ module/actor.js | 68 +++++- module/birth-augurs.mjs | 106 +++++++++ module/config.js | 8 + module/data/actor/base-actor.mjs | 10 + module/data/actor/player-data.mjs | 1 + module/pc-parser.js | 5 + templates/actor-partial-pc-common.html | 16 +- templates/dialog-actor-config.html | 6 + 13 files changed, 631 insertions(+), 16 deletions(-) create mode 100644 docs/user-guide/Birth-Augur.md create mode 100644 module/__tests__/birth-augur.test.js create mode 100644 module/birth-augurs.mjs diff --git a/docs/user-guide/Birth-Augur.md b/docs/user-guide/Birth-Augur.md new file mode 100644 index 00000000..f6ff9772 --- /dev/null +++ b/docs/user-guide/Birth-Augur.md @@ -0,0 +1,67 @@ +# Birth Augur (Lucky Roll) + +In DCC RPG, every character is born under a particular birth augur that modifies a specific game mechanic based on their luck modifier. The DCC system can now automate these modifiers for you. + +## Selecting a Birth Augur + +On the **Character** tab, the **Lucky Roll** section has a dropdown where you can select your character's birth augur from the full list of 30 options. + +When you select a birth augur, the system will automatically apply the luck modifier to the appropriate computed value (attack rolls, saving throws, AC, etc.). The current modifier value is displayed below the dropdown. + +The free-text field below the dropdown is kept for notes, custom text, or backwards compatibility with existing characters. + +## Setting the Luck Modifier + +The birth augur modifier is set via the **Birth Augur Luck Mod** field in the details section of the character. This should be the character's luck modifier at the time of character creation (their level-0 luck mod). + +If you import a character via the PC parser, the system will attempt to auto-detect the birth augur from the lucky sign text and set the index automatically. + +## Static vs Floating Mode + +By RAW (Rules As Written), the birth augur modifier is locked at character creation — it uses the luck modifier the character had at level 0, regardless of any later changes to their luck score. This is the default **Static** mode. + +Some groups house-rule that the birth augur modifier floats with the character's current luck score. To enable this: + +1. Click the **Toggle Controls** button (three vertical dots) in the title bar +2. Click **Config** +3. Find the **Birth Augur Mode** dropdown +4. Change it from **Static (RAW)** to **Floating (House Rule)** + +In **Floating** mode, the birth augur bonus will always use the character's current luck modifier instead of the stored birth augur luck mod value. + +## Automated Augurs + +The following 14 birth augurs are currently automated. Selecting one of these will modify the corresponding computed values on the character sheet: + +| # | Birth Augur | Effect | +|---|-------------|--------| +| 1 | Harsh winter | All attack rolls (melee and missile) | +| 2 | The bull | Melee attack rolls | +| 3 | Fortunate date | Missile fire attack rolls | +| 6 | Born on the battlefield | All damage rolls (melee and missile) | +| 7 | Path of the bear | Melee damage rolls | +| 8 | Hawkeye | Missile fire damage rolls | +| 13 | Seventh son | Spell checks | +| 17 | Lucky sign | All saving throws | +| 20 | Struck by lightning | Reflex saving throws | +| 21 | Lived through famine | Fortitude saving throws | +| 22 | Resisted temptation | Willpower saving throws | +| 23 | Charmed house | Armor Class | +| 24 | Speed of the cobra | Initiative | +| 30 | Wild child | Speed (multiplied by 5 feet per modifier point) | + +## Non-Automated Augurs + +The remaining augurs (#4, 5, 9–12, 14–16, 18–19, 25–29) can be selected from the dropdown to record the character's birth augur, but their effects are not yet automated. You will need to apply these modifiers manually when they are relevant (e.g. during specific skill checks, when unarmed, when mounted, etc.). + +## Interaction with Active Effects + +Birth augur bonuses stack with Active Effects. For example, if a character has the "Charmed house" augur (+1 AC) and also has an Active Effect that adds +2 to AC, both bonuses will be applied. + +## Existing Characters + +Characters created before this feature was added will continue to work unchanged. Their free-text birth augur field is preserved. If the system can recognise the augur from the existing text, it will automatically set the birth augur index during migration. Otherwise, you can manually select the correct augur from the dropdown. + +## Luck, Crits, Fumbles, and Turn Unholy + +The system already applies the luck modifier to critical hits, fumbles, and turn unholy checks for all characters regardless of birth augur. This existing behaviour is unchanged — those bonuses are applied whether or not the character's birth augur matches those effects. diff --git a/lang/en.json b/lang/en.json index 93022b1a..9d97b9c4 100644 --- a/lang/en.json +++ b/lang/en.json @@ -92,6 +92,45 @@ "DCC.BadValueFormulaWarning": "Bad formula in item value field!", "DCC.BaseACAbilityConfig": "Base AC Ability", "DCC.BirthAugur": "Birth Augur", + "DCC.BirthAugurCustom": "Custom", + "DCC.BirthAugurIndex": "Birth Augur", + "DCC.BirthAugurMode": "Birth Augur Mode", + "DCC.BirthAugurModeHint": "Static uses the luck modifier at character creation (RAW). Floating uses the current luck modifier (house rule).", + "DCC.BirthAugurModeStatic": "Static (RAW)", + "DCC.BirthAugurModeFloating": "Floating (House Rule)", + "DCC.BirthAugurMod": "Birth Augur Modifier", + "DCC.BirthAugurNone": "— None —", + "DCC.BirthAugurNotAutomated": "(not yet automated)", + "DCC.BirthAugur.harshWinter": "Harsh winter: All attack rolls", + "DCC.BirthAugur.theBull": "The bull: Melee attack rolls", + "DCC.BirthAugur.fortunateDate": "Fortunate date: Missile fire attack rolls", + "DCC.BirthAugur.raisedByWolves": "Raised by wolves: Unarmed attack rolls", + "DCC.BirthAugur.conceivedOnHorseback": "Conceived on horseback: Mounted attack/damage rolls", + "DCC.BirthAugur.bornOnTheBattlefield": "Born on the battlefield: Damage rolls", + "DCC.BirthAugur.pathOfTheBear": "Path of the bear: Melee damage rolls", + "DCC.BirthAugur.hawkeye": "Hawkeye: Missile fire damage rolls", + "DCC.BirthAugur.packHunter": "Pack hunter: Attack/damage for 0-level trained weapons", + "DCC.BirthAugur.bornUnderTheLoom": "Born under the loom: Skill checks", + "DCC.BirthAugur.foxsCunning": "Fox's cunning: Find/disable traps", + "DCC.BirthAugur.fourLeafedClover": "Four-leafed clover: Find secret doors", + "DCC.BirthAugur.seventhSon": "Seventh son: Spell checks", + "DCC.BirthAugur.theDwarvenStar": "The Dwarven star: Spell damage", + "DCC.BirthAugur.unholy": "Unholy: Turn unholy checks", + "DCC.BirthAugur.scepter": "Scepter: Healing spells", + "DCC.BirthAugur.luckySign": "Lucky sign: Saving throws", + "DCC.BirthAugur.guardianAngel": "Guardian angel: Saves vs. traps", + "DCC.BirthAugur.survivedThePlague": "Survived the plague: Saves vs. poison", + "DCC.BirthAugur.struckByLightning": "Struck by lightning: Reflex saving throws", + "DCC.BirthAugur.livedThroughFamine": "Lived through famine: Fortitude saving throws", + "DCC.BirthAugur.resistedTemptation": "Resisted temptation: Willpower saving throws", + "DCC.BirthAugur.charmedHouse": "Charmed house: Armor Class", + "DCC.BirthAugur.speedOfTheCobra": "Speed of the cobra: Initiative", + "DCC.BirthAugur.bountifulHarvest": "Bountiful harvest: Hit points (per level)", + "DCC.BirthAugur.warriorsBattle": "Warrior's battle: Critical hit tables", + "DCC.BirthAugur.markOfTheDemon": "Mark of the demon: Corruption rolls", + "DCC.BirthAugur.doomedToFail": "Doomed to fail: Fumbles", + "DCC.BirthAugur.twinned": "Birdsong: Languages", + "DCC.BirthAugur.wildChild": "Wild child: Speed (×5 feet)", "DCC.BlindnessDeafness": "Blindness/Deafness", "DCC.Bonus": "Bonus", "DCC.BrokenLimbs": "Broken limbs", diff --git a/module/__mocks__/foundry.js b/module/__mocks__/foundry.js index e20f113e..ba87bbf6 100644 --- a/module/__mocks__/foundry.js +++ b/module/__mocks__/foundry.js @@ -1099,6 +1099,8 @@ class ActorMock { adjustment: '+0' } }, + birthAugurIndex: null, + birthAugurLuckMod: 0, lastRolledAttackBonus: '', level: { value: 1 @@ -1153,7 +1155,8 @@ class ActorMock { baseACAbility: 'agl', initiativeDieOverride: '', sortInventory: true, - removeEmptyItems: true + removeEmptyItems: true, + birthAugurMode: 'static' } } }) @@ -1800,6 +1803,7 @@ const DOCUMENT_DEFAULTS = { attackHitBonus: { melee: { value: '+0', adjustment: '+0' }, missile: { value: '+0', adjustment: '+0' } }, attackDamageBonus: { melee: { value: '+0', adjustment: '+0' }, missile: { value: '+0', adjustment: '+0' } }, birthAugur: '', + birthAugurIndex: null, birthAugurLuckMod: 0, critRange: 20, languages: '', @@ -1835,7 +1839,8 @@ const DOCUMENT_DEFAULTS = { showSpells: false, showSkills: false, showBackstab: false, - showSwimFlySpeed: false + showSwimFlySpeed: false, + birthAugurMode: 'static' } }, Player: { diff --git a/module/__tests__/birth-augur.test.js b/module/__tests__/birth-augur.test.js new file mode 100644 index 00000000..951ce7c1 --- /dev/null +++ b/module/__tests__/birth-augur.test.js @@ -0,0 +1,296 @@ +/** + * Tests for birth augur automation + */ + +import { describe, test, expect, vi } from 'vitest' +import '../__mocks__/foundry.js' +import DCCActor from '../actor' +import { BIRTH_AUGURS, matchAugurFromText } from '../birth-augurs.mjs' + +// Mock the actor-level-change module +vi.mock('../actor-level-change.js') + +/** + * Create a test actor with isPC set to true + * The mock doesn't set type/isPC automatically like the real actor does + */ +function createPCActor () { + const actor = new DCCActor() + actor.isPC = true + return actor +} + +describe('BIRTH_AUGURS table', () => { + test('has 30 entries', () => { + expect(BIRTH_AUGURS).toHaveLength(30) + }) + + test('indices are 1-30', () => { + BIRTH_AUGURS.forEach((augur, i) => { + expect(augur.index).toBe(i + 1) + }) + }) + + test('each augur has key and effect', () => { + BIRTH_AUGURS.forEach(augur => { + expect(augur.key).toBeTruthy() + expect(augur.effect).toBeTruthy() + }) + }) +}) + +describe('matchAugurFromText', () => { + test('returns null for empty/invalid input', () => { + expect(matchAugurFromText(null)).toBeNull() + expect(matchAugurFromText('')).toBeNull() + expect(matchAugurFromText(undefined)).toBeNull() + expect(matchAugurFromText(42)).toBeNull() + }) + + test('matches augur names', () => { + expect(matchAugurFromText('Harsh winter: All attack rolls (+2)')).toBe(1) + expect(matchAugurFromText('The bull: Melee attack rolls (-1)')).toBe(2) + expect(matchAugurFromText('Fortunate date: Missile fire attack rolls (+3)')).toBe(3) + expect(matchAugurFromText('Hawkeye: Missile fire damage rolls (+1)')).toBe(8) + expect(matchAugurFromText('Seventh son: Spell checks (-2)')).toBe(13) + expect(matchAugurFromText('Lucky sign: Saving throws (+1)')).toBe(17) + expect(matchAugurFromText('Struck by lightning: Reflex saving throws (+2)')).toBe(20) + expect(matchAugurFromText('Charmed house: Armor Class (+1)')).toBe(23) + expect(matchAugurFromText('Speed of the cobra: Initiative (+2)')).toBe(24) + expect(matchAugurFromText('Wild child: Speed (+1)')).toBe(30) + }) + + test('matches case-insensitively', () => { + expect(matchAugurFromText('HARSH WINTER: All attack rolls')).toBe(1) + expect(matchAugurFromText('the bull: melee attack rolls')).toBe(2) + }) + + test('returns null for unrecognized text', () => { + expect(matchAugurFromText('Some random text')).toBeNull() + expect(matchAugurFromText('Not a real augur')).toBeNull() + }) +}) + +describe('_getBirthAugurBonusFor', () => { + test('returns 0 for NPC', () => { + const actor = createPCActor() + actor.isPC = false + actor.system.details.birthAugurIndex = 1 + actor.system.details.birthAugurLuckMod = 2 + expect(actor._getBirthAugurBonusFor('allAttack')).toBe(0) + }) + + test('returns 0 when birthAugurIndex is null', () => { + const actor = createPCActor() + expect(actor._getBirthAugurBonusFor('allAttack')).toBe(0) + }) + + test('returns 0 when effect type does not match', () => { + const actor = createPCActor() + actor.system.details.birthAugurIndex = 1 // harshWinter -> allAttack + actor.system.details.birthAugurLuckMod = 2 + expect(actor._getBirthAugurBonusFor('meleeAttack')).toBe(0) + }) + + test('returns birthAugurLuckMod in static mode', () => { + const actor = createPCActor() + actor.system.details.birthAugurIndex = 1 // harshWinter -> allAttack + actor.system.details.birthAugurLuckMod = 2 + actor.system.config.birthAugurMode = 'static' + expect(actor._getBirthAugurBonusFor('allAttack')).toBe(2) + }) + + test('returns lck.mod in floating mode', () => { + const actor = createPCActor() + actor.system.details.birthAugurIndex = 1 // harshWinter -> allAttack + actor.system.details.birthAugurLuckMod = 2 + actor.system.config.birthAugurMode = 'floating' + // actor has lck value 18 -> mod 3 + expect(actor._getBirthAugurBonusFor('allAttack')).toBe(3) + }) + + test('matches multiple effect types', () => { + const actor = createPCActor() + actor.system.details.birthAugurIndex = 1 // harshWinter -> allAttack + actor.system.details.birthAugurLuckMod = 2 + actor.system.config.birthAugurMode = 'static' + expect(actor._getBirthAugurBonusFor('meleeAttack', 'allAttack')).toBe(2) + }) +}) + +describe('birth augur integration with compute methods', () => { + test('allAttack augur adds to both melee and missile attack', () => { + const actor = createPCActor() + actor.system.details.birthAugurIndex = 1 // harshWinter -> allAttack + actor.system.details.birthAugurLuckMod = 2 + actor.system.config.birthAugurMode = 'static' + actor.computeMeleeAndMissileAttackAndDamage() + // Base: str mod (-1) + attack bonus (0) + augur (+2) = +1 + expect(actor.system.details.attackHitBonus.melee.value).toBe('+1') + // Missile: agl mod (-1) + attack bonus (0) + augur (+2) = +1 + expect(actor.system.details.attackHitBonus.missile.value).toBe('+1') + }) + + test('meleeAttack augur adds only to melee attack', () => { + const actor = createPCActor() + actor.system.details.birthAugurIndex = 2 // theBull -> meleeAttack + actor.system.details.birthAugurLuckMod = 3 + actor.system.config.birthAugurMode = 'static' + actor.computeMeleeAndMissileAttackAndDamage() + // Melee: str mod (-1) + attack bonus (0) + augur (+3) = +2 + expect(actor.system.details.attackHitBonus.melee.value).toBe('+2') + // Missile: agl mod (-1) + attack bonus (0) + no augur = -1 + expect(actor.system.details.attackHitBonus.missile.value).toBe('-1') + }) + + test('missileAttack augur adds only to missile attack', () => { + const actor = createPCActor() + actor.system.details.birthAugurIndex = 3 // fortunateDate -> missileAttack + actor.system.details.birthAugurLuckMod = 1 + actor.system.config.birthAugurMode = 'static' + actor.computeMeleeAndMissileAttackAndDamage() + // Melee: str mod (-1) + attack bonus (0) + no augur = -1 + expect(actor.system.details.attackHitBonus.melee.value).toBe('-1') + // Missile: agl mod (-1) + attack bonus (0) + augur (+1) = +0 + expect(actor.system.details.attackHitBonus.missile.value).toBe('+0') + }) + + test('allDamage augur adds to both melee and missile damage', () => { + const actor = createPCActor() + actor.system.details.birthAugurIndex = 6 // bornOnTheBattlefield -> allDamage + actor.system.details.birthAugurLuckMod = 2 + actor.system.config.birthAugurMode = 'static' + actor.computeMeleeAndMissileAttackAndDamage() + // Melee damage: str mod (-1) + augur (+2) = +1 + expect(actor.system.details.attackDamageBonus.melee.value).toBe('+1') + // Missile damage: augur (+2) = +2 + expect(actor.system.details.attackDamageBonus.missile.value).toBe('+2') + }) + + test('meleeDamage augur adds only to melee damage', () => { + const actor = createPCActor() + actor.system.details.birthAugurIndex = 7 // pathOfTheBear -> meleeDamage + actor.system.details.birthAugurLuckMod = 1 + actor.system.config.birthAugurMode = 'static' + actor.computeMeleeAndMissileAttackAndDamage() + // Melee damage: str mod (-1) + augur (+1) = +0 + expect(actor.system.details.attackDamageBonus.melee.value).toBe('+0') + // Missile damage: no augur = +0 + expect(actor.system.details.attackDamageBonus.missile.value).toBe('+0') + }) + + test('missileDamage augur adds only to missile damage', () => { + const actor = createPCActor() + actor.system.details.birthAugurIndex = 8 // hawkeye -> missileDamage + actor.system.details.birthAugurLuckMod = 3 + actor.system.config.birthAugurMode = 'static' + actor.computeMeleeAndMissileAttackAndDamage() + // Melee damage: str mod (-1) + no augur = -1 + expect(actor.system.details.attackDamageBonus.melee.value).toBe('-1') + // Missile damage: augur (+3) = +3 + expect(actor.system.details.attackDamageBonus.missile.value).toBe('+3') + }) + + test('reflexSave augur adds to reflex save', () => { + const actor = createPCActor() + actor.system.details.birthAugurIndex = 20 // struckByLightning -> reflexSave + actor.system.details.birthAugurLuckMod = 2 + actor.system.config.birthAugurMode = 'static' + actor.computeSavingThrows() + // Ref: agl mod (-1) + augur (+2) = +1 + expect(actor.system.saves.ref.value).toBe('+1') + // Fort: sta mod (0) = +0 + expect(actor.system.saves.frt.value).toBe('+0') + // Will: per mod (2) = +2 + expect(actor.system.saves.wil.value).toBe('+2') + }) + + test('fortSave augur adds to fort save', () => { + const actor = createPCActor() + actor.system.details.birthAugurIndex = 21 // livedThroughFamine -> fortSave + actor.system.details.birthAugurLuckMod = 1 + actor.system.config.birthAugurMode = 'static' + actor.computeSavingThrows() + expect(actor.system.saves.frt.value).toBe('+1') + expect(actor.system.saves.ref.value).toBe('-1') + }) + + test('willSave augur adds to will save', () => { + const actor = createPCActor() + actor.system.details.birthAugurIndex = 22 // resistedTemptation -> willSave + actor.system.details.birthAugurLuckMod = 1 + actor.system.config.birthAugurMode = 'static' + actor.computeSavingThrows() + expect(actor.system.saves.wil.value).toBe('+3') + expect(actor.system.saves.ref.value).toBe('-1') + }) + + test('allSaves augur adds to all saves', () => { + const actor = createPCActor() + actor.system.details.birthAugurIndex = 17 // luckySign -> allSaves + actor.system.details.birthAugurLuckMod = 1 + actor.system.config.birthAugurMode = 'static' + actor.computeSavingThrows() + // Ref: agl mod (-1) + augur (+1) = +0 + expect(actor.system.saves.ref.value).toBe('+0') + // Fort: sta mod (0) + augur (+1) = +1 + expect(actor.system.saves.frt.value).toBe('+1') + // Will: per mod (2) + augur (+1) = +3 + expect(actor.system.saves.wil.value).toBe('+3') + }) + + test('spellCheck augur adds to spell check', () => { + const actor = createPCActor() + actor.system.details.birthAugurIndex = 13 // seventhSon -> spellCheck + actor.system.details.birthAugurLuckMod = 2 + actor.system.config.birthAugurMode = 'static' + actor.computeSpellCheck() + // Level (1) + int mod (+1) + augur (+2) = "+1+1+2" (formula string) + expect(actor.system.class.spellCheck).toBe('+1+1+2') + }) + + test('initiative augur adds to initiative', () => { + const actor = createPCActor() + actor.system.details.birthAugurIndex = 24 // speedOfTheCobra -> initiative + actor.system.details.birthAugurLuckMod = 2 + actor.system.config.birthAugurMode = 'static' + actor.computeInitiative({ addClassLevelToInitiative: false }) + // Agl mod (-1) + otherMod (0) + augur (+2) = 1 + expect(actor.system.attributes.init.value).toBe(1) + }) + + test('no augur selected does not affect computations', () => { + const actor = createPCActor() + // birthAugurIndex is null by default + actor.computeMeleeAndMissileAttackAndDamage() + expect(actor.system.details.attackHitBonus.melee.value).toBe('-1') + expect(actor.system.details.attackHitBonus.missile.value).toBe('-1') + actor.computeSavingThrows() + expect(actor.system.saves.ref.value).toBe('-1') + expect(actor.system.saves.frt.value).toBe('+0') + expect(actor.system.saves.wil.value).toBe('+2') + }) + + test('non-automated augur does not affect computations', () => { + const actor = createPCActor() + actor.system.details.birthAugurIndex = 4 // raisedByWolves -> 'none' + actor.system.details.birthAugurLuckMod = 3 + actor.system.config.birthAugurMode = 'static' + actor.computeMeleeAndMissileAttackAndDamage() + expect(actor.system.details.attackHitBonus.melee.value).toBe('-1') + actor.computeSavingThrows() + expect(actor.system.saves.ref.value).toBe('-1') + actor.computeInitiative({ addClassLevelToInitiative: false }) + expect(actor.system.attributes.init.value).toBe(-1) + }) + + test('negative birthAugurLuckMod applies correctly', () => { + const actor = createPCActor() + actor.system.details.birthAugurIndex = 1 // harshWinter -> allAttack + actor.system.details.birthAugurLuckMod = -2 + actor.system.config.birthAugurMode = 'static' + actor.computeMeleeAndMissileAttackAndDamage() + // Melee: str mod (-1) + augur (-2) = -3 + expect(actor.system.details.attackHitBonus.melee.value).toBe('-3') + }) +}) diff --git a/module/actor-sheet.js b/module/actor-sheet.js index a9d779d1..bc21d28f 100644 --- a/module/actor-sheet.js +++ b/module/actor-sheet.js @@ -4,6 +4,7 @@ import DCCActorConfig from './actor-config.js' import MeleeMissileBonusConfig from './melee-missile-bonus-config.js' import SavingThrowConfig from './saving-throw-config.js' import EntityImages from './entity-images.js' +import { BIRTH_AUGURS } from './birth-augurs.mjs' const { HandlebarsApplicationMixin } = foundry.applications.api // eslint-disable-next-line no-unused-vars @@ -164,6 +165,9 @@ class DCCActorSheet extends HandlebarsApplicationMixin(ActorSheetV2) { saveEffects: this.#prepareSaveEffects(), attributeEffects: this.#prepareAttributeEffects(), actor: this.options.document, + birthAugurs: this.#prepareBirthAugurs(), + birthAugurMod: this.options.document._computedBirthAugurMod ?? null, + birthAugurEffect: this.options.document._computedBirthAugurEffect ?? null, compendiumLinks: this.#prepareCompendiumLinks(), config: CONFIG.DCC, corruptionHTML: await this.#prepareCorruption(), @@ -809,6 +813,18 @@ class DCCActorSheet extends HandlebarsApplicationMixin(ActorSheetV2) { return CONFIG.DCC.coreBookCompendiumLinks } + /** + * Prepare birth augur select options for the template + * @returns {Object} Object keyed by augur index with i18n labels, suitable for selectOptions + */ + #prepareBirthAugurs () { + const options = {} + for (const augur of BIRTH_AUGURS) { + options[augur.index] = `DCC.BirthAugur.${augur.key}` + } + return options + } + /** * Search the object and then its parent elements for a dataset attribute @param {Object} element The starting element diff --git a/module/actor.js b/module/actor.js index 0e170dc6..7cff62ae 100644 --- a/module/actor.js +++ b/module/actor.js @@ -3,6 +3,7 @@ import { ensurePlus, getCritTableResult, getCritTableLink, getFumbleTableResult, getNPCFumbleTableResult, getFumbleTableNameFromCritTableName, addDamageFlavorToRolls } from './utilities.js' import DCCActorLevelChange from './actor-level-change.js' +import { BIRTH_AUGURS } from './birth-augurs.mjs' const { TextEditor } = foundry.applications.ux @@ -183,14 +184,16 @@ class DCCActor extends Actor { } } if (config.computeAC) { + const augurACBonus = this._getBirthAugurBonusFor('armorClass') this.system.attributes.ac.baseAbility = abilityMod this.system.attributes.ac.baseAbilityLabel = abilityLabel this.system.attributes.ac.armorBonus = armorBonus - this.system.attributes.ac.value = 10 + abilityMod + armorBonus + acOtherMod + this.system.attributes.ac.value = 10 + abilityMod + armorBonus + acOtherMod + augurACBonus } if (config.computeSpeed) { + const augurSpeedBonus = this._getBirthAugurBonusFor('speed') * 5 this.system.attributes.ac.speedPenalty = speedPenalty - this.system.attributes.speed.value = baseSpeed + speedPenalty + this.system.attributes.speed.value = baseSpeed + speedPenalty + augurSpeedBonus } } @@ -199,6 +202,20 @@ class DCCActor extends Actor { this.computeInitiative(config) } + // Compute birth augur display info for UI + if (this.isPC) { + const augurIndex = this.system.details.birthAugurIndex + if (augurIndex != null) { + const augur = BIRTH_AUGURS[augurIndex - 1] + if (augur) { + this._computedBirthAugurEffect = augur.effect + this._computedBirthAugurMod = this.system.config.birthAugurMode === 'floating' + ? this.system.abilities.lck.mod + : this.system.details.birthAugurLuckMod + } + } + } + // Re-prepare embedded items so they can see active effect modifications // Items initially prepare before applyActiveEffects runs, so they need // to re-read actor values that may have been modified by effects @@ -585,16 +602,39 @@ class DCCActor extends Actor { return actionDice } + /** + * Get the birth augur bonus for one or more effect types. + * Returns the effective modifier based on the selected birth augur and mode. + * @param {...string} effectTypes - Effect types to check (e.g. 'allAttack', 'meleeAttack') + * @returns {number} The birth augur bonus, or 0 if no matching augur + */ + _getBirthAugurBonusFor (...effectTypes) { + if (!this.isPC) return 0 + const augurIndex = this.system.details.birthAugurIndex + if (augurIndex == null) return 0 + const augur = BIRTH_AUGURS[augurIndex - 1] + if (!augur || !effectTypes.includes(augur.effect)) return 0 + return this.system.config.birthAugurMode === 'floating' + ? this.system.abilities.lck.mod + : this.system.details.birthAugurLuckMod + } + /** Compute Melee/Missile Base Attack and Damage Modifiers */ computeMeleeAndMissileAttackAndDamage () { const attackBonus = this.system.details.attackBonus || '0' const strengthBonus = parseInt(this.system.abilities.str.mod) || 0 const agilityBonus = parseInt(this.system.abilities.agl.mod) || 0 - const meleeAttackBonusAdjustment = parseInt(this.system.details.attackHitBonus?.melee?.adjustment) || 0 - const meleeDamageBonusAdjustment = parseInt(this.system.details.attackDamageBonus?.melee?.adjustment) || 0 - const missileAttackBonusAdjustment = parseInt(this.system.details.attackHitBonus?.missile?.adjustment) || 0 - const missileDamageBonusAdjustment = parseInt(this.system.details.attackDamageBonus?.missile?.adjustment) || 0 + const augurAttackBonus = this._getBirthAugurBonusFor('allAttack') + const augurMeleeAttackBonus = this._getBirthAugurBonusFor('meleeAttack') + const augurMissileAttackBonus = this._getBirthAugurBonusFor('missileAttack') + const augurDamageBonus = this._getBirthAugurBonusFor('allDamage') + const augurMeleeDamageBonus = this._getBirthAugurBonusFor('meleeDamage') + const augurMissileDamageBonus = this._getBirthAugurBonusFor('missileDamage') + const meleeAttackBonusAdjustment = (parseInt(this.system.details.attackHitBonus?.melee?.adjustment) || 0) + augurAttackBonus + augurMeleeAttackBonus + const meleeDamageBonusAdjustment = (parseInt(this.system.details.attackDamageBonus?.melee?.adjustment) || 0) + augurDamageBonus + augurMeleeDamageBonus + const missileAttackBonusAdjustment = (parseInt(this.system.details.attackHitBonus?.missile?.adjustment) || 0) + augurAttackBonus + augurMissileAttackBonus + const missileDamageBonusAdjustment = (parseInt(this.system.details.attackDamageBonus?.missile?.adjustment) || 0) + augurDamageBonus + augurMissileDamageBonus let meleeAttackBonus let missileAttackBonus let meleeAttackDamage @@ -627,14 +667,15 @@ class DCCActor extends Actor { const perMod = parseInt(this.system.abilities.per.mod) const aglMod = parseInt(this.system.abilities.agl.mod) const staMod = parseInt(this.system.abilities.sta.mod) + const augurAllSaves = this._getBirthAugurBonusFor('allSaves') const refSaveClassBonus = parseInt(this.system.saves.ref.classBonus || 0) - const refSaveOtherBonus = parseInt(this.system.saves.ref.otherBonus || 0) + const refSaveOtherBonus = parseInt(this.system.saves.ref.otherBonus || 0) + augurAllSaves + this._getBirthAugurBonusFor('reflexSave') const refSaveOverride = this.system.saves.ref.override const frtSaveClassBonus = parseInt(this.system.saves.frt.classBonus || 0) - const frtSaveOtherBonus = parseInt(this.system.saves.frt.otherBonus || 0) + const frtSaveOtherBonus = parseInt(this.system.saves.frt.otherBonus || 0) + augurAllSaves + this._getBirthAugurBonusFor('fortSave') const frtSaveOverride = this.system.saves.frt.override const wilSaveClassBonus = parseInt(this.system.saves.wil.classBonus || 0) - const wilSaveOtherBonus = parseInt(this.system.saves.wil.otherBonus || 0) + const wilSaveOtherBonus = parseInt(this.system.saves.wil.otherBonus || 0) + augurAllSaves + this._getBirthAugurBonusFor('willSave') const wilSaveOverride = this.system.saves.wil.override this.system.saves.ref.value = ensurePlus(`${aglMod + refSaveClassBonus + refSaveOtherBonus}`) @@ -672,7 +713,12 @@ class DCCActor extends Actor { if (this.system.class.spellCheckOtherMod) { otherMod = ensurePlus(this.system.class.spellCheckOtherMod) } - this.system.class.spellCheck = ensurePlus(this.system.details.level.value + abilityMod + otherMod) + let augurMod = '' + const augurSpellBonus = this._getBirthAugurBonusFor('spellCheck') + if (augurSpellBonus) { + augurMod = ensurePlus(augurSpellBonus) + } + this.system.class.spellCheck = ensurePlus(this.system.details.level.value + abilityMod + otherMod + augurMod) if (this.system.class.spellCheckOverride) { this.system.class.spellCheck = this.system.class.spellCheckOverride } @@ -691,7 +737,7 @@ class DCCActor extends Actor { * @param {Object} config - Actor configuration */ computeInitiative (config) { - this.system.attributes.init.value = parseInt(this.system.abilities.agl.mod) + parseInt(this.system.attributes.init.otherMod || 0) + this.system.attributes.init.value = parseInt(this.system.abilities.agl.mod) + parseInt(this.system.attributes.init.otherMod || 0) + this._getBirthAugurBonusFor('initiative') if (config.addClassLevelToInitiative) { this.system.attributes.init.value += this.system.details.level.value } diff --git a/module/birth-augurs.mjs b/module/birth-augurs.mjs new file mode 100644 index 00000000..0198dcec --- /dev/null +++ b/module/birth-augurs.mjs @@ -0,0 +1,106 @@ +/** + * Canonical table of all 30 DCC birth augurs + * + * Each augur has: + * - index: 1-30 (matches the d30 roll from the rulebook) + * - key: camelCase identifier used for i18n lookup (DCC.BirthAugur.{key}) + * - effect: the effect type used by _getBirthAugurBonusFor() for automation + * + * Effect types automated in Phase 1: + * allAttack, meleeAttack, missileAttack, allDamage, meleeDamage, missileDamage, + * spellCheck, allSaves, reflexSave, fortSave, willSave, armorClass, initiative, speed + * + * Effect type 'none' means the augur is defined but not yet automated. + */ +export const BIRTH_AUGURS = [ + { index: 1, key: 'harshWinter', effect: 'allAttack' }, + { index: 2, key: 'theBull', effect: 'meleeAttack' }, + { index: 3, key: 'fortunateDate', effect: 'missileAttack' }, + { index: 4, key: 'raisedByWolves', effect: 'none' }, + { index: 5, key: 'conceivedOnHorseback', effect: 'none' }, + { index: 6, key: 'bornOnTheBattlefield', effect: 'allDamage' }, + { index: 7, key: 'pathOfTheBear', effect: 'meleeDamage' }, + { index: 8, key: 'hawkeye', effect: 'missileDamage' }, + { index: 9, key: 'packHunter', effect: 'none' }, + { index: 10, key: 'bornUnderTheLoom', effect: 'none' }, + { index: 11, key: 'foxsCunning', effect: 'none' }, + { index: 12, key: 'fourLeafedClover', effect: 'none' }, + { index: 13, key: 'seventhSon', effect: 'spellCheck' }, + { index: 14, key: 'theDwarvenStar', effect: 'none' }, + { index: 15, key: 'unholy', effect: 'none' }, + { index: 16, key: 'scepter', effect: 'none' }, + { index: 17, key: 'luckySign', effect: 'allSaves' }, + { index: 18, key: 'guardianAngel', effect: 'none' }, + { index: 19, key: 'survivedThePlague', effect: 'none' }, + { index: 20, key: 'struckByLightning', effect: 'reflexSave' }, + { index: 21, key: 'livedThroughFamine', effect: 'fortSave' }, + { index: 22, key: 'resistedTemptation', effect: 'willSave' }, + { index: 23, key: 'charmedHouse', effect: 'armorClass' }, + { index: 24, key: 'speedOfTheCobra', effect: 'initiative' }, + { index: 25, key: 'bountifulHarvest', effect: 'none' }, + { index: 26, key: 'warriorsBattle', effect: 'none' }, + { index: 27, key: 'markOfTheDemon', effect: 'none' }, + { index: 28, key: 'doomedToFail', effect: 'none' }, + { index: 29, key: 'twinned', effect: 'none' }, + { index: 30, key: 'wildChild', effect: 'speed' } +] + +/** + * Match birth augur text against the augur table to find the index. + * Used by migration and PC parser to auto-detect augur from free-text. + * + * @param {string} text - The birth augur text to match + * @returns {number|null} - The augur index (1-30) or null if no match + */ +export function matchAugurFromText (text) { + if (!text || typeof text !== 'string') return null + + const normalizedText = text.toLowerCase().trim() + if (!normalizedText) return null + + // Match patterns from the DCC rulebook birth augur descriptions + // Order matters: more specific patterns (e.g. "melee damage") must come before + // general patterns (e.g. "damage rolls") to avoid false matches. + const patterns = [ + { index: 1, patterns: ['harsh winter', 'all attack rolls'] }, + { index: 2, patterns: ['the bull', 'melee attack rolls'] }, + { index: 3, patterns: ['fortunate date', 'missile fire attack rolls'] }, + { index: 4, patterns: ['raised by wolves', 'unarmed attack rolls'] }, + { index: 5, patterns: ['conceived on horseback', 'mounted'] }, + { index: 7, patterns: ['path of the bear', 'melee damage rolls'] }, + { index: 8, patterns: ['hawkeye', 'missile fire damage rolls'] }, + { index: 6, patterns: ['born on the battlefield', 'damage rolls'] }, + { index: 9, patterns: ['pack hunter', 'attack and damage rolls for 0-level'] }, + { index: 10, patterns: ['born under the loom', 'skill checks'] }, + { index: 11, patterns: ["fox's cunning", 'fox\u2019s cunning', 'find.+trap', 'disable.+trap'] }, + { index: 12, patterns: ['four-leafed clover', 'four leafed clover', 'find secret doors'] }, + { index: 13, patterns: ['seventh son', 'spell checks'] }, + { index: 14, patterns: ['the dwarven star', 'spell damage'] }, + { index: 15, patterns: ['unholy', 'turn unholy checks'] }, + { index: 16, patterns: ['scepter', 'healing spells'] }, + { index: 18, patterns: ['guardian angel', 'saves versus traps'] }, + { index: 19, patterns: ['survived the plague', 'saves versus poison'] }, + { index: 20, patterns: ['struck by lightning', 'reflex saving throws'] }, + { index: 21, patterns: ['lived through famine', 'fortitude saving throws'] }, + { index: 22, patterns: ['resisted temptation', 'willpower saving throws'] }, + { index: 17, patterns: ['lucky sign', 'saving throws'] }, + { index: 23, patterns: ['charmed house', 'armor class'] }, + { index: 24, patterns: ['speed of the cobra', 'initiative'] }, + { index: 25, patterns: ['bountiful harvest', 'hit points'] }, + { index: 26, patterns: ["warrior's battle", 'warrior\u2019s battle', 'critical hit tables'] }, + { index: 27, patterns: ['mark of the demon', 'corruption rolls'] }, + { index: 28, patterns: ['doomed to fail', 'fumbles'] }, + { index: 29, patterns: ['twinned', 'birdsong'] }, + { index: 30, patterns: ['wild child', 'speed\\b'] } + ] + + for (const entry of patterns) { + for (const pattern of entry.patterns) { + if (normalizedText.match(new RegExp(pattern, 'i'))) { + return entry.index + } + } + } + + return null +} diff --git a/module/config.js b/module/config.js index 06d50262..7d132251 100644 --- a/module/config.js +++ b/module/config.js @@ -207,6 +207,14 @@ DCC.attackBonusModes = { autoPerAttack: 'DCC.AttackBonusConfigModeAutoPerAttack' } +/** + * Birth Augur modes + */ +DCC.birthAugurModes = { + static: 'DCC.BirthAugurModeStatic', + floating: 'DCC.BirthAugurModeFloating' +} + /** * The valid currency denominations supported by the DCC system * @type {Object} diff --git a/module/data/actor/base-actor.mjs b/module/data/actor/base-actor.mjs index e9360675..24dbae55 100644 --- a/module/data/actor/base-actor.mjs +++ b/module/data/actor/base-actor.mjs @@ -4,6 +4,7 @@ * Contains the common template fields shared by all actor types */ import { AbilityField, CurrencyField, DiceField, SaveField, isValidDiceNotation, migrateFieldsToInteger } from '../fields/_module.mjs' +import { matchAugurFromText } from '../../birth-augurs.mjs' const { SchemaField, StringField, NumberField, ArrayField, HTMLField } = foundry.data.fields @@ -93,6 +94,14 @@ export class BaseActorData extends foundry.abstract.TypeDataModel { } } + // Auto-populate birthAugurIndex from birthAugur text if not already set + if (source.details?.birthAugur && source.details.birthAugurIndex == null) { + const matchedIndex = matchAugurFromText(source.details.birthAugur) + if (matchedIndex !== null) { + source.details.birthAugurIndex = matchedIndex + } + } + // Convert currency values to integers if needed migrateFieldsToInteger(source.currency, ['pp', 'ep', 'gp', 'sp', 'cp'], 0) @@ -225,6 +234,7 @@ export class BaseActorData extends foundry.abstract.TypeDataModel { }) }), birthAugur: new StringField({ initial: '' }), + birthAugurIndex: new NumberField({ initial: null, nullable: true, integer: true, min: 1, max: 30 }), birthAugurLuckMod: new NumberField({ initial: 0, integer: true }), critRange: new NumberField({ initial: 20, integer: true, min: 1 }), sheetClass: new StringField({ initial: '' }), diff --git a/module/data/actor/player-data.mjs b/module/data/actor/player-data.mjs index d2e19ac4..879e6eab 100644 --- a/module/data/actor/player-data.mjs +++ b/module/data/actor/player-data.mjs @@ -213,6 +213,7 @@ export class PlayerData extends BaseActorData { computeInitiative: new BooleanField({ initial: true }), computeMeleeAndMissileAttackAndDamage: new BooleanField({ initial: true }), computeSavingThrows: new BooleanField({ initial: true }), + birthAugurMode: new StringField({ initial: 'static' }), sortInventory: new BooleanField({ initial: true }), removeEmptyItems: new BooleanField({ initial: true }), showSpells: new BooleanField({ initial: false }), diff --git a/module/pc-parser.js b/module/pc-parser.js index 1dff2800..60adeb59 100644 --- a/module/pc-parser.js +++ b/module/pc-parser.js @@ -3,6 +3,7 @@ import EntityImages from './entity-images.js' import { getFirstDie, getFirstMod } from './utilities.js' import DCC from './config.js' +import { matchAugurFromText } from './birth-augurs.mjs' /** * Parses Player Stat Blocks (e.g. from Purple Sorcerer) into an Actor sheet @@ -225,6 +226,10 @@ function _parseJSONPCs (pcObject) { if (pcObject.luckySign) { notes = notes + game.i18n.localize('DCC.BirthAugur') + ': ' + pcObject.luckySign + '
' pc['details.birthAugur'] = pcObject.luckySign + const augurIndex = matchAugurFromText(pcObject.luckySign) + if (augurIndex !== null) { + pc['details.birthAugurIndex'] = augurIndex + } } if (pcObject.languages) { notes = notes + game.i18n.localize('DCC.Languages') + ': ' + pcObject.languages + '
' diff --git a/templates/actor-partial-pc-common.html b/templates/actor-partial-pc-common.html index b77a3072..722f4a65 100644 --- a/templates/actor-partial-pc-common.html +++ b/templates/actor-partial-pc-common.html @@ -338,10 +338,20 @@ - {{!-- Lucky Roll --}} + {{!-- Lucky Roll / Birth Augur --}}
- - + + + {{#if birthAugurMod}} +
+ {{localize "DCC.BirthAugurMod"}}: {{numberFormat birthAugurMod decimals=0 sign=true}} +
+ {{/if}} +
{{!-- Row separators --}} diff --git a/templates/dialog-actor-config.html b/templates/dialog-actor-config.html index 8ed5f49c..a3cd4fd6 100644 --- a/templates/dialog-actor-config.html +++ b/templates/dialog-actor-config.html @@ -84,6 +84,12 @@ id="system.config.computeSavingThrows" {{checked system.config.computeSavingThrows}} data-dtype="boolean"/> +
+ + +