// Unified behavior (App main): death flips isActive=false, dead participant // removed from active rotation, skipped by nextTurn. deathSave is a manual // DM action (button), not tied to rotation. turnOrderIds unchanged on death // (only isActive flag flips). Revive (heal from 0) reactivates. // // Previous "M4: dead stays in rotation" concept reversed by unification. // // New API: mutating funcs are async, take ctx last, write encounter + log // internally, return newEnc. Chained calls become `e = await fn(e, ctx)`. const shared = require('@ttrpg/shared'); const { makeParticipant, startEncounter, nextTurn, applyHpChange, deathSave } = shared; const { mockCtx } = require('./_helpers'); 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('unified: death deactivates, dead skipped in rotation', () => { test('dead PC not removed from turnOrderIds (stays in list, inactive)', async () => { const { ctx } = mockCtx(); const ps = [pc('a', 20), pc('b', 15), pc('c', 10)]; let e = enc(ps); e = await startEncounter(e, ctx); const orderBefore = e.turnOrderIds.slice(); // kill b e = await applyHpChange(e, 'b', 'damage', 100, ctx); expect(e.turnOrderIds).toEqual(orderBefore); }); test('dead PC skipped by nextTurn (isActive=false)', async () => { const { ctx } = mockCtx(); const ps = [pc('a', 20), pc('b', 15), pc('c', 10)]; let e = enc(ps); e = await startEncounter(e, ctx); // kill b e = await applyHpChange(e, 'b', 'damage', 100, ctx); // advance: a→c (b skipped, inactive) e = await nextTurn(e, ctx); expect(e.currentTurnParticipantId).toBe('c'); }); test('dead PC deathSave fires on manual call (not via rotation)', async () => { const { ctx } = mockCtx(); const ps = [pc('a', 20), pc('b', 15)]; let e = enc(ps); e = await startEncounter(e, ctx); // kill b (current = a) e = await applyHpChange(e, 'b', 'damage', 100, ctx); // b is dead: DM can still fire deathSave (manual action) const { enc: newEnc } = await deathSave(e, 'b', 'fail', 1, ctx); const b = newEnc.participants.find(x => x.id === 'b'); expect(b.deathFails).toBe(1); }); // D1 unification: shared now matches App main — death flips isActive=false. // Dead participant removed from active rotation (skipped by nextTurn). test('dead PC auto-set isActive=false by applyHpChange', async () => { const { ctx } = mockCtx(); const ps = [pc('a', 20), pc('b', 15)]; let e = enc(ps); e = await startEncounter(e, ctx); e = await applyHpChange(e, 'b', 'damage', 100, ctx); const b = e.participants.find(x => x.id === 'b'); expect(b.isActive).toBe(false); }); test('revive (heal from 0) reactivates participant', async () => { const { ctx } = mockCtx(); const ps = [pc('a', 20), pc('b', 15)]; let e = enc(ps); e = await startEncounter(e, ctx); e = await applyHpChange(e, 'b', 'damage', 100, ctx); const dead = e.participants.find(x => x.id === 'b'); expect(dead.isActive).toBe(false); // heal b back up e = await applyHpChange(e, 'b', 'heal', 50, ctx); const revived = e.participants.find(x => x.id === 'b'); expect(revived.isActive).toBe(true); expect(revived.currentHp).toBe(50); expect(revived.deathSaves).toBe(0); }); });