Skip to content

W-023: DND-F05 Rogue poison, ticking Poisoned effect

Type: feature · Size: M · Epic: E-dnd-m1 · Target: apps/demo-dnd · Breaking: no Depends: W-020, W-021, W-022, W-050, W-051 Refs: brief DND-F05; demo-dnd spec DND-080..092, DND-094; ecs-schedule spec SCH-013

Goal

Add the Rogue's poison to apps/demo-dnd: PoisonStrike from content, the Poisoned effect entity and HasEffect pairs with { turns, damage }, usePoison with a poison intent that applies the effect without a roll, refresh without stacking, the tick at the end of the poisoned combatant's own turn before the defeat check, effect observers and Cues, the Poison Strike button and key P, and the countdown labels. The advantage rule itself is DND-045 in W-021; this item only tests it (DND-088).

Steps

  • S1 (DND-080, DND-081): add PoisonStrike { range: 1, turns: 3, damage: 2 } to the Rogue entry in content only, so that setupEncounter grants the trait to the Rogue only (DND-001). Do not define the PoisonStrike trait: it comes from W-021 S5 with the fields range, turns, damage; if it is missing or has other fields, block this item and report. Use the Effect tag and the HasEffect relation from W-020 as they are; do not add or change them. Make setupEncounter spawn exactly one Poisoned entity (Name "Poisoned", Effect) and create it again after restartEncounter (DND-034). Nothing else spawns it (W-021 does not). Tests: only the Rogue has PoisonStrike (the Fighter and the Wizard do not); exactly one entity with Effect and Name "Poisoned" per world; a new handle after restart; it is not a combatant. Files: src/content/party.ts, src/ecs/actions.ts, src/ecs/poison-setup.test.ts
  • S2 (DND-082, DND-083, DND-084): replace the placeholder usePoison (returns false, from W-021 S5) in actions.ts and createActions with the real implementation; keep its signature usePoison(world, target), so confirmTarget in 'poison' mode needs no change. Check every guard (status, an active hero with PoisonStrike that has not acted, a living opposing target with 1 ≤ distance ≤ range). Set Intent { kind: 'poison' } and acted, then end the turn as in DND-029. Extend combat/act for 'poison': add HasEffect(Poisoned) with the content { turns, damage }, append an 'ability' entry, roll no dice (test with an injected Rng that counts calls), and deal no damage. The pair targets the Poisoned entity of S1 and has the data { turns, damage } (DND-081; the relation is W-020's). Poisoning an already poisoned target refreshes the pair data and adds no second pair. This relies on validTargets and confirmTarget supporting the 'poison' range from W-021; if they do not, block this item and report it. Test every rejection path: it returns false, consumes no dice, and changes nothing (friendly, defeated, out of range, distance 0, already acted, not ongoing, a hero without PoisonStrike). Files: src/ecs/actions.ts, src/ecs/systems.ts, src/ecs/poison-action.test.ts
  • S3 (DND-085, DND-086, DND-087, DND-088, SCH-013): add effects/tick as the first system in Resolve, before combat/defeat. It ticks only the active combatant: hp = max(0, hp − damage), turns − 1, removes the pair at 0, and appends one 'poison' entry per tick. Tests: N ticks over N own turns for N × D damage, also on turns in which the combatant did nothing. No tick on other combatants' turns. The pair is gone in the same Resolve as the Nth tick. A lethal tick gives Defeated before turn/end, with the poison entry logged before the 'defeat' entry. DND-088 scenario (rule DND-045 is from W-021, not implemented again here): one d20 before poisoning, two d20 and "advantage" after usePoison, one d20 after the pair is removed, and the same for a monster attacking a hero that carries the pair (pair added directly in the test). Files: src/ecs/systems.ts, src/ecs/poison-tick.test.ts
  • S4 (DND-089, DND-090): register the effect observers in setup. On add they log 'effect' "Goblin 2 is poisoned (3 turns, 2 damage per turn)". On remove they log "Poison wears off Goblin 2". Each event logs exactly once, also after a restart (DND-053). Extend turn/cues so that pairs added or removed during one action call (including the monster turns that follow) land in Cues.effectsAdded / Cues.effectsRemoved. They stay there until the next accepted hero action (DND-035). Files: src/ecs/actions.ts, src/ecs/systems.ts, src/ecs/poison-cues.test.ts
  • S5 (DND-091, DND-092): build on the UI of W-050 and W-051 and add only the poison parts. Add a "Poison Strike" button to the "Actions" group of W-050, next to "Attack" from W-051, and reuse its disabled pattern. It is visible only for an active hero with PoisonStrike and disabled with "No target in reach" when validTargets for its range is empty. Activating it calls beginTargeting('poison') and moves focus to the game area. Add key P to the game-area key handler of W-051 (key rule DND-016), calling beginTargeting('poison'). Do not reimplement Tab, Shift+Tab, Escape, Enter, or cell click: only test that these W-051 controls work in 'poison' mode (Enter or a click on a valid cell reaches confirmTarget → usePoison), and that P pressed on a focused button does nothing. Extend the initiative item (W-050) and the grid cell label (W-051) to show "Poisoned, N turns left", with "(new)" while the pair is in Cues.effectsAdded. The text is gone after removal or defeat. Component test with a real world in WorldProvider and real key events. Files: src/ui/actions/, src/ui/input/, src/ui/initiative/, src/ui/grid/, src/ui/poison.test.tsx
  • S6 (acceptance, DND-094): run Acceptance checklist A4 and A3 step 2 (attack on a Poisoned monster) in pnpm --filter demo-dnd dev, then record the result in the Log

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: W-020 and W-021 (and through them the ecs-schedule items listed in the W-020 Log). W-021 S2 needs the Poisoned entity from S1 here, see the W-021 Log
  • 2026-11-01 OQ1 default applied (DND-082, DND-083): Poison Strike rolls no dice and deals no damage; it only applies the effect to a valid target
  • 2026-11-01 OQ2 default applied (DND-085 to DND-087): the poisoned combatant ticks at the end of its own turn in Resolve, before combat/defeat
  • 2026-11-01 OQ9 default applied (DND-103): the Poisoned entity is an ordinary entity, so it appears in the devtools list (see W-024)
  • 2026-11-01 spec ID DND-093 does not exist in the approved spec, so the IDs skip from DND-092 to DND-094. Not a gap in this item
  • 2026-11-01 Depends completed, steps handed to Planner (Triage)
  • 2026-11-01 6 steps planned (4 ECS, 1 UI, 1 acceptance). DND-045 is only tested here (S3, DND-088). Open: the code for Depends is missing, W-020 needs a split, the Poisoned entity is owned by S1 or W-021 S2, and confirmTarget may not handle 'poison' mode yet (Planner)
  • 2026-11-01 Depends extended with W-050 and W-051 (Triage): the S5 UI part (Poison Strike button, key P, countdown labels) uses the turn UI and the targeting controls.
  • 2026-11-01 Ownership (Triage): this item spawns the Poisoned entity (DND-081) and owns usePoison, the effects/tick body (S3), the DND-089 observers (S4), and the S5 UI part. The Effect tag and the HasEffect relation are now in W-020, so S1 must not add them when re-planned.
  • 2026-11-01 W-022 is not in Depends (Triage): the monster half of DND-088 sets the attack intent in the test, so ai/decide is not needed.
  • 2026-11-01 Assumption for W-021 (Triage): validTargets and confirmTarget support the 'poison' mode range from PoisonStrike. W-021 covers this (see its Goal).
  • 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-020, W-021, W-050 and W-051 are done.
  • 2026-11-01 Steps re-planned (Planner): S1 no longer adds Effect or HasEffect (W-020) and only adds PoisonStrike and the Poisoned entity; S2 checks the pair target and data; S5 builds on W-050/W-051 and adds only the button, key P, and countdown labels. Still 6 steps, size M. Open: W-051 may not expose a reusable key handler or disabled pattern for a second mode
  • 2026-11-01 Poison seam decided, option A (user, Triage): the PoisonStrike trait and the placeholder usePoison (returns false) come from W-021 S5. W-023 S1 uses that trait and does not add it again. W-023 S2 replaces the placeholder with the real usePoison. Steps not edited; the Planner adapts them when re-planning
  • 2026-11-01 Depends: W-022 added (user decision, Triage). Cycle check: W-022 depends only on W-020 and W-021, and nothing in that chain depends on W-023, so no cycle. The earlier line saying W-022 is not in Depends is superseded
  • 2026-11-01 Steps S1 and S2 adapted to poison seam option A (Planner): S1 no longer adds the PoisonStrike trait (W-021 S5) and only adds the content entry and the Poisoned entity; S2 replaces the W-021 S5 usePoison placeholder. Still 6 steps, size M
  • 2026-11-01 A3 step 2 (attack on a Poisoned monster) runs here in S6, not in W-051 (Triage)
  • 2026-11-01 held: waits on the OQ6 decision after the W-039 S1 spike (decision logged in W-039 first) (Triage)