// BUG-10: deact+reactivate same round double-acts participant. // OLD bug: reactivation re-inserted by initiative (front of pointer) → // participant acted again before round end. // // 1-list model: toggleActive keeps slot position. Reactivation does NOT // grant second turn. Each participant acts at most once per round. // // Test: walk rounds, count visits. Deactivate+reactivate mid-round must // not cause any participant to act twice in same round. // // New API: mutating funcs are async, take ctx, return newEnc. 'use strict'; const { makeParticipant, startEncounter, nextTurn, toggleParticipantActive, } = require('@ttrpg/shared'); const { mockCtx } = require('./_helpers'); function p(id, init) { return makeParticipant({ id, name: id, type: 'monster', initiative: init, maxHp: 100, currentHp: 100 }); } function enc(ps) { return { name:'t', participants:ps, isStarted:false, isPaused:false, round:0, currentTurnParticipantId:null, turnOrderIds:[] }; } describe('BUG-10: deact+reactivate same round', () => { test('no participant acts twice in a round after deact+reactivate', async () => { const { ctx } = mockCtx(); // [a(10), b(7), c(3)] let e = enc([p('a',10),p('b',7),p('c',3)]); e = await startEncounter(e, ctx); // a current, r1 const r1 = []; r1.push(e.currentTurnParticipantId); // a acts e = await nextTurn(e, ctx); r1.push(e.currentTurnParticipantId); // b acts // b already acted and is still current. Deactivate b: status edit only, // no turn advance. DM clicks Next Turn explicitly; nextTurn skips inactive b. e = await toggleParticipantActive(e, 'b', ctx); // b off, current still b expect(e.currentTurnParticipantId).toBe('b'); e = await nextTurn(e, ctx); // skips inactive b → c r1.push(e.currentTurnParticipantId); // reactivate b same round; b must not re-act before round wraps. e = await toggleParticipantActive(e, 'b', ctx); // b on // nextTurn from c → round 2 (a). b must NOT re-act in round 1. e = await nextTurn(e, ctx); expect(e.round).toBe(2); expect(e.currentTurnParticipantId).toBe('a'); // b acted once in r1, must act once in r2 e = await nextTurn(e, ctx); expect(e.currentTurnParticipantId).toBe('b'); e = await nextTurn(e, ctx); expect(e.currentTurnParticipantId).toBe('c'); // per-round visit count const bCountR1 = r1.filter(id => id === 'b').length; expect(bCountR1).toBe(1); }); test('deactivate+reactivate non-current who already acted: no re-act', async () => { const { ctx } = mockCtx(); // [a(10), b(7), c(3)]. a acts, b acts, c acts (round 1 done). // Then deactivate a (already acted, not current since c is current). // Reactivate a. a must not act again until round 2. let e = enc([p('a',10),p('b',7),p('c',3)]); e = await startEncounter(e, ctx); // a e = await nextTurn(e, ctx); // b e = await nextTurn(e, ctx); // c (r1 full: a,b,c) // c is current. deactivate a (not current, already acted). e = await toggleParticipantActive(e, 'a', ctx); // a off, pointer stays c expect(e.currentTurnParticipantId).toBe('c'); e = await toggleParticipantActive(e, 'a', ctx); // a on expect(e.currentTurnParticipantId).toBe('c'); // nextTurn from c → a (round 2). a acts in r2, once. e = await nextTurn(e, ctx); expect(e.round).toBe(2); expect(e.currentTurnParticipantId).toBe('a'); }); });