Rework backend #1
@@ -1,35 +1,33 @@
|
|||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
## M4 — Initiative skip bug + dead-participant handling
|
## Milestone M4 — Initiative rotation bugs + features
|
||||||
|
|
||||||
### Dead participants must NOT be skipped in turn order (BUG-3 / M4)
|
Split: bug (rotation corruption) vs feature (dead-participant handling).
|
||||||
- Current: dead (HP=0) → `isActive=false` → removed from turn order → skipped
|
|
||||||
- WRONG. Dead participants still occupy initiative slot.
|
### BUG-5: Initiative skip (mid-round add/revive corrupts rotation)
|
||||||
- PCs (unconscious): death saves still resolve on their turn
|
- **Real bug.** Rotation corrupts when participant added/revived mid-round.
|
||||||
- Monsters/NPCs: may still have reaction/reaction-like considerations
|
- Test: `shared/tests/turn.combat.test.js` (jest, seeded, RED).
|
||||||
- Saw this problem in game Saturday.
|
- 13 dupes / 100 rounds. First at round 4 (Cleric twice).
|
||||||
- Fix: keep dead participants in turnOrderIds; their turn still comes up.
|
- Root cause: `computeTurnOrderAfterAddition` appends id to turnOrderIds
|
||||||
Damage/death-save UI already gated on HP=0 so row buttons stay usable.
|
end. Round wrap re-sorts by initiative. `currentTurnParticipantId` pointer
|
||||||
- Affects: `shared/turn.js` `nextTurn` (filters `isActive`), `applyHpChange`
|
stale → nextTurn revisits.
|
||||||
(sets isActive=false on death), `computeTurnOrderAfterRemoval`.
|
- See full detail below in Confirmed bugs section.
|
||||||
- Characterization tests (`src/tests/Combat.characterization.test.js`) lock CURRENT
|
|
||||||
(buggy) behavior — those tests must be UPDATED to desired behavior, not
|
### FEAT-1: Dead participants should stay in turn order (as-designed→change)
|
||||||
preserved.
|
- **Feature request, not bug.** Current behavior is as-designed (dead =
|
||||||
- RED test locked: `shared/tests/turn.dead-skip.test.js` (4 tests).
|
inactive = skipped). User wants change: dead occupy initiative slot,
|
||||||
|
PCs get death-save turn.
|
||||||
|
- Saw Saturday game.
|
||||||
|
- Desired:
|
||||||
- dead PC not removed from turnOrderIds
|
- dead PC not removed from turnOrderIds
|
||||||
- dead PC turn still comes up (nextTurn visits them)
|
- dead PC turn still comes up (nextTurn visits them)
|
||||||
- dead PC on their turn can deathSave
|
- dead PC on their turn can deathSave
|
||||||
- dead PC not auto-set isActive=false by applyHpChange
|
- dead PC not auto-set isActive=false by applyHpChange
|
||||||
|
- Affects: `shared/turn.js` `nextTurn` (filters `isActive`), `applyHpChange`
|
||||||
### JUMP_TURN_TO(participantId) manual turn override
|
(sets isActive=false on death), `computeTurnOrderAfterRemoval`.
|
||||||
- DM clicks participant → cursor jumps → that participant's turn now.
|
- Characterization tests (`src/tests/Combat.characterization.test.js`) lock
|
||||||
- Future NEXT_TURN continues from jumped position.
|
CURRENT behavior — UPDATE to desired when implementing.
|
||||||
- UI button: "Make This Turn"
|
- RED test locked (desired state): `shared/tests/turn.dead-skip.test.js` (4 tests).
|
||||||
- Backend action: new endpoint or via generic doc patch.
|
|
||||||
- RED test: `shared/tests/turn.jump.test.js` (3 tests, 2 RED).
|
|
||||||
- jump sets currentTurn, future nextTurn continues
|
|
||||||
- jump to first stays same round
|
|
||||||
- jump invalid throws (green via TypeError)
|
|
||||||
|
|
||||||
## Confirmed bugs (tests written, NOT fixed)
|
## Confirmed bugs (tests written, NOT fixed)
|
||||||
|
|
||||||
@@ -80,18 +78,15 @@
|
|||||||
- Test: render App + DisplayView, toggle hide-HP, assert display still shows
|
- Test: render App + DisplayView, toggle hide-HP, assert display still shows
|
||||||
encounter (not paused).
|
encounter (not paused).
|
||||||
|
|
||||||
## Pipeline
|
### BUG-5: mid-round addParticipant/revive corrupts rotation
|
||||||
|
|
||||||
### BUG-5: mid-round addParticipant/revive corrupts rotation (deterministic test)
|
|
||||||
- Test: `shared/tests/turn.combat.test.js` (jest, seeded RNG, RED).
|
- Test: `shared/tests/turn.combat.test.js` (jest, seeded RNG, RED).
|
||||||
- 13 rotation-dupes / 100 rounds. First at round 4 (Cleric twice).
|
- 13 rotation-dupes / 100 rounds. First at round 4 (Cleric twice).
|
||||||
- Pattern: Reinforce/Summon added mid-round → appears in rotation same round
|
- Pattern: Reinforce/Summon added mid-round → appears in rotation same round
|
||||||
→ round wrap re-sorts by initiative → currentTurnParticipantId pointer
|
→ round wrap re-sorts by initiative → currentTurnParticipantId pointer
|
||||||
stale → nextTurn revisits.
|
stale → nextTurn revisits.
|
||||||
- Root cause: `computeTurnOrderAfterAddition` appends id to turnOrderIds
|
- Root cause: `computeTurnOrderAfterAddition` appends id to turnOrderIds
|
||||||
end + `togglePause` resume rebuilds order via sort but doesn't re-anchor
|
end. Round wrap re-sorts by initiative. currentTurn pointer stale after
|
||||||
currentTurn to its new position. After several mid-round adds the pointer
|
sort → drifts → nextTurn revisits.
|
||||||
drifts.
|
|
||||||
- This is the test audit should have been. Mirrors replay-combat.js op
|
- This is the test audit should have been. Mirrors replay-combat.js op
|
||||||
sequence exactly (damage, heal, conditions, toggleActive, deathSave,
|
sequence exactly (damage, heal, conditions, toggleActive, deathSave,
|
||||||
remove, add, edit, pause/resume, reorder, revive-between-rounds).
|
remove, add, edit, pause/resume, reorder, revive-between-rounds).
|
||||||
@@ -120,12 +115,11 @@
|
|||||||
- Fix: `onclose` → reconnect + re-subscribe existing paths.
|
- Fix: `onclose` → reconnect + re-subscribe existing paths.
|
||||||
|
|
||||||
## Pipeline
|
## Pipeline
|
||||||
- [ ] Red test: dead participant still in turnOrderIds, turn still advances to them
|
- [ ] BUG-4: fix setDoc→updateDoc for all 5 activeDisplay sites
|
||||||
- [ ] Fix `shared/turn.js`: don't drop dead from turn order
|
- [ ] BUG-5: fix computeTurnOrderAfterAddition currentTurn re-anchor
|
||||||
- [ ] Update characterization tests to desired (not preserved) behavior
|
- [ ] BUG-6: reorderParticipants update turnOrderIds
|
||||||
(src/tests/Combat.characterization.test.js, etc)
|
- [ ] BUG-8: ws adapter reconnect
|
||||||
- [ ] JUMP_TURN_TO red test
|
- [ ] FEAT-1: dead participants stay in turn order (update characterization)
|
||||||
- [ ] JUMP_TURN_TO impl (shared + UI button)
|
|
||||||
- [ ] M5 docker-compose
|
- [ ] M5 docker-compose
|
||||||
- [ ] M6 undo rework (transactional events table)
|
- [ ] M6 undo rework (transactional events table)
|
||||||
- [ ] M7 Playwright E2E
|
- [ ] M7 Playwright E2E
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
// JUMP_TURN_TO feature: DM clicks participant → turn jumps → future NEXT_TURN
|
|
||||||
// continues from jumped position. Missing feature, not bug.
|
|
||||||
// Test asserts desired behavior = RED (function doesn't exist).
|
|
||||||
|
|
||||||
const shared = require('@ttrpg/shared');
|
|
||||||
const { makeParticipant, startEncounter, nextTurn } = shared;
|
|
||||||
|
|
||||||
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('JUMP_TURN_TO: manual turn override', () => {
|
|
||||||
test('jump sets currentTurn to target, future nextTurn continues', () => {
|
|
||||||
const ps = [p('a',20), p('b',15), p('c',10), p('d',5)];
|
|
||||||
let e = enc(ps);
|
|
||||||
e = { ...e, ...startEncounter(e).patch };
|
|
||||||
// current=a
|
|
||||||
e = { ...e, ...shared.jumpTurnTo(e, 'c').patch };
|
|
||||||
expect(e.currentTurnParticipantId).toBe('c');
|
|
||||||
// next turn continues from c → d
|
|
||||||
e = { ...e, ...nextTurn(e).patch };
|
|
||||||
expect(e.currentTurnParticipantId).toBe('d');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('jump to first stays in same round', () => {
|
|
||||||
const ps = [p('a',20), p('b',15), p('c',10)];
|
|
||||||
let e = enc(ps);
|
|
||||||
e = { ...e, ...startEncounter(e).patch };
|
|
||||||
e = { ...e, ...nextTurn(e).patch }; // b
|
|
||||||
e = { ...e, ...shared.jumpTurnTo(e, 'a').patch };
|
|
||||||
expect(e.round).toBe(1);
|
|
||||||
expect(e.currentTurnParticipantId).toBe('a');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('jump to invalid id throws', () => {
|
|
||||||
const ps = [p('a',20), p('b',15)];
|
|
||||||
let e = enc(ps);
|
|
||||||
e = { ...e, ...startEncounter(e).patch };
|
|
||||||
expect(() => shared.jumpTurnTo(e, 'zzz')).toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user