Skip to content

Core API

Everything exported by @idlekitjs/core. The engine-level contracts (Extension, EngineContext, System, SaveAdapter, ...) are re-exported from @idlekitjs/types — import them from either. Types owned by a mechanic, plugin, storage backend or the DOM renderer are exported by their own package, not by core.

createEngine

ts
function createEngine<T extends object>(config: EngineConfig<T>): Engine<T>;

interface EngineConfig<T extends object> {
  initialState: T; // plain, serializable data object
  step?: number; // fixed time step in seconds (default 1/20)
  renderer?: RenderTarget<T>; // e.g. Renderer from @idlekitjs/dom; omit = headless
  scheduler?: FrameScheduler; // e.g. createRafScheduler(); omit = manual driving
}

interface RenderTarget<T extends object> {
  connect(store: ReactiveStore<T>): void;
  render(): void;
}

Engine<T>

MemberSignature / behavior
stateT — the reactive state proxy; mutate directly
storeReactiveStore<T> — the underlying store
eventsEventBus<EngineEvents> — typed engine bus (loaded, resume)
addSystem(system)Register a System<T> run each step. Chainable
use(extension)Register an Extension<T>; runs its setup immediately. Chainable
load(save)Promise<boolean> — restore from a SaveManager, emit loaded(savedAt)
start() / stop()Begin / end frame driving via the scheduler
pause() / resume()Suspend / continue without ending the run (resume delta is 0)
advance(seconds)Run the simulation forward in fixed steps (offline catch-up; uncapped)
dispose()stop() + every extension's teardown()

Per fixed step: systems in registration order, then extension updates. Per frame: state flush → renderer → extension renders.

System<T>

ts
type System<T extends object> = (state: T, dt: number) => void; // dt in seconds

Extension<T> / EngineContext<T>

ts
interface Extension<T extends object> {
  id: string;
  setup?(engine: EngineContext<T>): void; // once, at engine.use
  update?(state: T, dt: number): void; // every fixed step, after systems
  render?(): void; // every frame, after bindings
  teardown?(): void; // at engine.dispose
}

interface EngineContext<T extends object> {
  readonly state: T;
  readonly events: EventEmitter<EngineEvents>;
  advance(seconds: number): void;
  pause(): void;
  resume(): void;
}

See the extensions guide.

Events

ts
interface EngineEvents {
  loaded: number; // savedAt timestamp (ms) — after engine.load
  resume: number; // elapsed background time (ms) — from the page-lifecycle bridge
}

EventBus<Events> implements EventEmitter<Events>:

MethodBehavior
on(type, handler)Subscribe; returns an unsubscribe function
off(type, handler)Unsubscribe
emit(type, payload)Notify all handlers

The event map is extensible per game (declare your own interface extending EngineEvents).

ReactiveStore<T>

Proxy-based reactive state (guide). Only top-level keys are tracked; deep mutations require reassignment.

MemberBehavior
stateThe proxied state object
isDirtyWhether keys changed since the last flush
subscribe(listener)Called on flush with the set of dirty keys; returns unsubscribe
flush()Notify subscribers, reset the dirty set (the game does this per frame)
track(deps, fn)Run fn, recording every key read into deps (renderer plumbing)

Scheduling

ts
interface FrameScheduler {
  start(onFrame: (now: number) => void): void; // now: ms timestamp
  stop(): void;
}

function manualScheduler(): ManualFrameScheduler;

interface ManualFrameScheduler extends FrameScheduler {
  frame(now: number): void; // push one frame by hand
  readonly started: boolean;
}

The browser driver is createRafScheduler from @idlekitjs/browser. manualScheduler is the headless/test driver — see the headless testing recipe.

SimulationLoop

The fixed-step loop behind Engine — public for advanced embedding:

ts
interface SimulationLoopOptions {
  update: (dt: number) => void;
  render?: () => void;
  step?: number; // default 1/20 s
  maxFrameTime?: number; // catch-up cap per frame, default 0.25 s
  scheduler?: FrameScheduler;
}

start(), stop(), pause(), resume(), isRunning — same lifecycle semantics as Engine.

SaveManager<T>

ts
interface SaveManagerOptions {
  key: string;
  version: number;
  adapter: SaveAdapter;
  migrations?: Record<number, Migration>; // migrations[n]: v(n-1) -> v(n)
}

type Migration = (data: unknown) => unknown;
interface LoadResult<T> {
  state: T;
  savedAt: number;
}
MethodBehavior
save(state)Promise<void> — persist { version, savedAt, state } as JSON
load()Promise<LoadResult<T> | null>null if absent/corrupt (never throws); applies migrations
clear()Promise<void> — delete the save

Parsing neutralizes __proto__/constructor injection from edited saves. See the saving guide.

Decimal

Big numbers beyond Number.MAX_VALUE (mantissa/exponent representation).

ts
type DecimalSource = number | string | Decimal;
const value = D("1.5e400").mul(2); // D(x) is shorthand for new Decimal(x)
GroupMethods
Arithmeticadd sub mul div neg abs recip pow(n) ln() log(base)
Comparisoncmp eq neq lt lte gt gte max min (+ static Decimal.max/min)
Roundingfloor ceil round trunc
PredicatesisZero isFinite isNaN isPositive isNegative
ConversiontoNumber toString toExponential(digits) toFixed(digits) toJSON

Operations return new instances (immutable style). toJSON makes it safe in saved state as a string — revive with D(saved).

Random

Seedable, serializable PRNG (mulberry32) — reproducible drops, save-scum proof.

ts
const rng = createRandom(seed); // or new Random(seed)
MemberBehavior
stateSerializable snapshot; new Random(state) resumes the sequence
next()Float in [0, 1)
int(maxExclusive)Integer in [0, max)
range(min, max)Integer in [min, max] (inclusive)
pick(items)Uniform pick (throws on empty)
weighted(items, weights)Weighted pick (throws on mismatch / zero total)

Formatting

ts
formatInteger(value: number): string;              // 1234567 -> "1,234,567"
formatNumber(value: number, decimals = 2): string; // 1500 -> "1.50K", 2.5e6 -> "2.50M"
formatDuration(ms: number): string;                // 90000 -> "1m 30s"

formatNumber uses idle-standard suffixes (K, M, B, T, Qa, Qi, ...) and falls back to scientific notation beyond them.