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:
@@ -1,6 +1,9 @@
|
||||
// M4 desired behavior: dead PC stays in turn order, turn still comes up,
|
||||
// deathSave fires. Current code filters isActive (set false on death) so
|
||||
// dead participants are SKIPPED. Test asserts desired state = RED on current.
|
||||
// Unified behavior (App main): death flips isActive=false, dead participant
|
||||
// removed from active rotation, skipped by nextTurn. deathSave is a manual
|
||||
// DM action (button), not tied to rotation. turnOrderIds unchanged on death
|
||||
// (only isActive flag flips). Revive (heal from 0) reactivates.
|
||||
//
|
||||
// Previous "M4: dead stays in rotation" concept reversed by unification.
|
||||
|
||||
const shared = require('@ttrpg/shared');
|
||||
const { makeParticipant, startEncounter, nextTurn, applyHpChange, deathSave } = shared;
|
||||
@@ -24,8 +27,8 @@ function enc(ps) {
|
||||
round:0, currentTurnParticipantId:null, turnOrderIds:[] };
|
||||
}
|
||||
|
||||
describe('M4: dead participants stay in turn order', () => {
|
||||
test('dead PC not removed from turnOrderIds', () => {
|
||||
describe('unified: death deactivates, dead skipped in rotation', () => {
|
||||
test('dead PC not removed from turnOrderIds (stays in list, inactive)', () => {
|
||||
const ps = [pc('a', 20), pc('b', 15), pc('c', 10)];
|
||||
let e = enc(ps);
|
||||
e = { ...e, ...startEncounter(e).patch };
|
||||
@@ -35,39 +38,53 @@ describe('M4: dead participants stay in turn order', () => {
|
||||
expect(e.turnOrderIds).toEqual(orderBefore);
|
||||
});
|
||||
|
||||
test('dead PC turn still comes up (nextTurn visits them)', () => {
|
||||
test('dead PC skipped by nextTurn (isActive=false)', () => {
|
||||
const ps = [pc('a', 20), pc('b', 15), pc('c', 10)];
|
||||
let e = enc(ps);
|
||||
e = { ...e, ...startEncounter(e).patch };
|
||||
// kill b
|
||||
e = { ...e, ...applyHpChange(e, 'b', 'damage', 100).patch };
|
||||
// advance: a→b→c. b's turn should come up.
|
||||
// advance: a→c (b skipped, inactive)
|
||||
e = { ...e, ...nextTurn(e).patch };
|
||||
expect(e.currentTurnParticipantId).toBe('b');
|
||||
expect(e.currentTurnParticipantId).toBe('c');
|
||||
});
|
||||
|
||||
test('dead PC on their turn can deathSave', () => {
|
||||
test('dead PC deathSave fires on manual call (not via rotation)', () => {
|
||||
const ps = [pc('a', 20), pc('b', 15)];
|
||||
let e = enc(ps);
|
||||
e = { ...e, ...startEncounter(e).patch };
|
||||
// kill b (current = a)
|
||||
e = { ...e, ...applyHpChange(e, 'b', 'damage', 100).patch };
|
||||
// advance to b's turn
|
||||
e = { ...e, ...nextTurn(e).patch };
|
||||
expect(e.currentTurnParticipantId).toBe('b');
|
||||
// b is dead, on their turn: deathSave should not throw
|
||||
// b is dead: DM can still fire deathSave (manual action)
|
||||
const r = deathSave(e, 'b', 1);
|
||||
expect(r.patch).toBeTruthy();
|
||||
const b = r.patch.participants.find(x => x.id === 'b');
|
||||
expect(b.deathSaves).toBe(1);
|
||||
});
|
||||
|
||||
test('dead PC not auto-set isActive=false by applyHpChange', () => {
|
||||
// D1 unification: shared now matches App main — death flips isActive=false.
|
||||
// Dead participant removed from active rotation (skipped by nextTurn).
|
||||
test('dead PC auto-set isActive=false by applyHpChange', () => {
|
||||
const ps = [pc('a', 20), pc('b', 15)];
|
||||
let e = enc(ps);
|
||||
e = { ...e, ...startEncounter(e).patch };
|
||||
e = { ...e, ...applyHpChange(e, 'b', 'damage', 100).patch };
|
||||
const b = e.participants.find(x => x.id === 'b');
|
||||
expect(b.isActive).toBe(true);
|
||||
expect(b.isActive).toBe(false);
|
||||
});
|
||||
|
||||
test('revive (heal from 0) reactivates participant', () => {
|
||||
const ps = [pc('a', 20), pc('b', 15)];
|
||||
let e = enc(ps);
|
||||
e = { ...e, ...startEncounter(e).patch };
|
||||
e = { ...e, ...applyHpChange(e, 'b', 'damage', 100).patch };
|
||||
const dead = e.participants.find(x => x.id === 'b');
|
||||
expect(dead.isActive).toBe(false);
|
||||
// heal b back up
|
||||
e = { ...e, ...applyHpChange(e, 'b', 'heal', 50).patch };
|
||||
const revived = e.participants.find(x => x.id === 'b');
|
||||
expect(revived.isActive).toBe(true);
|
||||
expect(revived.currentHp).toBe(50);
|
||||
expect(revived.deathSaves).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user