// 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. 'use strict'; const shared = require('@ttrpg/shared'); const { makeParticipant, startEncounter, nextTurn, toggleParticipantActive, } = shared; 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:[] }; } const apply = (e, r) => r && r.patch ? { ...e, ...r.patch } : e; describe('BUG-10: deact+reactivate same round', () => { test('no participant acts twice in a round after deact+reactivate', () => { // [a(10), b(7), c(3)] let e = enc([p('a',10),p('b',7),p('c',3)]); e = apply(e, startEncounter(e)); // a current, r1 const r1 = []; r1.push(e.currentTurnParticipantId); // a acts e = apply(e, nextTurn(e)); r1.push(e.currentTurnParticipantId); // b acts // b already acted. Deactivate b (NOT current now — b just became current // via nextTurn, so b IS current). Toggle off advances pointer to c. e = apply(e, toggleParticipantActive(e, 'b')); // b off // current advanced to c (b was current) r1.push(e.currentTurnParticipantId); // c "becomes" active turn // reactivate b same round e = apply(e, toggleParticipantActive(e, 'b')); // b on // nextTurn from c → round 2 (a). b must NOT re-act in round 1. e = apply(e, nextTurn(e)); expect(e.round).toBe(2); expect(e.currentTurnParticipantId).toBe('a'); // b acted once in r1, must act once in r2 e = apply(e, nextTurn(e)); expect(e.currentTurnParticipantId).toBe('b'); e = apply(e, nextTurn(e)); 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', () => { // [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 = apply(e, startEncounter(e)); // a e = apply(e, nextTurn(e)); // b e = apply(e, nextTurn(e)); // c (r1 full: a,b,c) // c is current. deactivate a (not current, already acted). e = apply(e, toggleParticipantActive(e, 'a')); // a off, pointer stays c expect(e.currentTurnParticipantId).toBe('c'); e = apply(e, toggleParticipantActive(e, 'a')); // a on expect(e.currentTurnParticipantId).toBe('c'); // nextTurn from c → a (round 2). a acts in r2, once. e = apply(e, nextTurn(e)); expect(e.round).toBe(2); expect(e.currentTurnParticipantId).toBe('a'); }); });