Skip to content

Mechanics overview

A mechanic is a generic gameplay primitive: the machinery of an idle-game system — cost curves, drop tables, job lifecycles, effect stacking — packaged as an Extension<T> and stripped of every game-specific decision. Your game provides the data (definitions, names, balancing); the mechanic provides the behavior.

The catalog

MechanicGives youTypical use
ProducersTiered production, geometric costs, bulk buy, cascadesGenerators, factories, populations
ModifiersA shared registry of stat bonuses resolved per targetUpgrades, buffs, card effects
CollectionsSeeded gacha packs, collectible levels, effect publishingCards, artifacts, pets
ProjectsOne-shot/repeatable purchases with triggers and persistenceResearch, upgrades, story beats
CraftingTimed transformation: inputs → job → duration → outputsKitchens, workshops, refineries
BoostsTemporary stackable effects with clean expiryPower-ups, buffs, debuffs
ContainersFinite-capacity holders with multiple content typesInventories, bins, tanks
PickupsTemporary identified items with lifetime and collectionLoot, bonuses, map items
TimersGeneric periodic gameplay triggersDrains, refreshes, patrols

They compose: collections and boosts publish effects into the modifier registry; producers resolve their yield and speed against it — none of them knowing the others exist.

They also have optional Economy adapters. Those adapters are official but live in @idlekitjs/mechanics/<mechanic>/economy, not in the mechanics barrel and not in @idlekitjs/economy/adapters. The core mechanics remain Economy-free.

Mechanic vs game code

The line is simple: mechanism in the package, meaning in the game.

ts
// The mechanic knows HOW producers work…
const ext = producers<State>({
  definitions: TIERS, // …your game says WHAT the tiers are
  getColumn: (s) => ({ owned: s.owned, total: s.total, progress: s.progress }),
  setColumn: (s, patch) => {
    /* write back */
  },
  resource: {
    get: (s) => s.potatoes,
    add: (s, n) => {
      s.potatoes += n;
    },
  },
});

A mechanic never contains a resource name, an icon, a price point or a balance decision. If you find yourself wanting one in a mechanic, it belongs in your game's data files.

Mechanic vs plugin

Both are extensions; the difference is what they contribute. Mechanics are rules — remove producers and your game plays differently. Plugins are practicality — remove autosave and your game plays the same but loses progress. The packaging follows the distinction: gameplay primitives here, browser bridges in @idlekitjs/browser, and engine policies in @idlekitjs/plugins.

The shared conventions

Mechanics share the same wiring style. Once you've wired one, the others use the same patterns:

  1. Factory in, extension out. producers(options) returns a ProducersExtension<T> — the base Extension<T> plus public methods (purchase, start, grant, ...). Keep the reference; engine.use installs it but doesn't hand it back.
  2. State through accessors. Runtime data (owned counts, jobs, active boosts) lives in your state as plain JSON, read/written through get*/set* options. Saving needs nothing extra.
  3. Reassign for reactivity. Structural writes go through set* with fresh objects so dirty-key tracking sees them. High-frequency counters (cycle progress, job elapsed) are mutated in place on purpose — read them via helpers like progressFraction.
  4. When a mechanic has update(state, dt), it tolerates any dt. Large offline catch-ups complete every due cycle/job/expiry in one pass.
  5. Load handling is mechanic-specific. Mechanics with derived data or completion indexes rebuild it on loaded; smaller primitives with only plain state do not need a load hook.
  6. Callbacks, not custom events. Mechanics expose callbacks such as onPurchase, onComplete, onChange and onExpire when they need to report changes.
  7. Statuses for runtime refusals, throws for bad definitions. "Can't afford" returns a status your UI can render; a duplicate recipe id throws at startup.
  8. Economy is optional. Use Economy adapters when you want explicit resources and atomic transactions around a mechanic; skip them when a mechanic's native API is enough.

Adding your own

A custom mechanic is just an extension following the conventions above — the extensions guide shows the factory pattern. If it's generic enough for two games, structure it like the built-ins (module conventions) and it will feel native.