Fix two bugs in the Custom Modifier Editor's live preview pane that
surfaced when loading thressgame-100 epic recipes:
BUG 1 \u2014 'unknown primitive: <kind>'
The narrator dispatcher (KIND_NARRATORS) was missing entries for the 9
new primitives shipped across the thressgame-100 epic. Loading any recipe
using them showed 'unknown primitive: add-resource' (etc.) in the preview.
Added 9 narrators (alphabetically inserted into KIND_NARRATORS):
- on-attr-expire (W2) \u2014 'When the <attr> attr on <target> expires: <children>'
- decrement-attr-each-turn (W2) \u2014 'each turn-end, subtract 1 from <attr> on <target>'
- set-board-topology (W4) \u2014 'set board topology to <value>'
- link-pieces (W4) \u2014 'link the pieces at <a> and <b>'
- unlink-pieces (W4) \u2014 'unlink the pieces at <a> and <b>'
- on-piece-pair-link-broken (W4) \u2014 'When a linked partner is destroyed: <children>'
- add-resource (W5) \u2014 'give <player> +<amount> score' (or 'subtract from')
- spend-resource (W5) \u2014 'If <player> has <amount> score, spend it, then: <then>; otherwise: <else>'
- on-resource-changed (W5) \u2014 'When <player>'s score crosses <threshold> <direction>: <children>'
BUG 2 \u2014 '[object Object]' for V3 resolver shapes
fmtPieceTarget (line 125) and inline square handling fell through to
String(t) when the field carried a V3 resolver shape ({$var}, {ctx-self-id},
{ctx-self-marker-id}, {ctx-attr}, {ctx-build}, {add/sub/mul/mod}).
User-reported case: tpl-treasure-chest's destroy-marker arm rendered as
'destroy [object Object]'.
Added fmtResolverShape(v) helper rendering the 9 V3 shapes as plain English:
- {$var: 'p'} \u2192 'p'
- {ctx-self-id: null} \u2192 'this piece'
- {ctx-self-marker-id: null} \u2192 'this marker'
- {ctx-attr: {entity, attr}} \u2192 "<entity>'s <attr>" (recursive)
- {ctx-build: {col, row}} \u2192 'e4' (algebraic if both literal) or
'the square at column <c>, row <r>'
- {add: [a, b]} \u2192 '<a> + <b>' (recursive)
- {sub/mul/mod: [a, b]} \u2192 same pattern, '-' / '\u00d7' / 'mod'
Added fmtSquareValue(v) helper for square-typed fields. fmtPieceTarget
extended to call fmtResolverShape before falling back to String.
Updated existing narrators to use fmtSquareValue for square fields:
- spawn-marker.square, spawn-marker-pair.{squareA,squareB}
- move-piece.to, place-piece.square, must-class.square
- destroy-marker.target now routes through fmtResolverShape
Test surface:
- narrate.test.ts: 69 \u2192 90 tests (+21)
- V3 resolver shapes describe block: 8 tests
- thressgame-100 primitives describe block: 12 tests
- tpl-treasure-chest end-to-end regression test: 1 test
asserts the user-reported failure is fixed (no '[object Object]',
no 'unknown primitive', expected English phrases present)
bun run check: 3291 tests pass (was 3270, +21). 0 regressions.
USER-REPORTED CASE NOW RENDERS:
Before: 'When this piece captures: spawn a treasure marker on <square>
(permanent). When a piece enters a treasure marker: unknown
primitive: add-resource; destroy [object Object].'
After: 'When this piece captures: spawn a treasure marker on this
piece's Position (permanent). When a piece enters a treasure
marker: give white +5 score; destroy this marker.'
Spot-check on 5 recipes (all render sensible English):
- tpl-treasure-chest \u2713 (see above)
- tpl-time-bomb \u2713
- tpl-down-with-the-ship \u2713
- tpl-summoning-ritual \u2713
- tpl-bouncing-ricochet \u2713
CALLOUT FOR FUTURE EPICS (recorded in learnings.md):
narrate.ts has zero engine/registry imports by design. When a new primitive
or resolver shape lands, the narrator must be updated MANUALLY \u2014 there
is no static link to detect drift. Add a registry-count-style sentinel
test in a follow-up to catch this earlier (e.g. compare KIND_NARRATORS
keys against PRIMITIVE_REGISTRY.list()).