# Death Timer — Design Spec Last updated: 2026-04-12 --- ## Goal Implement Shadowdark's dying mechanic: when a character drops to 0 HP they enter a dying state with a 1d4 + CON modifier round countdown. Each party turn they can roll d20 for recovery (18–20 = stand at 1 HP). If the timer expires they are marked dead. --- ## Data Model ### Dying state — `character_conditions` (existing table) When HP hits 0, insert a condition row: ``` name: "Dying" rounds_remaining: 1d4 + CON modifier (minimum 1, rolled server-side) description: "" (unused for this condition) ``` Using the existing conditions table keeps dying state consistent with the spell mishap system and is automatically cleaned up when a character is healed above 0 HP. ### Permanent death — `characters` table (new column) ```sql ALTER TABLE characters ADD COLUMN is_dead BOOLEAN NOT NULL DEFAULT FALSE; ``` `is_dead` is set when the timer reaches 0. It is a persistent character flag — not a condition — because death survives session resets. The DM can manually clear it (via the existing character edit flow) if story reasons bring a character back. --- ## Server Triggers ### 1. HP hits 0 — auto-start dying In `PATCH /characters/:id`, after writing `hp_current`: - If new `hp_current <= 0` and character is not already dying and not `is_dead`: - Roll 1d4 server-side, add CON modifier (from `character_stats`), clamp to minimum 1 - Insert Dying condition with `rounds_remaining = result` - Broadcast `character:updated` (includes conditions array) - If new `hp_current > 0` and character has a Dying condition: - Delete the Dying condition (healed out of dying state) - Broadcast `character:updated` ### 2. Party turn starts — tick the timer In the `initiative:next` socket handler, when `current_side` flips to `"party"`: - Query all Dying conditions for characters in this campaign - Decrement each `rounds_remaining` by 1 - For any that reach 0: - Delete the Dying condition - Set `is_dead = true` on the character - Broadcast `character:updated` - Broadcast updated conditions for remaining dying characters via `character:updated` ### 3. Recovery roll — `death:recovery-roll` New socket event emitted by the client when the DM clicks Roll Recovery. Payload: `{ campaignId, characterId }` Server handler: - Verify caller is DM of the campaign - Verify character has a Dying condition - Roll d20 server-side (via existing `rollDie` utility in `dice.ts`) - Emit a `roll:result` event to the campaign room (same schema as existing dice rolls) with `label: "Death Save"` so it appears in the existing roll log UI - If result >= 18: - Set `hp_current = 1` - Delete Dying condition - Broadcast `character:updated` - Otherwise: no state change (character remains dying) --- ## Client UI ### Character card — dying indicator On `CharacterCard`, when `conditions` includes a "Dying" entry: - Add a pulsing red border to the card - Show `💀 N` (skull + rounds remaining) in the vitals row alongside the HP bar When `is_dead === true`: - Card renders with a muted/grey visual treatment - Skull icon persists, HP locked at 0 - No action buttons (HP +/- disabled) ### Initiative tracker — recovery roll button In `InitiativeTracker`, in the party combatant list, for each character with a Dying condition: - Show `"💀 Dying (N rounds)"` label inline - Show `"⚀ Roll Recovery"` button (DM only) - Clicking emits `death:recovery-roll` to the server - Button disappears on success (character stands) or death (timer expires) ### Roll log Recovery rolls appear with a `"Death Save"` label: - Failure: `"Morgana — Death Save: 14"` - Success: `"Morgana — Death Save: 19 — stands at 1 HP!"` --- ## Edge Cases - **Already dying when HP drops to 0 again:** No new condition inserted; existing timer continues. - **Healed above 0 while dying:** Dying condition deleted on the same PATCH that raises HP. No recovery roll needed. - **CON modifier is negative:** `rounds_remaining = max(1, 1d4 + CON_mod)` — always at least 1 round. - **Combat ends while character is dying:** Timer stops ticking (no initiative turns). Dying condition persists so it resumes if combat restarts. DM can manually heal or mark dead. - **`is_dead` character receives healing:** PATCH still updates `hp_current`; `is_dead` is only cleared by a new "Revive" action on the character card (DM only) — a button that sets `is_dead = false` and `hp_current = 1`. This prevents accidental revival from a stray HP edit. The Revive action is in scope for this feature. - **Multiple dying characters same round:** All timers decrement together on the party turn flip; all death broadcasts fire in the same handler. --- ## Files Affected | File | Change | |---|---| | `server/migrations/006_death_timer.sql` | Add `is_dead` column to `characters` | | `server/src/routes/characters.ts` | Auto-start/clear dying on HP change | | `server/src/socket.ts` | Tick timer on `initiative:next`; handle `death:recovery-roll` | | `client/src/types.ts` | Add `is_dead` to `Character` type | | `client/src/components/CharacterCard.tsx` | Dying border + countdown; dead state | | `client/src/components/InitiativeTracker.tsx` | Dying label + Roll Recovery button | | `client/src/components/InitiativeTracker.tsx` | Emit `death:recovery-roll` (already listed above, also handles Revive button) | | `client/src/components/CharacterCard.tsx` | Revive button (DM only, appears when `is_dead`) |