Appearance
Core concepts
Five ideas explain the entire toolkit. Everything else is detail.
1. The state is a plain object — and it is reactive
Your game state is a plain, serializable data object that you define:
ts
interface State {
coins: number;
clickPower: number;
}
const engine = createEngine<State>({ initialState: { coins: 0, clickPower: 1 } });The engine wraps it in a proxy (ReactiveStore): mutate it directly (engine.state.coins += 1) and the engine records which top-level keys changed. The DOM renderer uses that record to re-run only the bindings that read a changed key.
One rule to remember: only top-level keys are tracked. Deep mutations (state.list.push(x)) are invisible — reassign instead (state.list = [...state.list, x]). Details in State.
2. Systems advance the simulation
A System<T> is a function (state, dt) => void called on every fixed time step:
ts
engine.addSystem((state, dt) => {
state.coins += state.clickPower * dt;
});The loop runs at a fixed step (default 1/20 s) regardless of the frame rate, so the simulation is deterministic: 60 fps, 30 fps and a headless test all produce the same result. Details in Simulation loop.
3. Extensions are the one plug-in contract
Anything installable — a gameplay mechanic, an autosave plugin, your own add-on — implements Extension<T> and is registered with engine.use(...):
ts
engine.use(pageLifecycle());
engine.use(offlineProgress({ maxMs: 8 * 3600_000 }));
engine.use(producersExt);An extension can hook setup (once, on registration), update (every step), render (every frame) and teardown (on dispose). Every hook is optional.
4. Mechanics are generic; your game supplies the data
A mechanic like producers or crafting owns the mechanism — cycle progress, job lifecycles, drop math — and reads/writes its runtime data in your state through accessors you provide:
ts
const kitchen = crafting({
recipes,
machines,
getResources: (state) => state.resources,
setResources: (state, resources) => {
state.resources = resources;
},
getJobs: (state) => state.crafting.jobs,
setJobs: (state, jobs) => {
state.crafting.jobs = jobs;
},
});Because the mechanic's data lives in your state as plain JSON, saving, loading and offline catch-up work with zero extra effort. The mechanics overview explains the pattern in depth.
Atomic economic actions are separate: @idlekitjs/economy declares resources, costs, rewards, requirements and transactions. Use it when a click should be previewed, paid and applied as one action; keep continuous dt simulation in mechanics and systems.
5. Everything platform-specific is injected
The core never calls requestAnimationFrame, localStorage or document — a CI guard enforces it. Platform concerns are injected at the edges:
| Concern | Contract | Browser implementation |
|---|---|---|
| Frame driving | FrameScheduler | createRafScheduler() |
| Rendering | RenderTarget | Renderer |
| Persistence | SaveAdapter | LocalStorageAdapter |
| Tab lifecycle | pause() / resume() | pageLifecycle() bridge |
Omit them all and you have a headless simulation you can drive from a test with manualScheduler or engine.advance(seconds).
Putting it together
txt
your game
├─ defines State (plain object)
├─ adds systems (state, dt) => void
├─ declares economy resources / costs / transactions
├─ uses mechanics producers / crafting / boosts / ...
├─ uses plugins autosave / offline-progress
└─ injects platform Renderer + createRafScheduler + pageLifecycle + LocalStorageAdapter
@idlekitjs/core runs the loop, tracks the state, emits the events.Continue with the architecture overview or jump into the quickstart.