1
0
Fork 0
polaris-task-force/docs/economy/supply-demand-design.md
Z8MB1E 447df220f5 feat(economy): add economy tick with demand simulation and price modifiers
Add the src/lib/economy service (GM baselines, deterministic per-period demand drift, price modifier computation, event emission) and the economy-tick bin. Gated by GameRules economy.enabled; period-idempotent with a cold-start observation gate.

Ultraworked with [Sisyphus](https://github.com/code-yeongju/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-06 17:22:30 -04:00

7 KiB
Raw Permalink Blame History

Supply & Demand Economy — Phase 1 Design

Status: Approved direction (decisions confirmed with product owner, 2026-09-06). Not yet implemented.

Confirmed decisions

Decision Choice
Supply model Abstract per-item ledger (market-state collection), GM baseline + transaction-driven adjustments. Never derived by scanning physical stockpiles.
Item scope Resources + tradeable assets only. Vehicles deferred to a later phase.
Pricing reach New NPC listings only + displayed reference price. Existing listings, player listings, and open negotiations are never repriced.
Simulation cadence New economy-tick bin (external cron, idempotent, SSE notify). market-tick consumes the resulting state.
GM surface Minimal per-item (baseline supply, baseline demand, price band, enable toggle) on Resources/Assets; global knobs on GameRules.

Current-state facts this builds on

  • Price anchors: Assets.marketTrading.perceivedValue.baseBuyPrice/baseSellPrice (src/lib/market/index.ts — getBaseBuyPrice/getBaseSellPrice), Resources.baseValue, Structures.sellPrice.
  • NPC listing generation: src/scripts/marketTick.ts — autoPrice (±15%) × vendor.priceModifier, minPrice = price × 0.6.
  • Real-demand signal sources that already exist: bank-transactions (type payment, memo carries asset + qty), market-listings (sold/buyer/soldAt), market:sale event logs with structured data.
  • Unwired production engine: Structures.autoGeneratedResources[] (min/max amount, frequency, produce chance) — schema exists, no tick consumes it.
  • Tick bin pattern: bin[] registration in src/payload.config.ts, src/scripts/<key>.ts exporting script, createBinLogger (src/scripts/lib/binFileLogger.ts), SSE notify via /api/game-tick/notify with x-game-tick-secret.
  • Negotiation system invariants to protect: NPC stance anchored to asking price, minPrice floor, patience meter, strictly-increasing buyer offers (src/lib/market/negotiations.ts).

Architecture

1. market-state collection (new) — the supply/demand ledger

One doc per item (itemType: "resource" | "asset", itemId). Fields:

  • gmBaselineSupply, gmBaselineDemand — numbers, GM-set (copied from per-item fields on first creation).
  • currentSupply — adjusted by: unit purchases from NPC listings (decrement), unit sales to NPC buyers (increment, later phase), production events (increment).
  • artificialDemand — slow-drifting NPC pressure, updated per tick (random walk bounded around gmBaselineDemand).
  • realDemand — decaying rolling aggregate of completed unit purchases (quantity-weighted), decayed each tick by a GameRules decay factor.
  • priceModifier — computed, bounded: derived from demand-vs-supply ratio, clamped to the item's band (±maxModifier).
  • lastComputedAt.

2. Per-item GM fields (Resources + Assets)

  • economy.enabled (default true for tradeable items)
  • economy.baselineSupply, economy.baselineDemand (numbers; defaults derived from rarity)
  • economy.maxModifier (price band, e.g. 0.5 → price can move ±50% of base)

3. GameRules global knobs (new group economy)

  • economyTickIntervalHint (docs for cron), realDemandWindowTicks, realDemandDecay, artificialDemandDrift, coldStartBehavior (use baseline until N observations), global price band default, economyEnabled master gate (pattern: staffHiringEnabled).

4. economy-tick bin (new)

src/scripts/economyTick.ts + logic in src/lib/economy/:

  1. For each enabled market-state doc (creating missing ones from GM baselines): decay realDemand, drift artificialDemand, recompute priceModifier from demand / supply ratio, clamp to band.
  2. Cold start: items with no purchase observations keep priceModifier = 1 (neutral).
  3. Emits a batch summary economy:tick event + per-item economy:price-change events only when the modifier crosses a visible threshold (avoid event spam).
  4. SSE notify (same path as other bins). Idempotent: a re-run recomputes the same values from stored state; drift steps are seeded deterministically per period.

5. Consumption by market-tick

When generating a new NPC auto listing, price becomes:

price = round(baseBuyPrice × priceModifier × vendor.priceModifier)

autoPrice ±15% randomness is dropped (the modifier replaces it) — minPrice = price × 0.6, desiredPrice = price unchanged, so negotiation invariants are untouched. A "reference price" (modifier-applied base) is displayed on item views for player pricing guidance.

Shortage behavior (phase 1): modifier raises prices and NPC restock quantity scales down when supply < demand (bounded, never zero without a GM flag).

6. Real-demand ingestion

Hook points (no new collection needed in phase 1 — read from existing records):

  • buyListing (src/lib/market/index.ts) — on successful purchase of an NPC/vendor listing, increment realDemand on that item's market-state (quantity-weighted).
  • Player-to-player sales do not move prices in phase 1 (no double counting).

Out of scope (explicit deferrals)

NPC faction economies, autonomous NPC buyers with wallets, production/crafting chains, vehicle market, physical-stock reconciliation, multi-currency, repricing existing listings/negotiations, real-time price streaming (refresh-on-tick only — SSE bus is single-instance).

Migration & rollout

  • New collection + fields = named migration (bun run payload migrate:create add_market_economy_state), registered in migrations/index.ts; dev applies via bun run payload migrate (push: false). Never db push.
  • Existing listings stay at their prices; modifier applies only to newly generated listings. economyEnabled master flag allows instant disable.
  • Regenerate src/payload-types.ts after schema changes.

Top risks (from design review)

  1. Non-atomic mutations — buyListing already credits locker before clearing payment; new flows must not compound this. Define failure/retry behavior before adding ledger writes.
  2. Negotiation breakage — never reprice under open negotiations (phase 1 avoids entirely by only touching new listings).
  3. Ledger/physical divergence — abstract supply will not track destroyed/admin-edited stock; GM can reset per-item state from the admin panel (rebuild from baselines).
  4. Tick duplication — external cron may retry; idempotency + deterministic period boundaries required.
  5. Scope explosion — every extension (factions, vehicles, production) is a separate approved phase.

Implementation phases

  1. P1a — schema: market-state collection, per-item economy fields, GameRules knobs, named migration, generated types.
  2. P1b — engine: src/lib/economy/ (modifier math, decay, drift, cold start) + unit-testable pure functions.
  3. P1c — tick: economy-tick bin + event types + SSE wiring.
  4. P1d — consumption: market-tick uses modifier for new NPC listings; restock scaling; reference price display; buyListing real-demand ingestion; GM reset action.