W-019: DND-F01 grid, party, monsters, movement¶
Type: feature · Size: M · Epic: E-dnd-m1 · Target: apps/demo-dnd · Breaking: no Depends: W-010, W-012, W-013, W-038, W-039, W-040 Refs: brief DND-F01; demo-dnd spec DND-001..006, DND-010..013, DND-015..019 (no 007..009, 014); ecs-prefab spec PREFAB-001, PREFAB-003, PREFAB-005, PREFAB-010..012, PREFAB-014, PREFAB-030, PREFAB-032, PREFAB-070, PREFAB-071, PREFAB-073
Goal¶
Build the first playable slice of apps/demo-dnd: the Encounter world trait and Active tag, setup of the party, the monster templates and instances from content, the labelled grid, the focusable game area with arrow-key and click movement, and the "Moves left" display. Movement is orthogonal, one cell per arrow key. Initiative and turns are W-020, monster AI is W-022.
Steps¶
- S1 (DND-001, DND-005, DND-006): content
party.ts(Fighter, Rogue, Wizard with stats and start cells),monsters.ts(Goblin, Orc),encounter.ts(grid size, monster list: 3 Goblins, 1 Orc with start cells); traitsName,Position,Health,ArmorClass,Speed,Attack,Hero,Monster,Defeated,Activeand world traitEncounter { round, status, movesLeft, acted };setupEncounter(world, content?)addsEncounter(round 1, status'ongoing', movesLeft 0, acted false), validates all content positions (in grid, no shared cell, grid ≤ 12×12, throws namingsetupEncounterand the entry) and spawns the heroes from content. Test with default and custom content, no stat literals insrc/ecs/; files:apps/demo-dnd/src/content/{party,monsters,encounter,index}.ts,apps/demo-dnd/src/ecs/traits.ts,apps/demo-dnd/src/ecs/actions.ts,apps/demo-dnd/src/ecs/setup.test.ts - S2 (DND-002, DND-003, DND-004, DND-006; PREFAB-001, PREFAB-003, PREFAB-005, PREFAB-010, PREFAB-011, PREFAB-012, PREFAB-014, PREFAB-030, PREFAB-032, PREFAB-070, PREFAB-071, PREFAB-073): add
@workspace/ecs-prefab(workspace:*) toapps/demo-dnddependencies (needs user approval); setup creates one template per distinct monster kind withcreatePrefaband one instance per encounter entry withinstantiate, overridingName(unique, "Goblin 2") andPosition. Tests:world.query(Health)is exactly heroes + instances; aworld.onQueryAdd([Health], …)registered before setup never sees a template; damaging one Goblin leaves siblings and the template unchanged; each instance hasIsA(template); custom monster content yields the content values; files:apps/demo-dnd/package.json,apps/demo-dnd/src/ecs/actions.ts,apps/demo-dnd/src/ecs/monsters.test.ts - S3 (DND-010, DND-011, DND-012, DND-013):
moveActive(world, dir)inactions.ts(plain function, also exposed throughcreateActions): one orthogonal cell,Positionwritten withset,movesLeft− 1, returnstrue/falseper DND-010. Tests setActiveandEncounterdirectly: success, each of the eight rejection cases of DND-011 (nothing changes), exactlySpeedmoves incl. moving back then the (s+1)th fails, moving onto aDefeatedcombatant's cell; files:apps/demo-dnd/src/ecs/actions.ts,apps/demo-dnd/src/ecs/move.test.ts - S4 (DND-015, DND-018):
Gridview: region "Grid" withwidth × heightcells, each with the text label"x,y Name (hero|monster)"or"x,y empty", defeated combatants hidden, side as text (not colour only), cells not tab stops; clicking a cell orthogonally adjacent to the active hero callsmoveActivewith the matching direction, any other cell does nothing. Component test with a real world inWorldProvider; files:apps/demo-dnd/src/ui/grid/Grid.tsx,apps/demo-dnd/src/ui/grid/Grid.test.tsx - S5 (DND-016, DND-017):
GameArea(focusabletabIndex=0, label "Game", focused after mount) holding the grid and the actions area;keydownon the game area only, acting only whenevent.targetis the game area itself, ignoringevent.repeatbut stillpreventDefaulting repeated arrows; arrows callmoveActiveandpreventDefault.MovesLeftshows the active hero's name and "Moves left: N" during a hero turn.App.tsxrunssetupEncounteron the app world and rendersGameArea; theW-010heading test stays green. Tests: arrow moves and updates "Moves left", nopreventDefault/move for keys from a button inside the game area, held arrow (repeat) moves nothing but ispreventDefaulted, focus after mount; files:apps/demo-dnd/src/ui/input/GameArea.tsx,apps/demo-dnd/src/ui/actions/MovesLeft.tsx,apps/demo-dnd/src/App.tsx,apps/demo-dnd/src/ecs/world.ts,apps/demo-dnd/src/ui/input/GameArea.test.tsx - S6 (acceptance): run
pnpm turbo run typecheck lint test --filter=demo-dnd, then the spec's acceptance checklist item A1 (DND-019) for the grid and action controls (the initiative panel part of A1 step 1 is checked again in W-020)
Log¶
- 2026-11-01 created from the approved spec (Triage). Meets ready criteria (size M, 5 steps); held in Later until Depends are done
- 2026-11-01 open deps, not done: W-038 (prefab entities), W-039 (instantiate, PREFAB-010..012), W-040 (overrides, PREFAB-030/032) for S2. The board Depends lists only W-010, W-012, W-013 (all done), so the item cannot start yet. Triage question: add W-038..W-040 to Depends
- 2026-11-01 spec defaults applied: OQ3 orthogonal steps, distance |dx| + |dy|, adjacent is 1 (DND-010, DND-018); OQ11 content values only in content (DND-006); OQ12 key map, arrows only in this item (DND-016)
- 2026-11-01 OQ8 (ecs-prefab OQ6, relation-backed traits) is not blocking M1 because templates carry no pairs. Settle it before W-038..W-042
- 2026-11-01 Depends completed, steps handed to Planner (Triage)
- 2026-11-01 steps S1..S6 written (Planner). Found W-038..W-040 still in Next and
packages/ecs-prefab/src/index.tsexporting nothing, so S2 cannot run yet. S2 adds@workspace/ecs-prefabto demo-dnd (needs user approval) - 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 W-038, W-039 and W-040 are done. The split of W-020 and W-021 does not change this item's Depends.
- 2026-11-01 Dependency approval (user):
@workspace/ecs-prefab(workspace:*) may be added toapps/demo-dndfor S2. Approved, not yet added. Stays in Later until W-038, W-039 and W-040 are done (Triage) - 2026-11-01 held: waits on the OQ6 decision after the W-039 S1 spike (decision logged in W-039 first) (Triage)