Skip to content

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
                               └─────────────┘
PackageRoleMay depend on
@idlekitjs/typesShared contracts, zero logic
@idlekitjs/utilsPure helpers, platform-agnostic
@idlekitjs/coreHeadless enginetypes
@idlekitjs/economyResources, costs, rewards, transactionsutils
@idlekitjs/domDOM renderer & bindingstypes, core, utils
@idlekitjs/browserBrowser runtime bridgestypes, core, utils
@idlekitjs/mechanicsGameplay primitives and Economy bridgestypes, core, utils; economy only in */economy subpaths
@idlekitjs/pluginsEngine policiestypes, core
@idlekitjs/storagePersistence backendstypes
@idlekitjs/devtoolsDevelopment overlay and metrics (private)types, core, utils

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 mechanically enforced invariant (pnpm check:headless-core fails 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 DOM Renderer, or nothing);
  • persistence goes through a SaveAdapter;
  • tab lifecycle is an opt-in bridge (pageLifecycle()) driving the public pause()/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/economy

They 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.