Skip to content

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 s

Expiry 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 yield

Persist the causes (owned upgrades) and republish on loaded — the registry itself is never saved. Boosts already republish themselves.

Notes

  • Order of use barely matters (the registry tolerates one-tick lag), but publisher-before-consumer reads most naturally.
  • speed ≤ 0 pauses a tier while keeping its progress — that's the debuff pattern above.
  • Full background on the resolution model: Modifiers.