Schedule
Stages run in a fixed order each tick: startup → pre → update → post → render. startup runs once on the first tick. The other four run every tick in insertion order.
import { schedule } from "@f0rbit/forge";
const sch = schedule();
sch.add("startup", spawn_level, "level.setup");sch.add("update", player_input, "player.input");sch.add("update", movement, "movement");sch.add("post", collisions, "collisions");sch.add("render", draw_overlays, "ui.overlays");
sch.tick(world, ctx); // pumps input + runs every stage in orderadd(stage, sys, opts?)
Section titled “add(stage, sys, opts?)”schedule.add accepts either a positional name string (the common case) or an AddOpts bag for periodic gating:
type AddOpts = { every?: number; // run on every Nth tick (default 1) phase?: number; // tick offset for the gate (default 0) name?: string; // system name (default `__sys_<n>`)};
schedule.add( stage: Stage, system: System, opts?: AddOpts | string,): Schedule;Three forms in practice:
sch.add("update", player_input); // unnamed, every ticksch.add("update", movement, "movement"); // named, every ticksch.add("update", regen, { every: 60, phase: 30, name: "regen" }); // every 60 ticks, offset 30The wrapper fires when ctx.time.tick % every === phase % every. every: 1 is equivalent to no gate. Use phase to interleave two periodic systems with the same period instead of bunching them on tick 0.
every must be a positive integer; phase a non-negative integer. The schedule throws on registration if either is invalid (programmer-error guard, not a recoverable runtime error).
For grid-game tile movement, pair with ticks_per_step(cells_per_second, fixed_dt) so movement speed stays decoupled from grid resolution.
Inside sch.tick
Section titled “Inside sch.tick”startupruns once.ctx.input.advance(world, ctx)— drains the input source, refreshes action state, firespre_advanceandon_advancelisteners (replay hooks live here).pre,update,post,renderrun in that order. After therenderstage,ctx.debug.frame()is automatically drained (so debug commands always render in the same tick they’re issued).
sch.run(stage, world, ctx) runs one stage in isolation — useful inside the harness tick for headless tests that don’t want PIXI render systems to fire.
Pump auto-firing
Section titled “Pump auto-firing”The schedule fires input.advance before stages and debug.frame() after render automatically. You don’t add input or debug systems by hand — the schedule does it.
The startup_done flag is per-schedule() instance; if you clear() the world via snapshotter.restore, startup will not re-fire (this matches “loaded a save → don’t re-spawn the level”). Recreate the schedule if you want a fresh startup pass.
Migration from v0.2.0 add_periodic
Section titled “Migration from v0.2.0 add_periodic”schedule.add_periodic(stage, sys, { every, phase? }, name?) is removed. Pass the same opts to add instead:
// v0.2.0sch.add_periodic("update", movement, { every: 10 }, "movement");
// v0.3.0sch.add("update", movement, { every: 10, name: "movement" });The string-name positional shorthand is preserved for the common non-periodic case.