Appearance
Architecture overview
IdleKit is a monorepo of focused packages with strict, one-way boundaries. Understanding the layering takes a minute and explains every design decision in the toolkit.
The layers
txt
┌──────────────────┐
│ your game │ content, balancing, UI
└────────┬─────────┘
┌────────┬─────────┬──────────┬────────────┬───────────┬─────────┐
▼ ▼ ▼ ▼ ▼ ▼ ▼
┌────────┐ ┌───────┐ ┌─────────┐ ┌────────────┐ ┌─────────┐ ┌─────────┐ ┌─────┐
│ dom │ │browser│ │ economy │ │ mechanics │ │ plugins │ │ storage │ │utils│
└───┬────┘ └───┬───┘ └────┬────┘ └─────┬──────┘ └────┬────┘ └────┬────┘ └──┬──┘
│ │ │ │ │ │ │
└──────────┴──────────┴────────────┴──────┬──────┴───────────┘ │
▼ │
┌────────────┐ │
│ core │ ◄────────────────────┘
└─────┬──────┘
▼
┌────────────┐
│ types │ shared contracts
└────────────┘| Package | Role | May depend on |
|---|---|---|
@idlekitjs/types | Shared contracts, zero logic | — |
@idlekitjs/utils | Pure helpers, platform-agnostic | — |
@idlekitjs/core | Headless engine | types |
@idlekitjs/economy | Resources, costs, rewards, transactions | utils |
@idlekitjs/dom | DOM renderer & bindings | types, core, utils |
@idlekitjs/browser | Browser runtime bridges | types, core, utils |
@idlekitjs/mechanics | Gameplay primitives and Economy bridges | types, core, utils; economy only in */economy subpaths |
@idlekitjs/plugins | Engine policies | types, core |
@idlekitjs/storage | Persistence backends | types |
The arrows never point up or sideways: core cannot import dom, mechanics cannot import storage, and nothing imports a game.
The headless core
@idlekitjs/core contains no browser code — not as a style preference, but as a CI-enforced invariant (npm run check:headless-core fails the build if a browser global appears in core source). Platform concerns are injected:
- frames come from a
FrameScheduler(createRafScheduler()in the browser,manualScheduler()in tests); - rendering goes through a
RenderTarget(the DOMRenderer, or nothing); - persistence goes through a
SaveAdapter; - tab lifecycle is an opt-in bridge (
pageLifecycle()) driving the publicpause()/resume().
What this buys you in practice:
- headless tests that run the real simulation deterministically, no DOM mocks;
- balance scripts and server-side simulation with the same code;
- portability: swapping the renderer or storage backend never touches game logic.
Mechanics vs plugins
Both are Extension<T>s, installed with engine.use(...). The split is about what they contribute:
- Mechanics are gameplay: producers, crafting, boosts... They define how numbers grow. If removing it changes the rules of your game, it is a mechanic.
- Plugins are support: autosave, offline progress. They make the game practical to ship. If removing it leaves the rules intact but the experience worse, it is a plugin.
The rule for both: generic mechanism in the package, specific meaning in the game. producers knows its scalar producer cost curve; it does not know what a "clipper" costs. boosts knows stacking policies; it does not know what a rewarded ad is.
Economy vs mechanics
@idlekitjs/economy is the pure economic vocabulary and transaction runtime: explicit resources, state accessors, amounts, costs, rewards, requirements, cost curves and formatting view models. It never owns game state, never scans state automatically, never runs dt, and never replaces a simulation mechanic.
txt
Continuous simulation / dt / progress / timers -> mechanics.
Atomic actions with requirements + cost + reward + apply -> economy transactions.That boundary keeps both sides honest. Producers still own production cycles, crafting still owns jobs, projects still own completion bookkeeping, and boosts still own timers. Economy settles player actions such as purchases, claims and upgrades against resources that the game declares explicitly.
Official bridges live in mechanics subpaths:
txt
@idlekitjs/mechanics/producers/economy
@idlekitjs/mechanics/collections/economy
@idlekitjs/mechanics/crafting/economy
@idlekitjs/mechanics/projects/economy
@idlekitjs/mechanics/boosts/economy
@idlekitjs/mechanics/containers/economy
@idlekitjs/mechanics/pickups/economyThey are not exported from @idlekitjs/mechanics, and they do not live under @idlekitjs/economy/adapters. The adapter depends on both the mechanic and Economy, so placing it next to the mechanic avoids a cycle (economy -> mechanics -> economy) and keeps the mechanic's core files usable without Economy.
One module, one folder, one public subpath
Every autonomous brick lives in its own folder and is imported from its own public subpath:
ts
import { producers } from "@idlekitjs/mechanics/producers";
import { crafting } from "@idlekitjs/mechanics/crafting";
import { autosave } from "@idlekitjs/plugins/autosave";
import { LocalStorageAdapter } from "@idlekitjs/storage/local-storage";Helpers that belong to a module (ProjectManager, ModifierRegistry, SaveScheduler, the ResourceBag helpers, Economy adapters) are exported from that module's subpath, never as their own unrelated address. Deep imports into a module's internals are not part of the public API. See module conventions.
Dogfooding
The repository ships complete games in games/ (a paperclips-style clicker, two cascade-production games, a survival idle) built exclusively on the public APIs documented here. games/adventure-communist/src/content/economy.ts is the first Economy dogfood: potatoes, science and producer units are registered as resources, the producer purchase seam is implemented by economyPurchase, and a differential economy-wiring.test.ts proves parity with the native producer purchase behavior.
Related pages
- Package boundaries — the rules and how they are enforced.
- Module conventions — how packages are laid out and how new bricks are added.
- Core concepts — the runtime model.