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 thatsetupEncountergrants the trait to the Rogue only (DND-001). Do not define thePoisonStriketrait: it comes from W-021 S5 with the fieldsrange,turns,damage; if it is missing or has other fields, block this item and report. Use theEffecttag and theHasEffectrelation from W-020 as they are; do not add or change them. MakesetupEncounterspawn exactly onePoisonedentity (Name"Poisoned",Effect) and create it again afterrestartEncounter(DND-034). Nothing else spawns it (W-021 does not). Tests: only the Rogue hasPoisonStrike(the Fighter and the Wizard do not); exactly one entity withEffectandName"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(returnsfalse, from W-021 S5) inactions.tsandcreateActionswith the real implementation; keep its signatureusePoison(world, target), soconfirmTargetin'poison'mode needs no change. Check every guard (status, an active hero withPoisonStrikethat has not acted, a living opposing target with1 ≤ distance ≤ range). SetIntent { kind: 'poison' }andacted, then end the turn as in DND-029. Extendcombat/actfor'poison': addHasEffect(Poisoned)with the content{ turns, damage }, append an'ability'entry, roll no dice (test with an injectedRngthat counts calls), and deal no damage. The pair targets thePoisonedentity 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 onvalidTargetsandconfirmTargetsupporting the'poison'range from W-021; if they do not, block this item and report it. Test every rejection path: it returnsfalse, consumes no dice, and changes nothing (friendly, defeated, out of range, distance 0, already acted, not ongoing, a hero withoutPoisonStrike). 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/tickas the first system inResolve, beforecombat/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 sameResolveas the Nth tick. A lethal tick givesDefeatedbeforeturn/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" afterusePoison, 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). Extendturn/cuesso that pairs added or removed during one action call (including the monster turns that follow) land inCues.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
PoisonStrikeand disabled with "No target in reach" whenvalidTargetsfor its range is empty. Activating it callsbeginTargeting('poison')and moves focus to the game area. Add keyPto the game-area key handler of W-051 (key rule DND-016), callingbeginTargeting('poison'). Do not reimplementTab,Shift+Tab,Escape,Enter, or cell click: only test that these W-051 controls work in'poison'mode (Enteror a click on a valid cell reachesconfirmTarget→usePoison), and thatPpressed 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 inCues.effectsAdded. The text is gone after removal or defeat. Component test with a real world inWorldProviderand 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, beforecombat/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
confirmTargetmay 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
Poisonedentity (DND-081) and ownsusePoison, theeffects/tickbody (S3), the DND-089 observers (S4), and the S5 UI part. TheEffecttag and theHasEffectrelation 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/decideis not needed. - 2026-11-01 Assumption for W-021 (Triage):
validTargetsandconfirmTargetsupport the'poison'mode range fromPoisonStrike. 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
EffectorHasEffect(W-020) and only addsPoisonStrikeand thePoisonedentity; S2 checks the pair target and data; S5 builds on W-050/W-051 and adds only the button, keyP, 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
PoisonStriketrait and the placeholderusePoison(returnsfalse) 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 realusePoison. 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
PoisonStriketrait (W-021 S5) and only adds the content entry and thePoisonedentity; S2 replaces the W-021 S5usePoisonplaceholder. 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)