Appearance
Boosted production
The full composition: boosts publish into the modifier registry, producers resolve against it — three mechanics cooperating without knowing each other.
Wiring
ts
import { createEngine } from "@idlekitjs/core";
import { producers, type ProducerDef } from "@idlekitjs/mechanics/producers";
import { modifiers } from "@idlekitjs/mechanics/modifiers";
import { boosts, type ActiveBoost } from "@idlekitjs/mechanics/boosts";
interface State {
gold: number;
owned: number[];
total: number[];
progress: number[];
boosts: { active: Record<string, ActiveBoost> };
}
const TIERS: ProducerDef[] = [
{ id: "miner", cycleTime: 1, yieldPerUnit: 1, baseCost: 10, costGrowth: 1.15, tags: ["worker"] },
];
const engine = createEngine<State>({
initialState: {
gold: 15,
owned: [0],
total: [0],
progress: [0],
boosts: { active: {} },
},
});
// 1. The hub
const modifiersExt = modifiers<State>();
const registry = modifiersExt.registry;
// 2. The consumer: producers resolve yield & speed per tier
const mine = producers<State>({
definitions: TIERS,
getColumn: (s) => ({ owned: s.owned, total: s.total, progress: s.progress }),
setColumn: (s, patch) => {
if (patch.owned) s.owned = patch.owned;
if (patch.total) s.total = patch.total;
if (patch.progress) s.progress = patch.progress;
},
resource: {
get: (s) => s.gold,
add: (s, amount) => {
s.gold += amount;
},
},
getYieldMultiplier: (_s, i) =>
registry.resolve({ stat: "yield", id: TIERS[i].id, tags: TIERS[i].tags }),
getSpeedMultiplier: (_s, i) =>
registry.resolve({ stat: "speed", id: TIERS[i].id, tags: TIERS[i].tags }),
});
// 3. The publisher: boosts push effects while active
const boostExt = boosts<State>({
definitions: [
{
id: "gold-rush",
duration: 30,
effects: [{ target: { kind: "tag", tag: "worker" }, stat: "yield", op: "mult", value: 2 }],
},
{
id: "espresso",
duration: 20,
stacking: "stack",
maxStacks: 3, // ×2, ×4, ×8 — mult compounds per stack
effects: [{ target: { kind: "all" }, stat: "speed", op: "mult", value: 2 }],
},
{
id: "cave-in", // a debuff is just a reducing value
duration: 10,
effects: [{ target: { kind: "id", id: "miner" }, stat: "speed", op: "mult", value: 0 }],
},
],
getActive: (s) => s.boosts.active,
setActive: (s, active) => {
s.boosts.active = active;
},
registry, // <- the whole integration
});
engine.use(modifiersExt).use(mine).use(boostExt);Try it
ts
mine.purchaseMany(0, engine.state, 1);
boostExt.grant(engine.state, "gold-rush"); // miners yield ×2 for 30 s
boostExt.grant(engine.state, "espresso"); // everything runs ×2 faster…
boostExt.grant(engine.state, "espresso"); // …now ×4 (stacked)
mine.ratePerSecond(engine.state); // reflects yield × speed right now
boostExt.grant(engine.state, "cave-in"); // speed ×0 -> the miner tier pauses,
// progress frozen, resumes after 10 sExpiry retracts each boost's contribution atomically — production returns to baseline with no leftover multiplier, guaranteed (the registry entry boost:<id> is removed, not "counter-multiplied").
Permanent bonuses on the same stack
Upgrades and projects publish to the same registry under their own sources — they combine with boosts in the standard (base + Σadd) × Πmult stack:
ts
registry.set("upgrade:steel-picks", [
{ target: { kind: "tag", tag: "worker" }, stat: "yield", op: "add", value: 0.5 },
]);
// gold-rush active: (1 + 0.5) × 2 = ×3 yieldPersist the causes (owned upgrades) and republish on loaded — the registry itself is never saved. Boosts already republish themselves.
Notes
- Order of
usebarely matters (the registry tolerates one-tick lag), but publisher-before-consumer reads most naturally. speed ≤ 0pauses a tier while keeping its progress — that's the debuff pattern above.- Full background on the resolution model: Modifiers.