5.4 KiB
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)
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 <= 0and character is not already dying and notis_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)
- Roll 1d4 server-side, add CON modifier (from
-
If new
hp_current > 0and 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_remainingby 1 - For any that reach 0:
- Delete the Dying condition
- Set
is_dead = trueon 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
rollDieutility indice.ts) - Emit a
roll:resultevent to the campaign room (same schema as existing dice rolls) withlabel: "Death Save"so it appears in the existing roll log UI - If result >= 18:
- Set
hp_current = 1 - Delete Dying condition
- Broadcast
character:updated
- Set
- 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-rollto 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_deadcharacter receives healing: PATCH still updateshp_current;is_deadis only cleared by a new "Revive" action on the character card (DM only) — a button that setsis_dead = falseandhp_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) |