V2 Phase 3: combat rules automation (conditions, damage types, reminders)
Completes the deployable Phase 1-3 chunk: the combat tracker now runs the math.
- condition-effects engine: declarative CONDITION_EFFECTS_5E/PF2E table +
deriveState(system, speed, conditions) → effective speed / advantage state /
incapacitation / pf2e status penalty. Tracker shows effect badges
("Speed 0", "Disadv. attacks", "−2 status"); pure + 10 unit tests.
- damage types: applyDamage(c, amount, type?) consults snapshotted monster
DamageDefenses (immune→0, resist→halve, vulnerable→double), back-compatible.
Monster add snapshots damageDefenses; tracker shows a type picker only when a
creature has typed defenses, and the log notes resisted/vulnerable amounts.
- concentration + death saves are surfaced as REMINDERS, never auto-rolled:
damaging a concentrating PC logs "roll a DC N Con save…"; a downed PC's turn
logs "make a death saving throw." The player rolls and resolves via the Drop
button / death-save pips they already own. (Per user: no auto dice rolls —
it removes the player from their character.)
Build + 271 unit tests green. Verified live: grapple → Speed 0 badge;
fire-immune creature shows the immune-labelled damage picker.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -161,6 +161,19 @@ describe('HP transitions', () => {
|
||||
expect(applyDamage(c, Number.NaN).hp.current).toBe(10);
|
||||
expect(applyHealing(c, -5).hp.current).toBe(10);
|
||||
});
|
||||
|
||||
it('applies typed-damage resistances when defenses are present', () => {
|
||||
const base = { ...mk('a', 0, 20), damageDefenses: { resist: ['fire'], immune: ['poison'], vulnerable: ['cold'], conditionImmune: [], notes: [] } };
|
||||
expect(applyDamage(base, 10, 'fire').hp.current).toBe(15); // resisted → 5
|
||||
expect(applyDamage(base, 10, 'poison').hp.current).toBe(20); // immune → 0
|
||||
expect(applyDamage(base, 10, 'cold').hp.current).toBe(0); // vulnerable → 20
|
||||
expect(applyDamage(base, 10, 'acid').hp.current).toBe(10); // untracked type → full
|
||||
expect(applyDamage(base, 10).hp.current).toBe(10); // no type → full (back-compat)
|
||||
});
|
||||
|
||||
it('typeless damage is unchanged when no defenses are set', () => {
|
||||
expect(applyDamage(mk('a', 0, 10), 4, 'fire').hp.current).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('endEncounter', () => {
|
||||
|
||||
Reference in New Issue
Block a user