Timelines: things that happen one after another
The October audit's Wave 4 item "a Timeline kind for cutscenes and boss phases" (docs/design/audit-2026-10.md). Before it, a game could react to an event (a RuleSet) or keep a per-entity state (a StateMachine), but nothing could say "do this, wait two seconds, say that, wait until the boss is hurt, then do the next thing". Games faked delays with tick rules and counter variables, and nothing could take the player's control for an opening.
The document
schemas/v1/timeline.schema.json, kind Timeline, ids tl.*, in timelines/<name>.timeline.json.
on,where,once: it starts on an event, read as a rule'sonandwhereare. Or thestart_timelineaction starts it, with the self it names.cutscene: while it runs, kit.core'shas_control()is false, so no play kit applies the player's input.skippable(default true): the skip press ends it.steps, each with anidunique in the Timeline:waitsecs or ticks;untila condition holds (timeoutSecgives up waiting);doactions, at once;saya line on the message line (ui.message), waiting while it is read: 1.5 s plus a second for every 15 characters, unlesssecssays otherwise;movean entity in a straight line to a place, or a metre short of an entity, oversecs, on the world's ground, facing the way it goes;camerapoints the views' camera at an entity or a place and shakes it, forsecsor until its run ends.
How it runs (kit.core's timeline system, GwTimelineRunner)
- One run per Timeline per self. A boss's phases start on
entity.spawnedwhere the arch is the boss's, and two bosses get two runs. A Timeline already running for that self, or aonceone that started, is not started again; that is no error. - Ordered in the step list after the dialogue and before the rules, so rules hear
timeline.startedandtimeline.donein the same tick. Each tick every run does steps until one must wait. A run started before the system steps (bygame.start, or an earlier kit's event) steps in that tick; one started later (a rule, another run'sdo) steps from the next. - Ticks, never the clock (ADR-0004). A wait's end is an absolute tick in the run's state, a
move is a share of the way by ticks gone, and everything a run holds is plain data
(
save_state): a save in the middle of a move loads and goes on to the same state hash, tick for tick (test_a_save_in_the_middle_goes_on_exactly). - Bounded (ADR-0003). At most 32 runs at once; a run never goes back;
dosteps share the tick's action budget. - The skip is the InputMap's
skipaction, orconfirmin a game that has none (most do). Every skippable cutscene running ends at once: thedosteps left run, the moves left end where they were going, and lines, waits and camera steps are passed over, so a skipped opening leaves the game exactly where a watched one would, less the time. The press that leaves the title is not a skip (the system remembers whether the game was in play at the end of the last tick, as the dialogue does):game.startbegins the opening on that very tick. - No row until used. A system whose state is empty and that says
omits_empty_state()leaves no row inGwSim.state(), and a load with no row gives it{}. A game with no Timeline, or none running, saves and hashes exactly as before Timelines existed: no sample's hashes moved.
What views do with it
- The camera:
GwTimelineRunner.look_of(sim)is the point a camera step looks at, or nowhere. kit.actors' rig (3D), kit.platformer's view (2D) and kit.world_tiles' chunk streaming all follow it instead of the player, easing there at their own follow rates. - The shake: kit.vfx's juice holds a shake of
shaketimes a blast (0.04 of the view's height), the strongest shake winning as with a Style's; the person's Screen shake option scales it, and the menu offers that option when any Timeline shakes. - A platformer level holds still while a cutscene runs: kit.platformer steps nothing while the player has no control, so no foe can reach the hero mid-scene.
- The HUD:
cutscene()andtimeline_running(id)are pure functions. Thornwick hides its hintswhen: not cutscene()and shows askiphintwhen: cutscene().
Checks (core/src/game/timelines.ts)
Beyond the schema: step ids unique (error), a wait with neither secs nor ticks (error), a Timeline
nothing starts (warning), an unskippable cutscene that waits on a condition with no timeout
(warning: the player could be held forever), and a move in a game that runs kit.platformer
(warning: a platformer's bodies follow their own physics). Expressions in a Timeline read self,
target, game, var, tune, and event (with that event's fields typed) only when it has on.
Bots and tours
- The seek bot watches a cutscene: it sends nothing while one holds the player, as a person would. The monkey presses at random and so skips them.
- Tours await
timeline.done:<id>(andtimeline.started:<id>,timeline.step:<id>/<step id>).
In a sample
Thornwick opens on tl.intro: the camera goes to the empty shrine in Briar Wood while Maren says
what was lost, then to Maren by the well, and back to the hero (six seconds). Its look tour shoots
the opening, its proof tour skips it, its click and poster tours wait for it.
Not yet
- A
movethat walks a route (kit.move's nav) rather than a straight line. - Platformer bodies moved by a step (they follow their own physics; a brain moves them).
- Letterbox bars and a fade: a Screen can draw them
when: cutscene()today. - Scenes joined by doors (the other half of the audit item) come next, and a Timeline's
dowilltravelthere.