W-051: DND-F03 combat UI¶
Type: feature · Size: M · Epic: E-dnd-m1 · Target: apps/demo-dnd · Breaking: no Depends: W-019, W-021, W-050 Refs: brief DND-F03; demo-dnd spec DND-055..059; W-019 (game area and key rule); W-021 (targeting actions and attack); W-050 (Actions group and banner)
Goal¶
Add the combat controls of apps/demo-dnd on the W-021 targeting actions: the game-area keys of DND-055 (A begins attack targeting, Escape cancels, Tab, Shift+Tab, and Enter act only while targeting is active), the "target" and "selected target" cell labels, click on a valid target's cell to select and confirm (DND-058), the "Attack" button in the "Actions" group with its "No target in reach" state (DND-056), and the combat log as a role="log" region with live updates and the newest entry in view (DND-057, DND-059). Keys follow the W-019 key rule and never run from a button or the devtools panel. The Poison Strike button and key P are W-023.
Steps¶
- S1 (DND-055): targeting keys in the game-area
keydownhandler of W-019 (same key rule,event.targetis the game area,event.repeatignored):AcallsbeginTargeting('attack'),EscapecallscancelTargeting;Tab/Shift+TabcallcycleTarget(+1 / −1)withpreventDefaultandEntercallsconfirmTargetonly whileTargeting.modeis not'none'. While not targeting,TabandShift+Tabare notpreventDefaulted andEnterdoes nothing. Component tests with a real world and real key events:AsetsTargetingto the first valid target;Tab/Shift+Tabcycle with wrap-around and arepreventDefaulted;Enterattacks the selected target (hero turn ends, log gains an'attack'entry);EscaperesetsTargetingand releasesTab(notpreventDefaulted);TabandEnterwith no targeting change nothing; keys from a button inside the game area call no action; a repeatedTabwhile targeting is stillpreventDefaulted; files:apps/demo-dnd/src/ui/input/GameArea.tsx,apps/demo-dnd/src/ui/input/targeting-keys.test.tsx - S2 (DND-055 cell labels):
Gridcell labels in targeting mode (readTargetingandvalidTargetsfor the mode's range throughuseTrait/useQuery): a valid target's label gets the text "target" ("3,2 Goblin 2 (monster) target"), the selected one "selected target"; outside targeting the DND-015 labels are unchanged. Component tests: no "target" text whenTargeting.modeis'none'; with targeting active exactly the valid targets carry "target" and exactly the selected one "selected target"; the label updates aftercycleTarget; an out-of-reach monster and heroes carry neither; files:apps/demo-dnd/src/ui/grid/Grid.tsx,apps/demo-dnd/src/ui/grid/Grid.targeting.test.tsx - S3 (DND-058): click on a cell in
Gridwhile targeting is active: the cell of a valid target selects it and confirms (attack, viaconfirmTarget, so the attack resolves and the turn ends); any other cell (empty, hero, out-of-reach monster, adjacent empty cell) does not confirm and does not move the hero; with targeting inactive the DND-018 move click is unchanged. The click selects the clicked target before confirming, so a non-selected valid target is the one attacked. Component tests with a scriptedRng: click on the non-selected valid target attacks that one; click elsewhere changes noHealth,Position, or log and leaves targeting as it was; adjacent empty cell while targeting does not callmoveActive; adjacent click without targeting still moves; files:apps/demo-dnd/src/ui/grid/Grid.tsx,apps/demo-dnd/src/ui/grid/Grid.click-target.test.tsx - S4 (DND-056): "Attack" button in the "Actions" group of W-050 (next to "End turn"): it is
disabledwith the visible text "No target in reach" whenvalidTargetsforAttack.rangeis empty, and disabled outside a hero turn or after the hero acted; activating it callsbeginTargeting('attack')and moves focus to the game area, so the S1 keys then apply. Focus also moves to the game area when the focused "Attack" button becomes disabled (same mechanism as W-050, DND-037). Component tests: enabled with a target in reach and visibly "No target in reach" when none (Wizard range vs. Fighter range 1); disabled during a monster turn and whenstatusis not'ongoing'; click setsTargetingand focuses the game area;EnterandSpaceon the focused button only activate that button (noconfirmTarget, no attack, no log entry);A,Tab,Escapepressed on the button run no game action; focus is on the game area after the button becomes disabled; the button sits in the "Actions" group beside "End turn" and is reached byTabfrom the game area when not targeting; files:apps/demo-dnd/src/ui/actions/AttackButton.tsx,apps/demo-dnd/src/ui/actions/Actions.tsx(from W-050),apps/demo-dnd/src/ui/actions/AttackButton.test.tsx - S5 (DND-057, DND-059):
CombatLogview: arole="log"region labelled "Combat log" witharia-live="polite"listing thetextof everyCombatLog.entriesentry oldest to newest (useTrait(world, CombatLog)), setsscrollTop = scrollHeightafter each new entry so the newest is in view, and uses@workspace/uitheme colours (no colour-only meaning, contrast checked in A3);App.tsxrenders it next to the game area. Component tests: region found by role and name witharia-live="polite"; empty log renders no items; entries appear in order after a real attack (text holds both names, every d20, bonus, total, AC, outcome word, "advantage" for a Poisoned target set up in the test, DND-051); the list updates live afterattackand after a restart; files:apps/demo-dnd/src/ui/log/CombatLog.tsx,apps/demo-dnd/src/App.tsx,apps/demo-dnd/src/ui/log/CombatLog.test.tsx - S6 (acceptance): run
pnpm turbo run typecheck lint test --filter=demo-dnd, then the spec's acceptance checklist item A3 (DND-059) steps 1, 3, and 4 for the dice in the log (hit, miss, critical hit, newest entry in view, contrast in both themes). A3 step 2 (attack a Poisoned monster) is not run here: it is accepted in W-023's acceptance, which depends on W-051
Log¶
- 2026-11-01 created from the split of W-021 (Triage). Planner's draft: DND-055..059, M, 5 steps incl. A3 acceptance for DND-059. Held in Later until Depends are done
- 2026-11-01 Depends W-019 (key rule and game area), W-021 (targeting actions), W-050 (Actions group and banner) (Triage)
- 2026-11-01 steps S1..S6 written (Planner): keys, cell labels, cell click, Attack button, combat log, acceptance A3. Labels and click are separate steps because each needs its own
Gridtest file. File names of W-019 (GameArea.tsx,Grid.tsx) and W-050 (Actions.tsx) are assumed from those items; S4 needs the real names. A3 step 2 (Poisoned target) needs the W-023 Poisoned entity or a seeded setup - 2026-11-01 A3 split (user decision, Triage): the S6 acceptance covers A3 without step 2 (attack on a Poisoned monster). Step 2 runs in W-023 S6, which has a Poisoned monster
- 2026-11-01 S6 runs A3 steps 1, 3, 4 only; A3 step 2 (attack a Poisoned monster) is accepted in W-023's acceptance, because W-023 depends on W-051 and W-051 cannot wait for it (Planner)
- 2026-11-01 held: waits on the OQ6 decision after the W-039 S1 spike (decision logged in W-039 first) (Triage)