# Death Saves 5e - Implementation Plan TDD required. Tests first, implementation after. ## Gap Analysis **Current:** - ✅ Separate success/fail counters (deathSaves, deathFails) - ✅ 3 success → stable, 3 fail → dead - ✅ UI shows ✓/✕ buttons, status badges **Missing (from DEATH_SAVES.md):** - ❌ Nat 20: 1 HP + conscious, reset counters - ❌ Nat 1: 2 failures - ❌ Damage at 0 HP → add failures (normal=1, crit=2) - ❌ Massive damage: damage >= currentHp + maxHp → instant dead - ❌ Stabilize manual action - ❌ Healing resets counters - ❌ Damage breaks stable → back to dying **Test Gap:** - turn.deathsave.test.js: basic counters only - turn.combat.test.js: single deathSave call, no coverage - Combat.scenario.test.js: basic fail/death flow - No nat20, nat1, damage-at-0, massive damage, stabilize, healing integration ## Phase 1: Extend Tests (TDD) ### File: `shared/tests/turn.deathsave.nat20.test.js` Test cases: - Nat20 → hp=1, deathSaves=0, deathFails=0, isDying=false, isStable=false, status="conscious" - Nat20 overrides existing saves/fails - Nat20 log entry correct ### File: `shared/tests/turn.deathsave.nat1.test.js` Test cases: - Nat1 → deathFails += 2 - Nat1 triggers instant death (3 fails total) - Nat1 log entry correct ### File: `shared/tests/turn.deathsave.damage.test.js` Test cases: - Damage at 0 HP → deathFails += 1 - Crit damage at 0 HP → deathFails += 2 - Mixed: save success + damage → independent tracking works - Damage log includes "while dying" ### File: `shared/tests/turn.deathsave.massive.test.js` Test cases: - Damage >= currentHp + maxHp → instant dead - Bypasses death saves entirely - Log entry shows "massive damage" ### File: `shared/tests/turn.deathsave.stabilize.test.js` Test cases: - Stabilize → hp=0, isStable=true, isDying=false, counters=0 - Stabilize from dying state - Stabilize log entry correct ### File: `shared/tests/turn.deathsave.heal.test.js` Test cases: - Healing at 0 HP → reset counters, isDying=false, isStable=false - Healing at stable → same reset - Healing log includes "cleared death saves" ### File: `shared/tests/turn.deathsave.stable.test.js` Test cases: - Damage to stable → isStable=false, isDying=true, counters reset - Stable takes damage → fresh death save loop ### File: `shared/tests/turn.deathsave.undo.test.js` Test cases: - Undo deathSave restores previous state - Undo nat20 restores counters - Undo damage at 0 HP restores fail count - Undo stabilize restores dying state ## Phase 2: Combat Test Integration ### Update `shared/tests/turn.combat.test.js` Add phases: - Nat20 phase: roll nat20, verify 1 HP + conscious - Nat1 phase: roll nat1, verify 2 failures - Damage-at-zero phase: normal damage + crit damage at 0 HP - Massive damage phase: instant death bypass - Stabilize phase: stabilize dying character - Healing-reset phase: heal at 0 HP, verify counters reset - Assert death save state at each phase ### Update `src/tests/Combat.scenario.test.js` Add action functions: - deathSaveActionNat20(name) - deathSaveActionNat1(name) - stabilizeAction(name) - applyDamageAtZero(name, amount, isCrit) - applyMassiveDamage(name) - healingResetAction(name, amount) Add scenario phases: - Round 10-20: nat20/nat1 scenarios - Round 30-40: damage-at-zero scenarios (normal + crit) - Round 50-60: stabilize scenarios - Round 70-80: massive damage scenarios - Round 90-100: healing-reset scenarios - Verify log entries include all death save types ### Update `scripts/combat.js` Verify death save events: - Check death_save events in log - Verify nat20, nat1, damage_at_zero, massive_damage log types - Verify stabilize events - Verify healing_reset events ## Phase 3: Extend `deathSave` Function Add roll result parameter: ```ts async function deathSave(encounter, participantId, type, n, rollResult, ctx) { // rollResult: number (1-20) or null for manual input } ``` Handle nat20: ```ts if (rollResult === 20) { updates.hp = 1; updates.deathSaves = 0; updates.deathFails = 0; updates.isDying = false; updates.isStable = false; status = 'conscious'; } ``` Handle nat1: ```ts if (rollResult === 1) { const currentFails = participant.deathFails || 0; updates.deathFails = Math.min(3, currentFails + 2); // Check for death... } ``` ## Phase 4: Extend `applyHpChange` Function Add damage-at-zero logic: ```ts if (changeType === 'damage' && participant.currentHp === 0) { const failuresToAdd = isCritical ? 2 : 1; const currentFails = participant.deathFails || 0; updates.deathFails = Math.min(3, currentFails + failuresToAdd); updates.isStable = false; // Check for death... } ``` Add massive damage rule: ```ts if (changeType === 'damage' && damage >= participant.currentHp + participant.maxHp) { updates.hp = 0; updates.isDying = true; updates.deathSaves = 0; updates.deathFails = 3; updates.isStable = false; } ``` Add healing reset logic: ```ts if (changeType === 'heal' && (participant.currentHp === 0 || participant.isStable || participant.isDying)) { updates.deathSaves = 0; updates.deathFails = 0; updates.isDying = false; updates.isStable = false; } ``` ## Phase 5: New `stabilizeParticipant` Function ```ts async function stabilizeParticipant(encounter, participantId, ctx) { // hp=0, isStable=true, isDying=false, counters=0 // Log: "Stabilized" } ``` ## Phase 6: UI Extensions ### Phase 6a: Roll Result Modal Add roll result input: - Modal when clicking death save button - Input: d20 result (1-20) or "manual" - Auto-detect nat20/nat1 ### Phase 6b: Nat1/Nat20/Stabilize Buttons Death save row layout: ``` [Saves: □ □ □] [Fails: □ □ □] [Nat1] [Nat20] [Stabilize] ``` **Nat1 button:** - Red icon (AlertTriangle) - Click → deathSave(participantId, 'fail', null, 1, ctx) - Adds 2 failures immediately - Log: "Nat 1! [name] takes 2 death save failures" - Disable if deathFails >= 2 (would cause death anyway) **Nat20 button:** - Gold icon (Zap) - Click → deathSave(participantId, 'success', null, 20, ctx) - Sets hp=1, resets counters, conscious - Log: "Nat 20! [name] restored to 1 HP, conscious" **Stabilize button:** - Emerald icon (HeartPulse) - Only show when dying (hp=0, !isStable, !dead) - Click → stabilizeParticipant(participantId, ctx) - Sets isStable=true, counters=0 - Log: "[name] stabilized at 0 HP" - Hide if already isStable **Conditional visibility:** - All buttons: only when hp === 0 && !dead - Stabilize: hide if already isStable - Nat1: disable if deathFails >= 2 ### Phase 6c: Crit Checkbox for Damage Add crit checkbox: - For damage at 0 HP - Pass crit to applyHpChange - UI: checkbox "Critical hit?" ### Phase 6d: Enhanced Log Messages - Nat20: "Nat 20! [name] restored to 1 HP, conscious" - Nat1: "Nat 1! [name] takes 2 death save failures" - Damage at 0: "[name] takes [n] damage while dying" - Massive damage: "Massive damage! [name] instantly killed" - Stabilize: "[name] stabilized at 0 HP" - Healing reset: "[name] healed to [n] HP, death saves cleared" ## Phase 7: Integration Tests File: `shared/tests/turn.deathsave.flow.test.js` Test full scenarios: - Dying → nat20 → conscious → take damage → dying - Dying → nat1 → 2 fails → another fail → dead - Dying → stabilize → damage → dying again - Massive damage bypasses all - Healing resets mid-death-save loop ## Execution Order 1. Phase 1: Write all test files (fail expected) 2. Phase 2: Update combat tests (fail expected) 3. Phase 3: Extend deathSave, run tests 4. Phase 4: Extend applyHpChange, run tests 5. Phase 5: Add stabilizeParticipant, run tests 6. Phase 6a: Roll result modal 7. Phase 6b: Nat1/Nat20/Stabilize buttons 8. Phase 6c: Crit checkbox 9. Phase 6d: Enhanced log messages 10. Phase 7: Integration tests 11. Manual testing: full scenarios in dev server ## Risk Notes - Roll result input: optional field (DM may not want to input every roll) - Crit detection: manual checkbox (no way to auto-detect) - Massive damage: optional rule (some tables ignore) - Backward compat: existing API must still work with null rollResult - Nat1/Nat20/Stabilize buttons: DM-optional, not required to use