Skip to content

Economy API

Signatures and behavior for @idlekitjs/economy. Concepts and recipes live on the package page.

Imports

ts
import {
  createEconomy,
  stateKey,
  resourceAtLeast,
  describeFailure,
} from "@idlekitjs/economy";

import { arrayIndex, recordField } from "@idlekitjs/economy/accessors";
import { costCurve, flat, geometric } from "@idlekitjs/economy/cost-curves";

Public subpaths:

SubpathExports
@idlekitjs/economyFull facade
@idlekitjs/economy/accessorsAccessor helpers
@idlekitjs/economy/cost-curvesCost curve helpers

Resources

ts
type ResourceId = string;

interface ResourceAccessor<T> {
  get(state: T): number;
  add(state: T, amount: number): void;
}

type ResourceInit<T> =
  | {
      id: ResourceId;
      label?: string;
      description?: string;
      format?: (amount: number) => string;
      integer?: boolean;
      min?: number;
      max?: number;
      tags?: readonly string[];
      metadata?: Record<string, unknown>;
      accessor: ResourceAccessor<T>;
    }
  | {
      id: ResourceId;
      label?: string;
      description?: string;
      format?: (amount: number) => string;
      integer?: boolean;
      min?: number;
      max?: number;
      tags?: readonly string[];
      metadata?: Record<string, unknown>;
      get(state: T): number;
      add(state: T, amount: number): void;
    };

interface ResourceDef<T> {
  id: ResourceId;
  label: string;
  accessor: ResourceAccessor<T>;
  format?: (amount: number) => string;
  integer: boolean;
  min: number;
  max: number;
  readonly: boolean;
  tags: readonly string[];
  description?: string;
  metadata?: Record<string, unknown>;
}

Helpers:

ExportBehavior
defineResourceNormalize a ResourceInit into ResourceDef
validateResourceIdThrows on empty, whitespace, leading/trailing :

Accessors

ExportUse forWrite behavior
stateKeyTop-level numeric state fieldAssigns field in place
arrayIndexOne numeric array slotClones array, calls setArray
recordFieldNumeric field inside a recordClones record and entry, calls setRecord
computedHand-written get/add pairPass-through
readonlyObservable non-writable valueget only; transactions reject in cost/reward

Economy

ts
function createEconomy<T>(options?: EconomyOptions): Economy<T>;

interface EconomyOptions {
  format?: (amount: number) => string;
}

interface Economy<T> {
  resource(init: ResourceInit<T>): this;
  resource(id: ResourceId): ResourceDef<T>;
  resources(inits: readonly ResourceInit<T>[]): this;

  has(id: ResourceId): boolean;
  ids(): readonly ResourceId[];
  get(state: T, id: ResourceId): number;
  add(state: T, id: ResourceId, amount: number): number;
  spend(state: T, id: ResourceId, amount: number): boolean;

  canAfford(state: T, cost: AmountsInput): boolean;
  missing(state: T, cost: AmountsInput): Cost;
  pay(state: T, cost: AmountsInput): void;
  credit(state: T, reward: AmountsInput): Reward;

  preview(state: T, transaction: Transaction<T>): TransactionPreview;
  execute(state: T, transaction: Transaction<T>): TransactionResult;
  canExecute(state: T, transaction: Transaction<T>): boolean;

  formatAmount(id: ResourceId, amount: number): string;
  formatCost(state: T, cost: AmountsInput): readonly FormattedCostLine[];
  formatReward(reward: AmountsInput): readonly FormattedAmount[];
  audit(state: T): readonly TransactionFailure[];
}

Direct operations throw EconomyError on programming mistakes. Transaction operations return diagnostics for content-shaped failures.

Amounts

ts
interface ResourceAmount {
  resourceId: ResourceId;
  amount: number;
}

type Cost = readonly ResourceAmount[];
type Reward = readonly ResourceAmount[];
type AmountsInput = readonly (readonly [ResourceId, number] | ResourceAmount)[];

