diff --git a/shared/tests/turn.bulkadd.test.js b/shared/tests/turn.bulkadd.test.js new file mode 100644 index 0000000..9ed4a3d --- /dev/null +++ b/shared/tests/turn.bulkadd.test.js @@ -0,0 +1,49 @@ +// addParticipants (bulk) must slot by initiative, not append. +const shared = require('@ttrpg/shared'); +const { mockCtx } = require('./_helpers'); +const { buildCharacterParticipant, buildMonsterParticipant, addParticipants, makeParticipant } = shared; + +function enc(ps = []) { + return { name:'t', participants: ps, isStarted: false, isPaused: false, + round: 0, currentTurnParticipantId: null, turnOrderIds: [] }; +} +function p(id, init) { + return makeParticipant({ id, name: id, type: 'monster', initiative: init, maxHp: 10, currentHp: 10 }); +} + +describe('addParticipants slot order', () => { + test('bulk add slots by initiative desc', async () => { + const { ctx } = mockCtx(); + let e = enc([]); + e = await addParticipants(e, [p('a', 5), p('b', 20), p('c', 10)], ctx); + expect(e.participants.map(x => x.id)).toEqual(['b', 'c', 'a']); + }); + + test('bulk add preserves existing order + slots new', async () => { + const { ctx } = mockCtx(); + let e = enc([p('a', 20), p('b', 5)]); + e = await addParticipants(e, [p('c', 10)], ctx); + expect(e.participants.map(x => x.id)).toEqual(['a', 'c', 'b']); + }); + + test('bulk add same-init appends after existing (stable tie)', async () => { + const { ctx } = mockCtx(); + let e = enc([p('a', 10)]); + e = await addParticipants(e, [p('b', 10), p('c', 10)], ctx); + expect(e.participants.map(x => x.id)).toEqual(['a', 'b', 'c']); + }); + + test('bulk add mixed inits into existing list', async () => { + const { ctx } = mockCtx(); + let e = enc([p('a', 15), p('b', 5)]); + e = await addParticipants(e, [p('c', 20), p('d', 10), p('e', 15)], ctx); + expect(e.participants.map(x => x.id)).toEqual(['c', 'a', 'e', 'd', 'b']); + }); + + test('bulk add updates turnOrderIds to match', async () => { + const { ctx } = mockCtx(); + let e = enc([]); + e = await addParticipants(e, [p('a', 5), p('b', 20)], ctx); + expect(e.turnOrderIds).toEqual(['b', 'a']); + }); +}); diff --git a/shared/turn.js b/shared/turn.js index 0aa485a..ea17394 100644 --- a/shared/turn.js +++ b/shared/turn.js @@ -479,14 +479,20 @@ async function addParticipant(encounter, participant, ctx) { } async function addParticipants(encounter, newParticipants, ctx) { - const updatedParticipants = [...(encounter.participants || []), ...newParticipants]; + // Slot each new participant into list by initiative (desc), preserve + // existing order + drag-established ties. Matches addParticipant semantics. + let base = [...(encounter.participants || [])]; + for (const np of newParticipants) { + const idx = slotIndexForInit(base, np.initiative); + base.splice(idx, 0, np); + } const names = newParticipants.map(p => p.name).join(', '); - const patch = { participants: updatedParticipants }; + const patch = { participants: base, ...syncTurnOrder(base) }; const log = { type: 'add_participants', message: `Added ${newParticipants.length} participant${newParticipants.length === 1 ? '' : 's'}: ${names}`, delta: { count: newParticipants.length }, - undo: { participants: newParticipants }, + undo: { participants: newParticipants, newTurnOrderIds: patch.turnOrderIds }, }; return commit(encounter, patch, log, ctx); }