test(01-02): RED — recompute-derived-stats spec (Pitfall #8/#9)
Task 4 RED phase: 5 failing tests covering pure recompute pipeline. - hpMax = ancestryHP + (classHP + conMod) × newLevel - boost-cap-at-18 for CON boost (Pitfall #8) - proficiencyChanges from ClassProgression applied (fortitude EXPERT) - Pitfall #9: result MUST NOT contain hpCurrent or hpTemp - level passthrough Verified failure: module './recompute-derived-stats' not found.
This commit is contained in:
@@ -0,0 +1,89 @@
|
|||||||
|
import { recomputeDerivedStats } from './recompute-derived-stats';
|
||||||
|
import type { CharacterContext, ClassProgressionRow, WizardChoices, Proficiency } from './types';
|
||||||
|
|
||||||
|
type RecomputeInput = CharacterContext & {
|
||||||
|
ancestryHp: number;
|
||||||
|
classHp: number;
|
||||||
|
armorAc: number;
|
||||||
|
armorProficiency: Proficiency;
|
||||||
|
dexCap: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function baseCharacter(overrides: Partial<RecomputeInput> = {}): RecomputeInput {
|
||||||
|
return {
|
||||||
|
level: 4,
|
||||||
|
className: 'Fighter',
|
||||||
|
ancestryName: 'Human',
|
||||||
|
heritageName: undefined,
|
||||||
|
abilities: { STR: 18, DEX: 14, CON: 16, INT: 10, WIS: 12, CHA: 10 },
|
||||||
|
skills: {},
|
||||||
|
feats: new Set(),
|
||||||
|
ancestryHp: 8,
|
||||||
|
classHp: 10,
|
||||||
|
armorAc: 4,
|
||||||
|
armorProficiency: 'EXPERT',
|
||||||
|
dexCap: 2,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyProgression(level: number): ClassProgressionRow {
|
||||||
|
return {
|
||||||
|
className: 'Fighter',
|
||||||
|
level,
|
||||||
|
grants: [],
|
||||||
|
proficiencyChanges: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('recomputeDerivedStats — hpMax', () => {
|
||||||
|
it('computes hpMax = ancestryHP + (classHP + conMod) × newLevel for L4 → L5 Fighter, CON 16', () => {
|
||||||
|
// CON 16 → mod +3; classHP 10 + 3 = 13 per level; ancestryHP 8; L5: 8 + 13×5 = 73
|
||||||
|
const ch = baseCharacter();
|
||||||
|
const choices: WizardChoices = {};
|
||||||
|
const result = recomputeDerivedStats(ch, choices, emptyProgression(5));
|
||||||
|
expect(result.hpMax).toBe(73);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('respects boost-cap-at-18 when CON is boosted from 18', () => {
|
||||||
|
// CON starts at 18, boost target includes CON → new CON = 19 (not 20). conMod = +4.
|
||||||
|
// L5: 8 + (10+4)×5 = 78
|
||||||
|
const ch = baseCharacter({
|
||||||
|
abilities: { STR: 16, DEX: 14, CON: 18, INT: 10, WIS: 12, CHA: 10 },
|
||||||
|
});
|
||||||
|
const choices: WizardChoices = { boostTargets: ['CON', 'STR', 'DEX', 'INT'] };
|
||||||
|
const result = recomputeDerivedStats(ch, choices, emptyProgression(5));
|
||||||
|
expect(result.hpMax).toBe(78);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('recomputeDerivedStats — proficiencyChanges from ClassProgression', () => {
|
||||||
|
it('applies proficiencyChanges from ClassProgression at the new level (fortitude EXPERT)', () => {
|
||||||
|
// Fighter L4 → L5 with progression bumping fort to EXPERT.
|
||||||
|
// CON mod +3, level 5; fort = conMod + (level + 4) = 3 + 9 = 12
|
||||||
|
const ch = baseCharacter();
|
||||||
|
const progression: ClassProgressionRow = {
|
||||||
|
...emptyProgression(5),
|
||||||
|
proficiencyChanges: { fortitude: 'EXPERT' },
|
||||||
|
};
|
||||||
|
const result = recomputeDerivedStats(ch, {}, progression);
|
||||||
|
expect(result.fortitude).toBe(12);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('recomputeDerivedStats — Pitfall #9 (hpCurrent must not be in output)', () => {
|
||||||
|
it('does NOT include hpCurrent in the result object', () => {
|
||||||
|
const ch = baseCharacter();
|
||||||
|
const result = recomputeDerivedStats(ch, {}, emptyProgression(5));
|
||||||
|
expect(result).not.toHaveProperty('hpCurrent');
|
||||||
|
expect(result).not.toHaveProperty('hpTemp');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('recomputeDerivedStats — level passthrough', () => {
|
||||||
|
it('returns the new level in the output', () => {
|
||||||
|
const ch = baseCharacter();
|
||||||
|
const result = recomputeDerivedStats(ch, {}, emptyProgression(5));
|
||||||
|
expect(result.level).toBe(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user