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>
7 KiB
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(typepayment, memo carries asset + qty),market-listings(sold/buyer/soldAt),market:saleevent 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 insrc/payload.config.ts,src/scripts/<key>.tsexportingscript,createBinLogger(src/scripts/lib/binFileLogger.ts), SSE notify via/api/game-tick/notifywithx-game-tick-secret. - Negotiation system invariants to protect: NPC stance anchored to asking price,
minPricefloor, 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 aroundgmBaselineDemand).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,economyEnabledmaster gate (pattern:staffHiringEnabled).
4. economy-tick bin (new)
src/scripts/economyTick.ts + logic in src/lib/economy/:
- For each enabled market-state doc (creating missing ones from GM baselines): decay
realDemand, driftartificialDemand, recomputepriceModifierfromdemand / supplyratio, clamp to band. - Cold start: items with no purchase observations keep
priceModifier = 1(neutral). - Emits a batch summary
economy:tickevent + per-itemeconomy:price-changeevents only when the modifier crosses a visible threshold (avoid event spam). - 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, incrementrealDemandon 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 inmigrations/index.ts; dev applies viabun run payload migrate(push: false). Neverdb push. - Existing listings stay at their prices; modifier applies only to newly generated listings.
economyEnabledmaster flag allows instant disable. - Regenerate
src/payload-types.tsafter schema changes.
Top risks (from design review)
- Non-atomic mutations —
buyListingalready credits locker before clearing payment; new flows must not compound this. Define failure/retry behavior before adding ledger writes. - Negotiation breakage — never reprice under open negotiations (phase 1 avoids entirely by only touching new listings).
- 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).
- Tick duplication — external cron may retry; idempotency + deterministic period boundaries required.
- Scope explosion — every extension (factions, vehicles, production) is a separate approved phase.
Implementation phases
- P1a — schema:
market-statecollection, per-item economy fields, GameRules knobs, named migration, generated types. - P1b — engine:
src/lib/economy/(modifier math, decay, drift, cold start) + unit-testable pure functions. - P1c — tick:
economy-tickbin + event types + SSE wiring. - P1d — consumption: market-tick uses modifier for new NPC listings; restock scaling; reference price display;
buyListingreal-demand ingestion; GM reset action.