The `move` and `animation` steps previously had no dispatch point: the story
always suspended as a host-presented step and the host had to switch on the
step kind in its own loop, or reimplement movement per game mode.
Add two optional ports to `RpgStoryBinding`:
StoryMoveHandler moveActor drives `move`
StoryAnimationHandler playAnimation drives `animation`
Each handler returns a three-state status, so one port serves every shape of
host without the dialect assuming a game mode:
Arrived / Finished step completes inline, the story never suspends
Travelling / Playing the story suspends until the host calls advance()
Unreachable / Unavailable the step fails with a structured diagnostic
A failed `Result` from a handler is reported verbatim, so causes such as "the
navigation service is not loaded" are not swallowed.
Both payloads stay in the host's own space. `move`'s `x`/`y` are whatever the
host's movement model uses (tile indices, board cells, world units, lanes) and
never interpreted or rounded here; `animation`'s `clip` is an opaque locator
and is never resolved, loaded, or assumed to name a rig. The `actor` / `actor`
projection is null when the id belongs to another subject space (a map prop, a
camera, a squad) and the step still runs.
Omitting a handler leaves behaviour byte-for-byte unchanged: the step stays
host-presented and the host acknowledges it with advance(). That is the escape
hatch for hosts whose movement or playback must go through their own queue or
state machine, and the compatibility path for existing content.
Also let `RPGStorySession.begin` take `null` for `party` / `bag` / `equipment`
in scripts. They are optional collaborators -- a purely presentational story
needs none of them -- and the C++ entry point already documents them as
nullable, but ssq's default pointer binding rejects Squirrel `null`. Translate
through ssq::Object, matching the idiom in avatar/Avatar.cpp's addLayer.
`gameState` stays typed and required because it owns the completion fact.
Tests: 19 new zeroerr cases. 14 in test/rpg_dialect.cpp cover the semantics of
both ports -- dispatch, inline completion, unreachable/unavailable failure,
verbatim controller failure, the no-controller host-presented path, subjects
outside the party roster, and travelling/playing state across capture and
restore. 5 in test/rpg_dnut_script.cpp drive the steps end to end from
Squirrel and verify the effect, including movement the script controls itself.
Docs: docs/usr/modules/rpg.md documents both ports and the recommended
script-owned movement path; the L1 plan records the deliberate deviation from
its own "grid movement controller" design and why a map::Pathfinder binding
would weld the dialect to one game mode (and add an upward L1->map dependency).
New L1 module `dnut_interpreter` owns the shared lexer, the registry-driven
compiler, the compiled sequence model and the cross-frame interpreter. Domain
vocabularies are registered by the modules that own them; the core never
interprets a domain payload.
- `DnutLexer` / `DnutCompiler`: shared `.dnut` front end with statement-level
error recovery, skipping of non-`story` top-level blocks, and step legality
deferred entirely to the registry.
- `SequenceAsset` / `SequenceRuntime`: compiled sequence model plus a
cross-frame interpreter with call frames, a bounded execution budget, and
version-checked capture/restore of the cursor.
- `StepKindRegistry`: strict step validation that rejects undeclared fields and
runs the descriptor's own domain invariants.
- `rpg`: `RpgDialect` registers the RPG vocabulary (movement, animation, object
selection, skill/attribute/inventory/equipment changes) behind a Pimpl
boundary, so public headers stay free of cross-module includes.
- 17 unit tests across `test/dnut_interpreter.cpp` and `test/rpg_dialect.cpp`.