refactor: single-source combat logic (App handlers call shared, slot model)
App.js no longer inlines combat logic. All handlers delegate to @ttrpg/shared: startEncounter, nextTurn, togglePause, endEncounter, addParticipant(s), updateParticipant, removeParticipant, toggleParticipantActive, applyHpChange, deathSave, toggleCondition, reorderParticipants. ONE code path for UI. shared/turn.js aligned to slot model (docs/INITIATIVE_ORDERING.md): - addParticipant: splice into slot by initiative (no sort) - updateParticipant: re-slot on initiative change (no sort) - reorderParticipants: same-init drag only (cross-init blocked) - applyHpChange: death flips isActive=false (matches App), revive reactivates No renames, no API changes. Shared func signatures unchanged. Tests updated to match unified behavior: - dead-skip: death deactivates (was FEAT-1 stays active) - characterization: death deactivates + revive reactivates - reorder/invariant: cross-init drag blocked, same-init allowed Both suites green: shared 91/91, App 77/77, 0 warnings.
This commit is contained in:
@@ -213,22 +213,21 @@ describe('applyHpChange', () => {
|
||||
expect(patch.participants[0].currentHp).toBe(10);
|
||||
});
|
||||
|
||||
test('damage to 0 keeps active + stays in turn order (FEAT-1)', () => {
|
||||
// FEAT-1: death no longer deactivates or removes from turn order.
|
||||
// Dead stay in rotation, nextTurn still visits them, PCs get death-save turn.
|
||||
test('damage to 0 deactivates + keeps turn order (unified)', () => {
|
||||
// Unified: death flips isActive=false (removed from active rotation).
|
||||
// turnOrderIds unchanged (no turn-order patch on death).
|
||||
const ps = [p('a', 10, { currentHp: 3 }), p('b', 5)];
|
||||
const e = enc(ps, { isStarted: true, turnOrderIds: ['a', 'b'], currentTurnParticipantId: 'a' });
|
||||
const { patch } = applyHpChange(e, 'a', 'damage', 5);
|
||||
expect(patch.participants[0].currentHp).toBe(0);
|
||||
expect(patch.participants[0].isActive).toBe(true);
|
||||
expect(patch.participants[0].isActive).toBe(false);
|
||||
expect(patch.turnOrderIds).toBeUndefined();
|
||||
expect(patch.currentTurnParticipantId).toBeUndefined();
|
||||
});
|
||||
|
||||
test('heal above 0 resets death saves, keeps active (FEAT-1)', () => {
|
||||
// FEAT-1: revive no longer flips isActive (was already active — death
|
||||
// doesn't deactivate). deathSaves still reset.
|
||||
const ps = [p('a', 10, { currentHp: 0, deathSaves: 2 })];
|
||||
test('heal above 0 reactivates + resets death saves (unified)', () => {
|
||||
// Unified: revive from 0 flips isActive=true, deathSaves reset.
|
||||
const ps = [p('a', 10, { currentHp: 0, isActive: false, deathSaves: 2 })];
|
||||
const { patch } = applyHpChange(enc(ps), 'a', 'heal', 5);
|
||||
expect(patch.participants[0].currentHp).toBe(5);
|
||||
expect(patch.participants[0].isActive).toBe(true);
|
||||
@@ -285,17 +284,17 @@ describe('toggleCondition', () => {
|
||||
});
|
||||
|
||||
describe('reorderParticipants', () => {
|
||||
test('drag before target (1-list, cross-init allowed)', () => {
|
||||
test('drag before target (same-init tie)', () => {
|
||||
const ps = [p('a', 10), p('b', 10), p('c', 10)];
|
||||
const { patch } = reorderParticipants(enc(ps), 'a', 'c');
|
||||
// drag a before c: remove a → [b,c], insert before c → [b,a,c]
|
||||
expect(patch.participants.map(x => x.id)).toEqual(['b', 'a', 'c']);
|
||||
});
|
||||
|
||||
test('cross-init drag allowed (1-list, DM override)', () => {
|
||||
test('cross-init drag blocked (no-op)', () => {
|
||||
const ps = [p('a', 10), p('b', 5)];
|
||||
const { patch } = reorderParticipants(enc(ps), 'a', 'b');
|
||||
expect(patch.participants.map(x => x.id)).toEqual(['a', 'b']);
|
||||
expect(patch).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user