Devlog #13 — Leitmotif, or the Tool Learns to Direct
Date: July 1, 2026
Focus: Building the whole authoring app on top of the contract — timeline,
inspector, live stage, pickers, export — and the quiet satisfaction of a loop that
finally closes on itself.
Last time I made the scene file into something a second program could hold. This
time I built that second program until it could hold a whole scene — and then hand
it back to the game, unbroken, on purpose. There is a specific kind of joy in
watching a system close a loop it was designed to close. It is not loud. It is the
feeling of a door latching into a frame that was cut to fit it.
The app is called Leitmotif, which is the right word for a tool that arranges
recurring, deliberate themes into a whole. It lives in its own repository, nested
as a submodule, so none of its weight ever leans on the game. It talks to the game
through exactly three things and refuses to learn a fourth.
1. A Timeline You Can Shove Around
The first real milestone that felt like directing was the node-graph timeline.
A sequence becomes a row of steps, left to right, and each step holds its beats as
cards you can pick up and drop somewhere else — into a later step, into a parallel
lane, wherever the moment wants them.
The temptation with drag-and-drop is to make it clever. I made it obvious instead:
drop-zones that light up, a card that dims while it’s in the air, and one function
in the document model that owns every reorder so nothing else is allowed to have an
opinion about where a beat lives. When there is exactly one place that moves a
thing, moving things stops being frightening.
moveBeat(seqId, si, bi, toStep, toIndex) { */* the only path a beat may travel */* }
That is a boring line. Boring lines are how you earn the right to have interesting
features later.
2. Forms That Build Themselves
The inspector was the part I most wanted to cheat on, and most glad I didn’t. A
beat has a verb and a handful of fields, and the fields differ per verb. The lazy
version hand-writes a form for each verb and then rots the moment the engine grows
a new one.
Instead the form reads the generated schema for each field’s type and description,
consults a small map for which fields a verb even uses, and assembles itself.
Add a field to a beat in the engine, regenerate the schema, and it simply appears
in the app with its help text attached. The map that remains — the part the schema
couldn’t supply, because the engine parses those strings at runtime — is small and
honest about being small. I would rather have one visible seam than a hundred
invisible ones.
3. The Stage That Does Not Lie
Then the piece the whole architecture had been quietly building toward: a stage
where the writer scrubs time and watches the actors move.
The important word is truthful. The stage does not approximate what the game
does; it draws frames computed by the game’s own headless evaluator. A companion
enters from off the right edge, walks to her mark, turns, and stops — and it is the
same arithmetic the real game will run, minus the sprites. Because of that, the
preview is allowed to be trusted, which is the only thing that makes a preview
worth having.
$ choreo preview scene.toml eve_walk_in_demo
eve (2810,1080) -> (2070,1080) # off-screen east, then home to her mark
I drew it small — markers, name labels, facing arrows, a dashed camera frame that
pans and zooms. No art, just truth. The art comes later; the truth had to come
first.
4. Never Make Them Type an Id
A tool for non-programmers earns its keep in the moments it refuses to punish them.
So actors, targets, and sounds became pickers, fed by the game’s actual data —
thirteen characters, twelve-hundred-odd sound cues — offered as suggestions the
writer chooses from, while still allowing a typed id for the modder who’s inventing
something new. Forgiving without being rigid. The app should meet the writer where
they are, not where a parser would prefer them to be.
5. The Loop Closes
And finally the gold button: Export to game. It does one careful thing before it
does the obvious one — it validates, and if the scene has a problem it writes
nothing and says why, in a sentence a person can read:
error sequence 'bad' triggers on_sequence_finished of unknown sequence 'does_not_exist'
That refusal is the whole point. The writer can be as bold and wrong as they like
inside the editor, and the game will only ever receive scenes that are whole. The
freedom and the safety live in the same button, which is exactly where they should
live.
Author a scene, press the gold button, and the game plays what you made. The loop
that started as “make the file serializable” ends as "hand the game a finished
scene it can trust." Everything in between was just earning the right to that
sentence.
6. The Part I Am Allowed to Be Proud Of
I held this whole thing to one bar, stated plainly: be so intuitive and so
specialised for directing this game that we out-class the generic tools. Not by
having more buttons — by having fewer, better ones, a preview that doesn’t lie, and
an interface that teaches as it’s used. I don’t think we’re finished against that
bar. I think we’ve built something honest enough that finishing is now a matter of
polish and taste rather than architecture, which is the good kind of unfinished.
If I reduce it to one sentence: the game learned to be directed the day the tool
learned to hand it a scene without breaking it.
The scenes were always possible. Now they’re authorable — by someone who has
never seen a bracket, and never has to.

Reader notes
No notes yet.
Sign in with GitHub to leave a note.
Continue with GitHub