Exports:

ExportBehavior
normalizeAmountsStrict normalize; throws on invalid lines
collectAmountsLenient normalize; returns invalid lines
mergeAmountsMerge several inputs
scaleAmountsScale by finite factor >= 0

Normalization merges duplicates, drops zero totals and rejects negative, NaN and Infinity amounts.

Requirements

ts
interface Requirement<T> {
  id: string;
  label?: string;
  isMet(state: T, economy: EconomyReader<T>): boolean;
  progress?(state: T, economy: EconomyReader<T>): { current: number; target: number };
}

Exports:

ExportBehavior
resourceAtLeastNon-consuming >= resource gate
resourceAtMostNon-consuming <= resource gate
allOfAll child requirements must pass
notNegate one requirement

Transactions

ts
type CostInput<T> = AmountsInput | ((state: T) => AmountsInput);
type RewardInput<T> = AmountsInput | ((state: T) => AmountsInput);

interface Transaction<T> {
  id: string;
  label?: string;
  requirements?: readonly Requirement<T>[];
  cost?: CostInput<T>;
  reward?: RewardInput<T>;
  apply?: (state: T) => void;
  metadata?: Record<string, unknown>;
}
ts
interface TransactionPreview {
  ok: boolean;
  failures: readonly TransactionFailure[];
  cost: Cost;
  reward: Reward;
  missing: Cost;
  overflow: Reward;
}

type TransactionResult =
  | { ok: true; cost: Cost; reward: Reward; overflow: Reward }
  | { ok: false; failures: readonly TransactionFailure[]; missing: Cost };

Failure variants:

ts
type TransactionFailure =
  | { kind: "unknown-resource"; resourceId: ResourceId; where: "cost" | "reward" }
  | { kind: "invalid-amount"; resourceId: ResourceId; amount: number; where: "cost" | "reward" }
  | { kind: "invalid-state"; resourceId: ResourceId }
  | {
      kind: "requirement-failed";
      requirementId: string;
      label?: string;
      progress?: { current: number; target: number };
    }
  | { kind: "cannot-afford"; resourceId: ResourceId; required: number; available: number }
  | { kind: "readonly-resource"; resourceId: ResourceId; where: "cost" | "reward" };

Exports:

ExportBehavior
previewTransactionPure transaction evaluation
executeTransactionPreview, then pay -> apply -> credit

apply must not fail after preview. There is no generic rollback.

Formatting

ts
interface FormattedAmount {
  resourceId: ResourceId;
  label: string;
  amount: string;
  rawAmount: number;
}

interface FormattedCostLine extends FormattedAmount {
  available: number;
  affordable: boolean;
}

function describeFailure(
  failure: TransactionFailure,
  options?: { label?: (id: ResourceId) => string },
): string;

formatCost and formatReward return UI-agnostic view models. Economy never touches the DOM and never requires icons, colors or layout metadata.

Cost Curves

ts
type Rounding = "none" | "floor" | "ceil" | "round";

type CostCurveLine =
  | { kind: "flat"; resourceId: ResourceId; amount: number; round?: Rounding }
  | {
      kind: "geometric";
      resourceId: ResourceId;
      baseAmount: number;
      growth: number;
      round?: Rounding;
    };

interface CostCurve<T> {
  costFor(state: T, quantity: number): Cost;
  next(state: T): Cost;
  maxAffordable(state: T, economy: EconomyReader<T>): number;
}

Exports:

ExportBehavior
flatFlat per-unit cost line
geometricGeometric per-unit cost line
costCurveBuild a multi-line curve
geometricSumClosed-form sum helper
geometricAffordableClosed-form affordability helper

growth === 1 is linear. Invalid growth or negative amounts throw at wiring time. costFor(quantity <= 0) returns [].

Mechanics Adapters

Economy adapters are official mechanics subpaths, not Economy 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

See Economy adapters for adapter APIs and examples.