Docs Design

Contents

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's on and where are. Or the start_timeline action starts it, with the self it names.
  • cutscene: while it runs, kit.core's has_control() is false, so no play kit applies the player's input. skippable (default true): the skip press ends it.
  • steps, each with an id unique in the Timeline:
    • wait secs or ticks;
    • until a condition holds (timeoutSec gives up waiting);
    • do actions, at once;
    • say a line on the message line (ui.message), waiting while it is read: 1.5 s plus a second for every 15 characters, unless secs says otherwise;
    • move an entity in a straight line to a place, or a metre short of an entity, over secs, on the world's ground, facing the way it goes;
    • camera points the views' camera at an entity or a place and shakes it, for secs or 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.spawned where the arch is the boss's, and two bosses get two runs. A Timeline already running for that self, or a once one 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.started and timeline.done in the same tick. Each tick every run does steps until one must wait. A run started before the system steps (by game.start, or an earlier kit's event) steps in that tick; one started later (a rule, another run's do) 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; do steps share the tick's action budget.
  • The skip is the InputMap's skip action, or confirm in a game that has none (most do). Every skippable cutscene running ends at once: the do steps 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.start begins 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 in GwSim.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 shake times 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() and timeline_running(id) are pure functions. Thornwick hides its hints when: not cutscene() and shows a skip hint when: 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> (and timeline.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 move that 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 do will travel there.

From docs/design/timelines.md in the Gamewright repository.