From c90fc6ffb0461c909ee504b04133e27082f9dbd6 Mon Sep 17 00:00:00 2001 From: david raistrick <1108844+keen99@users.noreply.github.com> Date: Tue, 30 Jun 2026 13:57:55 -0400 Subject: [PATCH] tests: M4 dead-participant skip RED (4 tests, turn.dead-skip.test.js) Desired behavior locked: - dead PC not removed from turnOrderIds - dead PC turn still comes up (nextTurn visits them) - dead PC on their turn can deathSave - dead PC not auto-set isActive=false by applyHpChange All 4 RED on current code. Root cause: nextTurn filters isActive, applyHpChange sets isActive=false on death, computeTurnOrderAfterRemoval drops dead from turnOrderIds. TODO BUG-3/M4 updated with test refs. --- TODO.md | 9 +++- shared/tests/turn.dead-skip.test.js | 73 +++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 shared/tests/turn.dead-skip.test.js diff --git a/TODO.md b/TODO.md index 19b840f..572d83a 100644 --- a/TODO.md +++ b/TODO.md @@ -2,7 +2,7 @@ ## M4 — Initiative skip bug + dead-participant handling -### Dead participants must NOT be skipped in turn order +### Dead participants must NOT be skipped in turn order (BUG-3 / M4) - Current: dead (HP=0) → `isActive=false` → removed from turn order → skipped - WRONG. Dead participants still occupy initiative slot. - PCs (unconscious): death saves still resolve on their turn @@ -14,7 +14,12 @@ (sets isActive=false on death), `computeTurnOrderAfterRemoval`. - Characterization tests (`src/tests/Combat.characterization.test.js`) lock CURRENT (buggy) behavior — those tests must be UPDATED to desired behavior, not - preserved. Red desired-test first, then fix. + preserved. +- RED test locked: `shared/tests/turn.dead-skip.test.js` (4 tests). + - dead PC not removed from turnOrderIds + - dead PC turn still comes up (nextTurn visits them) + - dead PC on their turn can deathSave + - dead PC not auto-set isActive=false by applyHpChange ### JUMP_TURN_TO(participantId) manual turn override - DM clicks participant → cursor jumps → that participant's turn now. diff --git a/shared/tests/turn.dead-skip.test.js b/shared/tests/turn.dead-skip.test.js new file mode 100644 index 0000000..9d68e3b --- /dev/null +++ b/shared/tests/turn.dead-skip.test.js @@ -0,0 +1,73 @@ +// M4 desired behavior: dead PC stays in turn order, turn still comes up, +// deathSave fires. Current code filters isActive (set false on death) so +// dead participants are SKIPPED. Test asserts desired state = RED on current. + +const shared = require('@ttrpg/shared'); +const { makeParticipant, startEncounter, nextTurn, applyHpChange, deathSave } = shared; + +function p(id, init, extra = {}) { + return makeParticipant({ + id, name: id, type: 'monster', + initiative: init, maxHp: 100, currentHp: 100, + ...extra, + }); +} +function pc(id, init, extra = {}) { + return makeParticipant({ + id, name: id, type: 'character', + initiative: init, maxHp: 100, currentHp: 100, + ...extra, + }); +} +function enc(ps) { + return { name:'t', participants:ps, isStarted:false, isPaused:false, + round:0, currentTurnParticipantId:null, turnOrderIds:[] }; +} + +describe('M4: dead participants stay in turn order', () => { + test('dead PC not removed from turnOrderIds', () => { + const ps = [pc('a', 20), pc('b', 15), pc('c', 10)]; + let e = enc(ps); + e = { ...e, ...startEncounter(e).patch }; + const orderBefore = e.turnOrderIds.slice(); + // kill b + e = { ...e, ...applyHpChange(e, 'b', 'damage', 100).patch }; + expect(e.turnOrderIds).toEqual(orderBefore); + }); + + test('dead PC turn still comes up (nextTurn visits them)', () => { + const ps = [pc('a', 20), pc('b', 15), pc('c', 10)]; + let e = enc(ps); + e = { ...e, ...startEncounter(e).patch }; + // kill b + e = { ...e, ...applyHpChange(e, 'b', 'damage', 100).patch }; + // advance: a→b→c. b's turn should come up. + e = { ...e, ...nextTurn(e).patch }; + expect(e.currentTurnParticipantId).toBe('b'); + }); + + test('dead PC on their turn can deathSave', () => { + const ps = [pc('a', 20), pc('b', 15)]; + let e = enc(ps); + e = { ...e, ...startEncounter(e).patch }; + // kill b (current = a) + e = { ...e, ...applyHpChange(e, 'b', 'damage', 100).patch }; + // advance to b's turn + e = { ...e, ...nextTurn(e).patch }; + expect(e.currentTurnParticipantId).toBe('b'); + // b is dead, on their turn: deathSave should not throw + const r = deathSave(e, 'b', 1); + expect(r.patch).toBeTruthy(); + const b = r.patch.participants.find(x => x.id === 'b'); + expect(b.deathSaves).toBe(1); + }); + + test('dead PC not auto-set isActive=false by applyHpChange', () => { + const ps = [pc('a', 20), pc('b', 15)]; + let e = enc(ps); + e = { ...e, ...startEncounter(e).patch }; + e = { ...e, ...applyHpChange(e, 'b', 'damage', 100).patch }; + const b = e.participants.find(x => x.id === 'b'); + expect(b.isActive).toBe(true); + }); +});