Appearance
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>
| Member | Signature / behavior |
|---|---|
state | T — the reactive state proxy; mutate directly |
store | ReactiveStore<T> — the underlying store |
events | EventBus<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 secondsExtension<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>:
| Method | Behavior |
|---|---|
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.
| Member | Behavior |
|---|---|
state | The proxied state object |
isDirty | Whether 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;
}| Method | Behavior |
|---|---|
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)| Group | Methods |
|---|---|
| Arithmetic | add sub mul div neg abs recip pow(n) ln() log(base) |
| Comparison | cmp eq neq lt lte gt gte max min (+ static Decimal.max/min) |
| Rounding | floor ceil round trunc |
| Predicates | isZero isFinite isNaN isPositive isNegative |
| Conversion | toNumber 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)| Member | Behavior |
|---|---|
state | Serializable 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.