Appearance
Mechanics API
Signatures and types for every mechanic. Concepts, examples and pitfalls live on each mechanic's own page.
Producers
ts
import { producers } from "@idlekitjs/mechanics/producers";
function producers<T extends object>(options: ProducersOptions<T>): ProducersExtension<T>;ts
interface ProducerDef {
id: string;
cycleTime: number; // seconds per batch
yieldPerUnit: number; // units emitted per owned unit per cycle
baseCost: number;
costGrowth: number; // cost multiplier per purchased unit
tags?: string[]; // modifier targeting groups
}
interface ProducerColumn {
owned: number[]; // purchased -> cost curve
total: number[]; // present (owned + cascaded) -> output
progress: number[]; // seconds toward next cycle (mutated in place)
running?: boolean[]; // manual-cycle flags; absent = none in flight
}
interface ProducersOptions<T> {
definitions: ProducerDef[];
getColumn(state: T): ProducerColumn;
setColumn(state: T, patch: Partial<ProducerColumn>): void;
resource: { get(state: T): number; add(state: T, amount: number): void };
purchase?: {
getBudget(state: T, index: number): number; // scalar budget for tier `index`
pay(state: T, index: number, amount: number): void; // debit that budget
}; // default: resource.get / resource.add(-amount)
getIsAutomated?(state: T, index: number): boolean; // default: () => true
getYieldMultiplier?(state: T, index: number): number; // default 1
getSpeedMultiplier?(state: T, index: number): number; // default 1; <= 0 pauses
onPurchase?(index: number, state: T, result: PurchaseResult): void;
}
interface PurchaseResult {
bought: number;
spent: number;
remaining: number;
}ProducersExtension<T> methods:
| Method | Returns | Notes |
|---|---|---|
purchase(index, state) | boolean | Purchase one unit |
purchaseMany(index, state, quantity) | PurchaseResult | Purchase up to quantity |
purchaseWithBudget(index, state, budget) | PurchaseResult | Spend at most budget |
grant(index, state, quantity, options?) | boolean | Add whole units without paying; { owned: true } advances cost curve |
cost(index, state) | number | Next unit's price (Infinity for bad index) |
costFor(index, state, quantity) | number | Exact bulk price (geometric series) |
maxAffordable(index, state, budget?) | number | Units the budget (default: tier's budget) affords |
run(index, state) | boolean | Arm one manual cycle of a non-automated tier |
isRunning(state, index) | boolean | Cycle in flight (always true for automated + units) |
ratePerSecond(state, index?) | number | Average output/s with multipliers applied |
progressFraction(state, index) | number | [0, 1] toward the next cycle |
effectiveCycleTime(state, index) | number | Real s/cycle; Infinity when stopped/empty |
Modifiers
ts
import { modifiers, ModifierRegistry } from "@idlekitjs/mechanics/modifiers";
function modifiers<T extends object>(): ModifiersExtension<T>;
// ModifiersExtension<T> = Extension<T> & { registry: ModifierRegistry }ts
type ModifierTarget = { kind: "all" } | { kind: "id"; id: string } | { kind: "tag"; tag: string };
interface Modifier {
target: ModifierTarget;
stat: string; // "yield", "speed", or anything you define
op: "add" | "mult";
value: number;
}
interface ResolveQuery {
stat: string;
base?: number; // default 1
id?: string;
tags?: readonly string[];
}ModifierRegistry:
| Method | Behavior |
|---|---|
set(source, modifiers) | Replace a source's contribution atomically (empty = remove) |
remove(source) | Remove a source |
clear() | Remove everything |
resolve(query) | (base + Σ add) × Π mult over matching modifiers |
Never serialized — sources republish on loaded.
Collections
ts
import { collection } from "@idlekitjs/mechanics/collections";
function collection<T extends object>(options: CollectionOptions<T>): CollectionExtension<T>;Key types (full definitions in the source types are mirrored by your editor's IntelliSense):
ts
interface RarityDef { id: string; weight: number; defaults?: UpgradeDef; metadata?: ... }
interface CollectibleDef<T> {
id: string;
rarity: string;
tags?: string[];
effects?: CollectibleEffect[]; // Modifier shape with value: (level) => number
upgrade?: UpgradeDef; // { duplicates?, costs?, maxLevel? } — curves of level
eligibility?: Eligibility<T>; // visible / droppable / active / upgradeable predicates
metadata?: Record<string, unknown>;
}
interface PackDef<T> {
id: string;
draws?: WeightedInt; // legacy rarity-weighted model
rarityWeights?: Record<string, number>;
rewards?: PackRewardDef[]; // declarative model (cards/currency lines)
eligibility?: { visible?; openable? };
metadata?: Record<string, unknown>;
}
interface CollectionData {
collectibles: Record<string, { quantity: number; level: number }>;
rngState: number; // persisted PRNG
}Options: rarities, collectibles, packs, currencies ({ id, get, spend, add? }), registry (a ModifierRegistry), getData/setData, seed?, onChange?.
CollectionExtension<T> methods:
| Method | Returns | Notes |
|---|---|---|
openPack(packId, state) | OpenPackResult | Throws on unknown pack id |
canUpgrade(id, state) | UpgradeCheck | { ok, reason?, requirements } — UI-ready |
upgrade(id, state) | boolean | Spends duplicates + currencies, levels up |
getCollectible(id, state) | CollectibleView | undefined | UI view (level, quantity, metadata) |
getVisible(state) | CollectibleView[] | Every currently visible collectible |
rebuildModifiers(state) | void | Republish all active effects |
Upgrade block reasons: "not-owned" | "max-level" | "not-enough-duplicates" | "not-enough-currency" | "not-eligible".
Projects
ts
import { projects, ProjectManager } from "@idlekitjs/mechanics/projects";
function projects<T extends object>(options: ProjectsOptions<T>): ProjectsExtension<T>;
interface ProjectsOptions<T> {
projects: Project<T>[];
getCompleted(state: T): string[];
setCompleted(state: T, ids: string[]): void;
onComplete?(id: string, state: T): void;
}ProjectsExtension<T>: buy(id, state): boolean and manager: ProjectManager<T>.
ProjectManager<T>:
| Method | Behavior |
|---|---|
available(state) | Triggered, not-completed projects |
isCompleted(id) | Completion check |
complete(id, state) | Check affordability, apply effect, mark completed |
completedIds() | Serializable completion list |
restore(ids) | Rebuild from a save (unknown ids ignored) |
The Project<T> shape is exported by this mechanic — see Projects.
Crafting
ts
import {
crafting,
addResources,
subtractResources,
canAfford,
missingResources,
} from "@idlekitjs/mechanics/crafting";
function crafting<T extends object>(options: CraftingOptions<T>): CraftingExtension<T>;ts
type ResourceBag = Record<string, number>;
interface RecipeDef {
id: string;
inputs: ResourceBag;
outputs: ResourceBag;
duration: number; // seconds, finite > 0 (validated at wiring)
machineType?: string;
metadata?: Record<string, unknown>;
}
interface MachineDef {
id: string;
type?: string;
metadata?: Record<string, unknown>;
}
interface CraftingJob {
id: string; // default "<machineId>:<recipeId>"
recipeId: string;
machineId: string;
elapsed: number; // mutated in place
duration: number;
}
type CraftingStatus =
| { kind: "ready" }
| { kind: "unknown-recipe" }
| { kind: "unknown-machine" }
| { kind: "wrong-machine-type"; required: string; actual?: string }
| { kind: "machine-busy"; jobId: string } // running another recipe
| { kind: "missing-inputs"; missing: ResourceBag }
| { kind: "crafting"; job: CraftingJob; progress: number }; // running THIS recipeOptions: recipes, machines, getResources/setResources, getJobs/setJobs, jobId?(recipe, machine, state), onStart?(job, state), onComplete?(job, outputs, state).
CraftingExtension<T> methods:
| Method | Returns | Notes |
|---|---|---|
status(state, recipeId, machineId) | CraftingStatus | Never throws for runtime refusals |
canStart(state, recipeId, machineId) | boolean | status(...).kind === "ready" |
start(state, recipeId, machineId) | CraftingStatus | Consumes inputs, creates the job on ready |
cancel(state, machineId, { refund? }) | boolean | Refunds inputs by default |
activeJobs(state) | CraftingJob[] | Snapshot copy |
jobFor(state, machineId) | CraftingJob | undefined | |
progressFraction(state, machineId) | number | [0, 1]; 0 when idle |
ResourceBag helpers (pure, non-mutating; missing keys = 0): addResources(bag, delta), subtractResources(bag, cost), canAfford(bag, cost), missingResources(bag, cost).
Boosts
ts
import { boosts } from "@idlekitjs/mechanics/boosts";
function boosts<T extends object>(options: BoostsOptions<T>): BoostsExtension<T>;ts
type BoostStacking = "refresh" | "extend" | "stack";
interface BoostDef {
id: string;
duration: number; // seconds, finite > 0 (validated at wiring)
maxDuration?: number; // cap for "extend" and extend(); >= duration
stacking?: BoostStacking; // default "refresh"
maxStacks?: number; // integer >= 1, for "stack"
effects?: Modifier[]; // published under "boost:<id>" while active
metadata?: Record<string, unknown>;
}
interface ActiveBoost {
id: string;
remaining: number; // mutated in place
stacks: number; // >= 1
}Options: definitions, getActive/setActive (Record<string, ActiveBoost>), registry? (ModifierRegistry), onGrant?(boost, state), onExpire?(id, state) (timer/negative-extend expiry only — not manual remove).
BoostsExtension<T> methods:
| Method | Returns | Notes |
|---|---|---|
grant(state, id) | ActiveBoost | Applies the stacking policy; throws on unknown id |
extend(state, id, seconds) | boolean | Capped by maxDuration; result ≤ 0 expires |
remove(state, id) | boolean | Retracts modifiers; no onExpire |
isActive(state, id) | boolean | |
get(state, id) | ActiveBoost | undefined | |
active(state) | ActiveBoost[] | Snapshot |
rebuildModifiers(state) | void | Republish all active effects |
Stacked effect scaling: add → value × stacks; mult → value ^ stacks.
Containers
ts
import { containers } from "@idlekitjs/mechanics/containers";
function containers<T extends object>(options: ContainersOptions<T>): ContainersExtension<T>;ts
interface ContainerDef<T> {
id: string;
capacity: number | ((state: T) => number); // finite > 0 (validated)
volumeOf?: (contentId: string, state: T) => number; // default 1 per unit
metadata?: Record<string, unknown>;
}
type ContainerContents = Record<string, number>; // entries always > 0
type ContainersData = Record<string, ContainerContents>;Options: definitions, getData/setData (ContainersData, reassigned on change). Containers own finite capacity with multiple content types — not rewards, not periodic logic. used = sum(amount × volumeOf(contentId)).
ContainersExtension<T> methods:
| Method | Returns | Notes |
|---|---|---|
used(state, containerId) | number | 0 for unknown ids |
free(state, containerId) | number | capacity - used; negative if a dynamic capacity shrank |
capacity(state, containerId) | number | Dynamic capacities resolving invalid throw |
contents(state, containerId) | ContainerContents | Snapshot |
canFit(state, containerId, contentId, n?) | boolean | n defaults to 1 |
fill(state, containerId, contentId, n?) | boolean | All-or-nothing; false without mutation when it does not fit |
fillUpTo(state, containerId, contentId, n) | number | Accepts what fits; returns the accepted amount |
drain(state, containerId, input?) | DrainResult | No input = everything; { contentId } = one type; { contentId, amount } = partial |
drain returns { removed, remaining }. User-facing misuse (unknown container, bad amounts) is safe: reads report empty/zero, fills fail, drains remove nothing. Duplicate ids and invalid static capacities throw at wiring.
Timers
ts
import { timers } from "@idlekitjs/mechanics/timers";
function timers<T extends object>(options: TimersOptions<T>): TimersExtension<T>;ts
interface TimerDef {
id: string;
every: number; // seconds between firings, finite > 0 (validated)
autoStart?: boolean; // default true
metadata?: Record<string, unknown>;
}
interface TimerState {
remaining: number; // mutated in place
running: boolean;
}Options: definitions, getData/setData (Record<string, TimerState>; missing entries are materialized from the defs on the first update), onFire?(id, state, fires).
Timers own generic periodic firing — the effect stays in onFire. A large dt (offline catch-up) covering several periods calls onFire once with fires > 1, so the game settles missed firings in one pass.
TimersExtension<T> methods:
| Method | Returns | Notes |
|---|---|---|
start(state, id) | boolean | Resumes; progress is kept, not reset |
stop(state, id) | boolean | Pauses, freezing progress |
isRunning(state, id) | boolean | |
trigger(state, id) | boolean | Fires once manually (running or not), resets the period |
progressFraction(state, id) | number | [0, 1], for a bar |
Unknown ids return false/0.
Pickups
ts
import { createPickupsData, pickups } from "@idlekitjs/mechanics/pickups";
function pickups<T extends object>(options: PickupsOptions<T>): PickupsExtension<T>;
function createPickupsData(seed?: number): PickupsData; // fresh sub-statets
interface PickupDef<T> {
id: string; // type id
lifetime?: number; // seconds before expiry; omit = never
spawn?: {
every: number; // seconds between spawns
max?: number; // cap on simultaneously active items
when?: (state: T) => boolean; // while false, no time accumulates
};
position?: (state: T, random: () => number) => { x: number; y: number };
metadata?: Record<string, unknown>; // rendering hints, never interpreted here
}
interface PickupItem {
id: string; // "<type>#<n>"
type: string;
remaining?: number; // mutated in place
lifetime?: number;
position?: { x: number; y: number };
metadata?: Record<string, unknown>;
}
type PickupView = PickupItem & { lifetimeFraction?: number }; // 0 fresh -> 1 expiringOptions: definitions, getData/setData (PickupsData: items, id counter, persisted PRNG state, spawn accumulators), onSpawn?, onExpire?.
Pickups own temporary, individually identifiable items the player can act on before they expire. Spawning never grants anything — settle collects through an Economy transaction whose apply calls take (see the pickups Economy adapter). Auto-spawned types must declare lifetime and/or spawn.max (validated at wiring), which bounds offline catch-up: a large dt back-spawns only items that would still be alive, with staggered countdowns, never above max.
PickupsExtension<T> methods:
| Method | Returns | Notes |
|---|---|---|
spawn(state, type, options?) | PickupItem | undefined | Manual spawn; undefined for unknown types. Options: position, metadata (merged over the def's), lifetime override |
status(state, id) | PickupStatus | "ready" | "unknown" | "expired" discriminated union |
take(state, id) | PickupItem | undefined | Removes and returns a ready item — the collect settlement primitive |
visible(state) | PickupView[] | Active items with lifetimeFraction, for rendering |
active(state, type?) | number | Count of active items |
Economy adapter subpaths
These adapters are official but optional. They are not exported by the @idlekitjs/mechanics barrel.
ts
import {
economyPurchase,
producerOutput,
producerPurchase,
producerResourceId,
producerResources,
} from "@idlekitjs/mechanics/producers/economy";
import {
cardLevelAtLeast,
cardLevelBelow,
cardLevelResourceId,
cardResources,
cardShardResourceId,
hasCardShards,
} from "@idlekitjs/mechanics/collections/economy";
import {
amountsToBag,
bagToAmounts,
craftingResources,
recipeCost,
recipeReward,
} from "@idlekitjs/mechanics/crafting/economy";
import { projectFromTransaction } from "@idlekitjs/mechanics/projects/economy";
import { activateBoost, boostTokenResourceId } from "@idlekitjs/mechanics/boosts/economy";
import {
containerContentResources,
containerFreeSpaceResource,
containerHasSpace,
} from "@idlekitjs/mechanics/containers/economy";
import { collectPickup, pickupAvailable } from "@idlekitjs/mechanics/pickups/economy";See Economy adapters for behavior and examples.