Devlog #12 — Building the Tool That Builds the Scenes
Date: July 1, 2026
Focus: Turning the choreography engine into a contract, then standing up a
separate app on top of it — the unglamorous plumbing that decides whether
non-programmers ever get to direct this game.
There is a certain kind of feature that produces no screenshot. You work for a
day, everything compiles, the tests are green, and if you show it to someone they
say “so what does it do.” The honest answer is "it does nothing you can see, it
just makes six future things possible." This devlog is about one of those.
The choreography system already worked. It could stage the intro, walk companions
in from off-screen, fire camera nudges, run gestures — all from a TOML file. The
problem is that a TOML file is a programmer’s idea of “accessible.” It is
accessible the way a car engine is accessible when you remove the hood: technically
everything is right there.
The actual goal has always been a Windows app where a writer who has never seen a
curly brace can direct scenes like a film. So this stretch was about the boring,
load-bearing question underneath that goal: what does such an app actually need
from the engine, and can it get it without dragging the whole game along?
1. A Format You Can Only Read Is Half a Format
The first embarrassing discovery: the choreography structs could be read but not
written. They derived Deserialize and not Serialize. The engine could load a
scene; it could not save one back. For a game that is fine — the game never edits
its own scripts. For an editor it is fatal, because “load, change one thing, save”
is the entire job.
*// before*
#[derive(Debug, Clone, Deserialize)]
pub struct SequenceDef { */* ... */* }
*// after*
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SequenceDef { */* ... */* }
That is not a clever change. It is a change I should not have needed to make,
which is exactly the sort of thing you only notice when you stop asking "does this
run" and start asking "could someone else’s program hand this back to me
unchanged." The serde tags were already symmetric, so once the derive was there,
TOML → model → JSON → model → TOML round-tripped losslessly. I wrote a test that
does precisely that loop over the stock scene, because the day it stops being
lossless is the day the editor starts quietly corrupting people’s work.
2. The Editor Speaks JSON, The Writer Speaks Neither
The person using the tool will never see JSON. That was a firm requirement, and a
good one, because the moment a “content tool” leaks its serialization format the
non-technical user is gone. But internally, JSON is the right currency: editors,
schemas, and web UIs all love it, and TOML tooling is thinner.
So the arrangement became: the game keeps reading TOML, the app speaks JSON, and a
small CLI bridges the two. The writer sees a timeline and some actors on a stage.
JSON is the pipe under the sink — necessary, and nobody should have to look at it.
choreo validate <file> # deep checks, human errors
choreo convert <in> <out> # toml <-> json, lossless
choreo schema [--out path] # emit the JSON Schema
choreo preview <file> <seq> # headless scene timeline
The nicer part is that the schema is generated from the Rust types, not written
by hand. If someone adds a new beat to the engine next month, the schema updates
itself and a test fails if the checked-in copy went stale. A hand-written schema
would drift the first afternoon nobody was watching. This one cannot.
3. The Question That Decided Everything
Then came the fork that mattered: what technology is the app built in? Rust with an
in-process UI? A web app in a native shell? Something else entirely?
I nearly answered it the usual way — by preference and vibes. Instead I noticed
that the whole decision hinged on one unknown: live preview. If the only way to
show a writer what their scene looks like is to run the actual game engine, then
the app is chained to the engine’s language and framework forever. If a scene can
be previewed without the game, every option stays open.
So rather than argue about frameworks, I built the thing that answers the question.
4. The Preview That Doesn’t Need The Game
The keystone turned out to be a small pure module that computes, for a given
sequence at a given time, where every actor is, which way it faces, whether it is
visible, and what the camera is doing — with no Macroquad, no game loop, nothing
but arithmetic.
The trick to keeping it honest is that it does not reimplement the engine. It
drives the real ChoreographyEngine::tick, takes the same beat-intents the game
takes, and applies them to an abstract world of points instead of sprites. Same
timing, same triggers, same step-and-beat semantics — just a different thing on the
receiving end.
$ choreo preview Assets/Data/choreography.toml eve_walk_in_demo
...
eve (2798,1080) -> (2070,1080) # walks in from off-screen east, stops at her mark
That one line of output is the whole argument. A scene can be evaluated headlessly,
faithfully, in pure Rust, and emitted as JSON any program can draw. Which means the
editor never has to embed the game. Which means the framework choice is now about
ergonomics, not capability.
It also caught a real bug in itself immediately: the first version ran the preview
in intro mode, so the intro’s own “hide the companions” sequence kept firing and
fighting the walk-in I was trying to watch. The actor kept vanishing. The fix was to
preview each sequence in an isolated neutral mode, which is obvious in hindsight and
was not obvious at 1 a.m.
5. Naming Is Half the Work, Emotionally
With preview proven stack-independent, the app choice fell to whatever gives a
non-technical writer the best node-and-timeline editor — which is the web ecosystem,
wrapped in a small native shell, kept in its own repository so none of that weight
touches the game.
It needed a name. The tool composes recurring, deliberately-arranged themes and
weaves them through the work, which is the literal definition of a leitmotif. So
Leitmotif it is. It lives as a submodule, has its own version, and talks to the
game only through three things it will never be allowed to forget: the choreo
CLI, the generated schema, and the preview JSON. No source coupling. If the game
crate never learns the app exists, that is the correct amount.
The A0 scaffold does exactly one honest thing so far: it opens a window with three
buttons that reach across to the choreo binary and come back with real answers.
That is not a screenshot anyone will frame. But it is the first moment the tool and
the engine spoke to each other through a seam that was designed rather than
improvised, and that seam is the whole point.
6. The Part I Would Pretend Was Planned
I would like to claim I saw this sequence in advance — contract, then schema, then
preview, then app. I did not. What actually happened is that each step refused to
let me skip it: you cannot schema-drive a format that cannot serialize, you cannot
choose a UI stack until you know whether preview needs the engine, and you cannot
name a thing until you know what it is.
If I reduce it to one sentence: the editor became possible not when I built a UI,
but when I made the scene file into something a second program could hold, hand
back, and preview without asking the game for permission.
The scenes have not changed. What changed is who is now allowed to write them.

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