W-020: DND-F02 ECS turn loop¶
Type: feature · Size: M · Epic: E-dnd-m1 · Target: apps/demo-dnd · Breaking: no Depends: W-010, W-011, W-013, W-019, W-027, W-028, W-029, W-032, W-033, W-034, W-036, W-042 Refs: brief DND-F02; demo-dnd spec DND-020..035; ecs-schedule spec SCH-013, SCH-042, SCH-065, SCH-100, SCH-104, SCH-107..109, SCH-118, SCH-126, SCH-127, SCH-132, SCH-140, SCH-143; ecs-prefab spec PREFAB-014, PREFAB-030, PREFAB-066
Goal¶
Make the headless turn loop of apps/demo-dnd work: seeded Rng and initiative, the detached dnd/turn schedule with five phases and nine systems, the advance driver, endTurn, round and active rotation, defeat and outcome, the Cues trait, and restart. This item owns the Intent, Targeting, and Cues world traits, the Effect tag, the HasEffect relation, an empty CombatLog trait, and the registration of all nine dnd/turn systems. The bodies of ai/move and ai/decide (W-022), combat/act (W-021), and effects/tick (W-023) stay empty here. The turn UI (initiative panel, End turn button, victory or defeat banner, restart button) is W-050. Attacks (DND-042) and Poison Strike (DND-082) come in W-021 and W-023, so only endTurn ends a hero turn here.
Steps¶
- S1 (DND-020, DND-021, DND-033; PREFAB-014, PREFAB-030): content gains
initiativeBonusper party entry and monster kind; traitInitiative { value }; world traitRng(AoS{ next(): number }),createSeededRng(seed)androllDie(world, sides)=1 + floor(next() * sides);setupEncounter(world, content?, { seed? })installs a seededRngwhen given a seed, and rolls one d20 +initiativeBonusfor each hero and, as anInitiativeoverride atinstantiate, for each monster instance;initiativeOrder(world)returns living combatants (no templates) byInitiative.valuedescending, ties byentity.id()ascending. Tests with a scriptedRng: values equal d20 + bonus from custom content, two Goblins differ, the template'sInitiativeis unchanged, order and tie-break, templates andDefeatedexcluded, same seed gives the same sequence; files:apps/demo-dnd/src/content/{party,monsters}.ts,apps/demo-dnd/src/ecs/{traits,rng,actions}.ts,apps/demo-dnd/src/ecs/initiative.test.ts - S2 (DND-024, DND-025, DND-026; SCH-013, SCH-100, SCH-104, SCH-118, SCH-126, SCH-127, SCH-132, SCH-140, SCH-143): add
@workspace/ecs-schedule(workspace:*) toapps/demo-dnddependencies if missing (needs user approval); traitsEffect(tag),Intent,Targeting,Cues,CombatLog(emptyentries) and relationHasEffect(relation({ store: { turns: 0, damage: 0 } })); setup addsIntent('none'),Targeting('none'),Cues(three empty arrays) andCombatLogto the world;src/ecs/systems.tsdefines phasesStartOfTurn → Move → Act → Resolve → EndOfTurn(dependsOnchain, no builtin edge), the setdnd/monster-turn(exported,runIf: status'ongoing'and the active combatant is a monster withhp > 0) and registers all nine systems withafteredges and empty bodies (ai/moveandai/decidein the set), in the schedulednd/turn. Tests: every phase is manual (isManual) and depends on the previous one only,systemsOfper phase returns the DND-025 names in order, the setrunIfistrueonly for an active living monster during'ongoing',runPhases(world, Move, Act, 0)with a hero active leaves positions, healths andCombatLogunchanged,schedule.nameisdnd/turn; files:apps/demo-dnd/package.json,apps/demo-dnd/src/ecs/{traits,relations,systems,actions}.ts,apps/demo-dnd/src/ecs/pipeline.test.ts - S3 (DND-022, DND-023, DND-024, DND-027, DND-028, DND-029, DND-030; SCH-042, SCH-107, SCH-108, SCH-109): bodies of
turn/begin(movesLeft= activeSpeed,actedfalse,IntentandTargetingreset) andturn/end(next living combatant after the current one in the DND-021 order including defeated, wrap-around incrementsround; with status not'ongoing'only removesActive);advance(world)(looprunPhase(StartOfTurn); monster active:runPhases(Move, EndOfTurn, 0)and continue; hero active: stop; never clearsCues);endTurn(world)runsrunPhases(Act, EndOfTurn, 0)thenadvanceand returnsfalsewith no change unless status is'ongoing'and a hero is active, consumes no dice, logs nothing; an internalfinishHeroTurn(world)helper that W-021 and W-023 reuse for acting; setup setsround: 1,Activeon the first ofinitiativeOrderand runsadvance. Tests withdt = 0: after setup a hero is active or the encounter is over, exactly oneActivewhile'ongoing'and none otherwise,schedule.run(world, 0)leavesmovesLeftandActiveunchanged, consecutive monster turns resolve in one call in initiative order, A/B/C with B defeated goes A to C and C to A withround+ 1,endTurnreturnsfalsefor a monster active or status over, scriptedRngshows no dice used; files:apps/demo-dnd/src/ecs/{systems,actions}.ts,apps/demo-dnd/src/ecs/turns.test.ts - S4 (DND-029, DND-031, DND-032, DND-035): bodies of
combat/defeat(addsDefeatedathp0),encounter/outcome('victory'when every monster isDefeated,'defeat'when every hero is),turn/cues(ownedcreateAdded(Defeated),createAdded(HasEffect)andcreateRemoved(HasEffect)queries; each run appends to theCuesarrays throughworld.setwith new arrays, in order, no duplicates);moveActiveandendTurnclearCuesfirst when they returntrueand leave it untouched when they returnfalse; setup clearsCues. Tests: defeated combatant leavesinitiativeOrder, frees its cell (DND-013), gets no turn, victory and defeat set status,advancestops,moveActiveandendTurnreturnfalseonce over,Cues.defeatedstill holds a monster defeated before anendTurnafter two monster turns follow in the same call, and is cleared after the next accepted action; files:apps/demo-dnd/src/ecs/{systems,actions}.ts,apps/demo-dnd/src/ecs/resolve.test.ts - S5 (DND-033, DND-034; PREFAB-066, SCH-065):
restartEncounter(world)callsworld.reset()thensetupEncounterwith the content and seed remembered bysetupEncounterin aWeakMapkeyed by world inactions.ts(a scriptedRngis not kept; no seed means an unseeded restart). Tests: old entity handles are dead (PREFAB-066), round 1, status'ongoing',CombatLogandCuesempty, new templates,world.get(schedule.state)back to defaults afterworld.reset()(SCH-065), and a seeded scripted fight (moveActiveandendTurninputs) played, restarted and replayed ends with the same combatant state,CuesandCombatLogentries, and two worlds with the same seed and inputs also match; files:apps/demo-dnd/src/ecs/actions.ts,apps/demo-dnd/src/ecs/restart.test.ts - S6 (acceptance): run
pnpm turbo run typecheck lint test --filter=demo-dnd(no[manual]requirement in this item)
Log¶
- 2026-11-01 created from the approved spec (Triage). Meets ready criteria (size M, 6 steps); held in Later until Depends are done
- 2026-11-01 open deps, not done: ecs-schedule W-027..W-034 and W-036 (phases, systems, run and stepping, conditions, sets, per-world state, systemsOf) for S2 and S3. ecs-prefab W-042 (handles dead after reset, PREFAB-066) for S6. The board Depends lists W-010, W-011, W-013, W-019 only
- 2026-11-01 spec defaults applied: OQ5 acting ends the hero turn with no move after it (DND-029); OQ6 consecutive monster turns resolve in one action call (DND-028); OQ10 initiative is d20 plus bonus rolled at setup, ties by entity id (DND-020, DND-021)
- 2026-11-01 OQ13 (restart via
world.reset()): DND-037 is the canary. If the banner does not show the new encounter, the restart design changes (DND-034). Triage decides, not this item - 2026-11-01 DND-034 requires observers of DND-052 and DND-089 to be registered again. Those observers come with W-021 and W-023, so that part of S6 is checked when they land
- 2026-11-01 Depends completed, steps handed to Planner (Triage)
- 2026-11-01 not planned: DND-020..039 needs 8 steps at one test file per step (5 ECS, 2 UI, 1 acceptance), above the M limit of 6. Must be split: ECS turn loop DND-020..035 (M, 5 steps) and turn UI DND-036..039 (S, 3 steps). Back to Triage (Planner)
- 2026-11-01 Split (Triage), accepting the Planner's draft: this item keeps the ECS turn loop DND-020..035 (M). The turn UI DND-036..039 moves to W-050 (S, depends on this item). Goal and Refs updated.
- 2026-11-01 Ownership (Triage): this item owns the
Intent,Targeting, andCuesworld traits, theEffecttag, theHasEffectrelation, an emptyCombatLogtrait, and the registration of all ninednd/turnsystems. The bodies ofai/moveandai/decide(W-022),combat/act(W-021), andeffects/tick(W-023) stay empty here. - 2026-11-01 Correction (Triage): the earlier "Depends completed" line only meant the Depends list was completed, not that the dependencies are done. Stays in Later until every Depends item is done.
- 2026-11-01 steps S1..S6 written (Planner). Final ordering is ECS only: S1 rng and initiative, S2 pipeline and traits, S3 turn flow, S4 defeat, outcome and Cues, S5 restart, S6 check run. S2 adds
@workspace/ecs-scheduleto demo-dnd if W-019 or W-010 did not (needs user approval) - 2026-11-01 Dependency approval (user):
@workspace/ecs-schedule(workspace:*) may be added toapps/demo-dndfor S2. Approved, not yet added (Triage) - 2026-11-01 Correction (Triage): the 2026-11-01 line on DND-034 observer re-registration says S6. The restart work is S5 (
restartEncounter), so that line should read S5. S6 is only the acceptance run - 2026-11-01 held: waits on the OQ6 decision after the W-039 S1 spike (decision logged in W-039 first) (Triage)