diff --git a/TODO.md b/TODO.md index 3b54537..19b840f 100644 --- a/TODO.md +++ b/TODO.md @@ -79,6 +79,18 @@ sequence exactly (damage, heal, conditions, toggleActive, deathSave, remove, add, edit, pause/resume, reorder, revive-between-rounds). +### BUG-6: reorderParticipants doesn't update turnOrderIds +- Test: `shared/tests/turn.reorder.test.js` 'reorder updates turnOrderIds' (RED). +- `reorderParticipants(enc, draggedId, targetId)` swaps two same-initiative + participants in `participants[]` array but leaves `turnOrderIds` unchanged. +- nextTurn rotates via `turnOrderIds` only → reorder has NO effect on combat + rotation. Mid-encounter drag-drop = pointless. +- replay-combat.js calls reorderParticipants with WRONG signature + `(enc, reorderedArray)` — swallowed by try/catch, silent no-op. So + replay never exercised real path either. +- Fix: reorder must also update turnOrderIds to match new participant order + (within same-initiative tie). + ## Pipeline - [ ] Red test: dead participant still in turnOrderIds, turn still advances to them - [ ] Fix `shared/turn.js`: don't drop dead from turn order diff --git a/shared/tests/turn.reorder.test.js b/shared/tests/turn.reorder.test.js new file mode 100644 index 0000000..0ef86d8 --- /dev/null +++ b/shared/tests/turn.reorder.test.js @@ -0,0 +1,66 @@ +// Characterization for reorderParticipants correct usage. +// replay-combat.js calls it with wrong signature (swallowed by try/catch), +// so real behavior untested. Lock what it actually does. + +const shared = require('@ttrpg/shared'); +const { makeParticipant, startEncounter, nextTurn, reorderParticipants } = shared; + +function p(id, init, extra = {}) { + return makeParticipant({ + id, name: id, type: 'monster', + 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('reorderParticipants', () => { + test('swaps two same-initiative participants', () => { + const ps = [p('a', 10), p('b', 20), p('c', 20)]; // b,c tie + let e = enc(ps); + e = { ...e, ...startEncounter(e).patch }; + // initial order: b,c,a (init 20,20,10) + expect(e.turnOrderIds).toEqual(['b', 'c', 'a']); + const r = reorderParticipants(e, 'c', 'b'); + expect(r.patch.participants.map(p => p.id)).toEqual(['a', 'c', 'b']); + }); + + test('throws if initiatives differ', () => { + const ps = [p('a', 10), p('b', 20)]; + let e = enc(ps); + e = { ...e, ...startEncounter(e).patch }; + expect(() => reorderParticipants(e, 'a', 'b')).toThrow(); + }); + + test('throws if id not found', () => { + const ps = [p('a', 10), p('b', 20)]; + let e = enc(ps); + e = { ...e, ...startEncounter(e).patch }; + expect(() => reorderParticipants(e, 'a', 'zzz')).toThrow(); + }); + + test('does NOT touch turnOrderIds (only reorders participants array)', () => { + // Documents current behavior. If reorder is meant to affect combat + // rotation mid-encounter, this is BUG-6. + const ps = [p('a', 10), p('b', 20), p('c', 20)]; + let e = enc(ps); + e = { ...e, ...startEncounter(e).patch }; + const r = reorderParticipants(e, 'c', 'b'); + expect(r.patch.turnOrderIds).toBeUndefined(); + }); + + // BUG-6 candidate: reorder should affect turnOrderIds so mid-combat + // drag-drop changes who goes next within same-initiative tie. + // Currently RED (turnOrderIds not in patch). + test('reorder updates turnOrderIds to reflect new participant order', () => { + const ps = [p('a', 10), p('b', 20), p('c', 20)]; + let e = enc(ps); + e = { ...e, ...startEncounter(e).patch }; + // order: b,c,a + e = { ...e, ...reorderParticipants(e, 'c', 'b').patch }; + expect(e.turnOrderIds).toEqual(['c', 'b', 'a']); + }); +});