2026-07-04 17:06:07 -04:00
|
|
|
// 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.
|
2026-07-06 10:31:46 -04:00
|
|
|
//
|
|
|
|
|
// New API: mutating funcs are async, take ctx, return newEnc.
|
2026-07-04 17:06:07 -04:00
|
|
|
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
makeParticipant,
|
|
|
|
|
startEncounter, nextTurn, toggleParticipantActive,
|
2026-07-06 10:31:46 -04:00
|
|
|
} = require('@ttrpg/shared');
|
|
|
|
|
const { mockCtx } = require('./_helpers');
|
2026-07-04 17:06:07 -04:00
|
|
|
|
|
|
|
|
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', () => {
|
2026-07-06 10:31:46 -04:00
|
|
|
test('no participant acts twice in a round after deact+reactivate', async () => {
|
|
|
|
|
const { ctx } = mockCtx();
|
2026-07-04 17:06:07 -04:00
|
|
|
// [a(10), b(7), c(3)]
|
|
|
|
|
let e = enc([p('a',10),p('b',7),p('c',3)]);
|
2026-07-06 10:31:46 -04:00
|
|
|
e = await startEncounter(e, ctx); // a current, r1
|
2026-07-04 17:06:07 -04:00
|
|
|
|
|
|
|
|
const r1 = [];
|
|
|
|
|
r1.push(e.currentTurnParticipantId); // a acts
|
2026-07-06 10:31:46 -04:00
|
|
|
e = await nextTurn(e, ctx); r1.push(e.currentTurnParticipantId); // b acts
|
2026-07-04 17:06:07 -04:00
|
|
|
|
2026-07-06 10:31:46 -04:00
|
|
|
// 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
|
2026-07-04 17:06:07 -04:00
|
|
|
|
|
|
|
|
// nextTurn from c → round 2 (a). b must NOT re-act in round 1.
|
2026-07-06 10:31:46 -04:00
|
|
|
e = await nextTurn(e, ctx);
|
2026-07-04 17:06:07 -04:00
|
|
|
expect(e.round).toBe(2);
|
|
|
|
|
expect(e.currentTurnParticipantId).toBe('a');
|
|
|
|
|
|
|
|
|
|
// b acted once in r1, must act once in r2
|
2026-07-06 10:31:46 -04:00
|
|
|
e = await nextTurn(e, ctx);
|
2026-07-04 17:06:07 -04:00
|
|
|
expect(e.currentTurnParticipantId).toBe('b');
|
2026-07-06 10:31:46 -04:00
|
|
|
e = await nextTurn(e, ctx);
|
2026-07-04 17:06:07 -04:00
|
|
|
expect(e.currentTurnParticipantId).toBe('c');
|
|
|
|
|
|
|
|
|
|
// per-round visit count
|
|
|
|
|
const bCountR1 = r1.filter(id => id === 'b').length;
|
|
|
|
|
expect(bCountR1).toBe(1);
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-06 10:31:46 -04:00
|
|
|
test('deactivate+reactivate non-current who already acted: no re-act', async () => {
|
|
|
|
|
const { ctx } = mockCtx();
|
2026-07-04 17:06:07 -04:00
|
|
|
// [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)]);
|
2026-07-06 10:31:46 -04:00
|
|
|
e = await startEncounter(e, ctx); // a
|
|
|
|
|
e = await nextTurn(e, ctx); // b
|
|
|
|
|
e = await nextTurn(e, ctx); // c (r1 full: a,b,c)
|
2026-07-04 17:06:07 -04:00
|
|
|
// c is current. deactivate a (not current, already acted).
|
2026-07-06 10:31:46 -04:00
|
|
|
e = await toggleParticipantActive(e, 'a', ctx); // a off, pointer stays c
|
2026-07-04 17:06:07 -04:00
|
|
|
expect(e.currentTurnParticipantId).toBe('c');
|
2026-07-06 10:31:46 -04:00
|
|
|
e = await toggleParticipantActive(e, 'a', ctx); // a on
|
2026-07-04 17:06:07 -04:00
|
|
|
expect(e.currentTurnParticipantId).toBe('c');
|
|
|
|
|
|
|
|
|
|
// nextTurn from c → a (round 2). a acts in r2, once.
|
2026-07-06 10:31:46 -04:00
|
|
|
e = await nextTurn(e, ctx);
|
2026-07-04 17:06:07 -04:00
|
|
|
expect(e.round).toBe(2);
|
|
|
|
|
expect(e.currentTurnParticipantId).toBe('a');
|
|
|
|
|
});
|
|
|
|
|
});
|