Skip to content

v0.54.0 — 5e action definitions

Picks up v0.51.0's first draft (vtt-modules, unpublished dnd5e 0.4.0). The spec, the closed vocabularies and validateSpec are written and tested; what has never happened is a real run.

  • [x] AA-1 — Run the sample and read the drafts. authoring:spell-actions --slugs=fireball,fire-bolt,cure-wounds,fly. The prompt is a hypothesis until this happens, and the drafts are what answer the two questions the spec left open: whether the caster's slot choice belongs in the spec or in the action's own prompt (3c.4 and 5E-3c both wait on this), and whether confidence: low specs are worth keeping. Needs ANTHROPIC_API_KEY. Done: cantrips and all 48 level-1 spells authored. Both questions answered by the run — the slot stays out of the spec (it is chosen when the button is pressed, and derivation happens when the sheet is saved), and confidence: low is dropped at promotion by default.
  • [x] AA-2 — The spec → action script translator. Deliberately ordinary code, not a second model call: a spec is a closed vocabulary precisely so the translation is total and testable. Done: core/specToActions.ts, plus scripts/promote-spell-actions.mjs for the review gate between the draft and the shipped data.
  • [ ] AA-3 — Tune the prompt against a wider sample — a stratified ~30 spells across attack, save, heal, utility and the awkward middle, scored against hand-written expectations.
  • [ ] AA-4 — The full run over all 317 spells, with the refusal rate and the unmodelled lists reported, not just the successes.
  • [x] AA-5 — Where the output lives and how the module serves it. Then wire it in: a character's spells get their authored actions the way 5E-3b's derived ones arrive today. Done: data/<edition>/spell-actions.json, read by buildDeriveSpellActions, which prefers an authored spec and falls back to the regex parser for a spell nobody has authored. An authored spell with NO actions is an answer, not an absence, and outranks the parser.

Spells that paint the map

The gap the first real run exposed. Roughly a seventh of level 1 — Faerie Fire, Grease, Entangle, Web — is an area spell with no damage in it, and the translator refuses every one, because a save that only imposes a condition is a save the tabletop cannot resolve. That refusal is correct and should stay. But it also throws away the half of those spells the tabletop can do: Faerie Fire's 20-foot cube is a shape on the map, and drawing it is worth having long before anything can apply charmed to what stands inside it.

  • [x] AA-6 — draw() can draw where the user aimed. The drawing half is already built — makeDrawApi (apps/web/src/utils/drawAction.ts) puts a draw in the action sandbox beside roll and target, with circle / square / rectangle / cone / line / wall, a duration in rounds that the initiative tracker cleans up, and the same createDrawing path the toolbar uses. Three things stand between it and this feature: - origin is pixels relative to the executing token, so an action can draw around itself and nowhere else. - useTargetingMode's setOnAreaConfirmed(placedArea, hitTokens) drops placedArea — the one thing that knows where the reticle landed. - _targetingMetadata is written into the execution context and never read by actionExecutor, so even today's area metadata does not reach action code. The fix is a passthrough and a squares→pixels conversion, not a new drawing system. Also expose IDrawing.text through DrawArgs: the drawing model already carries a visible label, and metadata (a hover tooltip) is not one.
  • [x] AA-7 — Teach the authoring pass which area effects PERSIST. A new spec field, and the distinction it has to draw is the hard part: - Paints the world — Wall of Fire, Sleet Storm, Web, Grease, Cloud of Daggers. The area itself endures and keeps mattering to whatever enters it. This wants a drawing. - Catches what is standing there, then stops being a place — an instantaneous burst whose rider lingers on the creatures it caught. Many System 8 powers are this shape: ongoing damage or a debuff carried by the target, with no persistent zone. This wants no drawing; one would leave a permanent scorch mark where a fireball briefly was. The failure mode is asymmetric, which decides the default: a missing drawing is a spell the GM narrates as usual, while a spurious one is map litter every player can see and nobody can explain. So the model refuses when unsure, as it does everywhere else in this pipeline. Note that concentration and a stated duration are hints, not the rule — Faerie Fire persists and is a zone; Hold Person persists and is not.
  • [x] AA-8 — The action shape itself. Targeting mode exactly as a damaging spell uses it, and on confirm the action creates a drawing in the chosen area, labelled with the effect (Faerie Fire — outlined, advantage to attackers). Where it also has a save or damage, one action does both: the drawing is what the area leaves behind, not a second button. specActionRefusal then stops declining a save with no damage when the spec says the area persists — that action now resolves something, which is the only reason it was ever refused.

Map click no-reg

  • [x] When left-clicking the map, and the cursor is not over an interactive element (i.e. a token on the current layer), currently nothing happens. In this change, when this click occurs, but another token or drawing on some other layer is present, the click pops up a context menu showing all possible tokens/drawings on other layers matching the cursor position. Selecting one of these selects the given token/drawing, changing the current layer accordingly.