# Orbit — Trade & Economy (design) This is the design for the game's trade/economy. It hangs off the deterministic galaxy Orbit already has — the jump network, factions, planet classes, settlements, the SHOP button on the surface deck, and the `ship.stats.cargoHold` slot — so nothing here requires new world-structure. **Status: MODEL v2 — IMPLEMENTED.** The goods catalog (`data/goods.json`) and the tuning knobs (`data/economy.json`) are the source of truth; the engine (`js/economy/Economy.js`), the batch clock (`js/economy/Stock.js`), and the shop UI (`js/ui/shop/*`) are built. Model v2 — **ONE WORLD, ONE GOOD** — was locked in after v1's price model (producer/demand class discounts) turned out to make **distance a weak signal** and, worse, left a **buy→sell→buyback infinite-credit loop** at any shop. v2's invariants are enforced by `dev/economy.test.mjs`. --- ## 1. Design goals (v2) 1. **Distance is the dominant factor in profit.** A good's sell price grows with its hop distance from its ORIGIN (the one world that makes it). Longer journey ⇒ meaningfully better reward. This is the business model. 2. **Planet type is a secondary modifier.** A world's class contributes a small demand bonus (the class *wants* the good) and drives the THEMATIC FIT of the origin assignment (a gas world is where fuel is made) — but it never overrides distance. 3. **Goods are regionally produced — one world, one good.** Every tradeable good has exactly ONE origin system (a seeded strict 1:1 assignment); every settled system sells exactly ONE product — its signature good, cheap, in a daily batch. The catalog's remainder is the **hub pool**, resold by stations/capitals at standard price. 4. **Same-system trading earns money, but it is not the business model.** A buy-local/sell-local loop in one system is *bounded* by the daily batch (a few units a day) — pocket change. Long-haul SELLs are unlimited: the market is a sink. 5. **Interact, don't control.** No market simulation, no stock you can manipulate, no persistent shop state beyond the day's remaining batch. The economy is a landscape you travel through and exploit. 6. **Local money is possible without crossing the galaxy.** You never *have* to cross the map to make a living; you choose to, for much more. --- ## 2. The core loop ``` LAND on the ORIGIN world SHIP a good LAND far away ┌──────────────┐ (jump) ┌──────────────┐ │ open SHOP │ ◄──────────────────────────────── │ SELL for │ │ BUY the │ │ CREDITS │ │ signature │ ─────────────────────────────────► │ (dear — it │ │ (half price,│ carry in cargoHold │ is far from │ │ daily batch│ │ its home) │ └──────────────┘ └──────────────┘ ▲ │ └──────────────── spend credits on builds/quests ◄──────┘ ``` - **BUY** a world's signature good at its ORIGIN — the only place it is cheap (half of base value), limited to the day's batch. - **CARRY** it in your cargo hold (`ship.stats.cargoHold`). - **SELL** it wherever a class wants it — the price rises with hop distance from the origin (the banded term). The further you carry it, the more it's worth. - **PROFIT** = sell price − buy price. The gap is *dominated* by distance. - **SPEND** credits on ship builds, quest rewards, upgrades. You are never *balancing* a market. You are *finding* an arbitrage the galaxy already set up and running it. --- ## 3. The eight economic classes `data/goods.json` defines them; they map straight onto existing structures: | Class | Where it exists | Thematic producers (origin fit) | Strongly WANTS (+10% demand bonus) | |---|---|---|---| | **terran** | `terran` planets | food, water, timber, pharma, biology | rare metals, fuel, heat goods, high-end tech | | **arid** | `arid` planets | salt, silica, solar, spice | water, humidity, coolants, food | | **rocky** | `rocky` planets | ores, metal, stone | fuel, food, water, processed tech | | **gas** | `gas` planets | fuel, gases, isotopes | solid metal, food, water, hull parts | | **ice** | `ice` planets | water, ice, cryo, volatiles | heat, energy, food, fuel | | **lava** | `lava` planets | heat, rare metals, volcanic glass | coolants, water, food, fuel | | **station** | deep-space stations | (hubs — resellers, not producers) | raw ore, food, water, bulk fuel | | **capital** | a faction's seat | (hubs — resellers, not producers) | everything, at a premium (the universal buyer) | In v2 the classes play TWO roles, both secondary to distance: - **Origin fit** — the seeded origin assignment prefers systems whose classes *produce* the good (a gas world gets fuel, an ice world gets cryo). The `producer` list is the fit signal; where the catalog leaves no fitting system, the assignment falls back (a few goods are "anywhere" — the deterministic seed still fixes exactly where). - **Demand** — a class in the good's `demand` list pays a +10% bonus (`economy.json → pricing.demandBonus`). The `capital` class wants everything: the great sink, the universal buyer, the long-haul ceiling. The opposition pairs (ice ⇄ lava, terran ⇄ arid, gas ⇄ rocky) still give the catalog its flavor and keep the demand web symmetric — but the *price* no longer derives from "produces here, wants there"; it derives from **how far from the origin you sell it**. --- ## 4. One world, one good (the v2 identity) v1 gave every shop a floor of 8–16 class-appropriate goods and a per-class signature gem. v2 replaces both with a single rule: > **Every settled system has exactly ONE signature good — and every signature > good has exactly ONE origin system.** - **The assignment is a seeded strict bijection** (the `origin-assign` pass in `Economy.buildEconomy`, greedy most-constrained-first with zone→tier fit and a seeded tie-break). 78 settled systems ↔ 78 signature goods out of the 114-goods catalog; the 12 jump-gate-only (barren) systems host nothing — there is no world to sell from. - **The catalog's remainder (36 goods) is the HUB POOL** — no origin world; stations and capitals resell a seeded selection of it (standard price, wide floors for variety). - **Thematic fit is near-total** — the origin assignment prefers systems whose classes produce the good (in the test seed, 78/78 fit; the fallback only fires where the catalog leaves a good with no fitting system). - **Gems are no longer special.** The eight gemstones are still in the catalog (and can be a world's signature, earning half-price origin + the demand bonus), but there is no more "one per class, always on every shelf" rule. Why: one product per world makes each place *legible* — you can name a system by what it makes ("the fuel world"), the buy menu is trivially small, and the only economic question a player has to answer is **where does it go?** --- ## 5. Pricing — fixed per location, distance dominant **Prices are FIXED per (good, location) pair — a pure function of the seed.** They do not depend on where you got the good or what's in your hold. The shops are stateless; the whole economy re-derives from `(seed, placeId, economyVersion)`. The only state is the day's remaining batch (see §5.3). For a good `g` at a system `S` (class `C`) there are TWO fixed prices: ### 5.1 The BUY price (what you pay) ``` origin sale (S is g's ORIGIN) = baseValue × (1 − originDiscount) × jitter hub resale (hub-pool good, S is a hub) = baseValue × hubPriceFactor × jitter everywhere else = NOT ON SALE (buyPrice = 0) ``` - **originDiscount** = 50% — the origin sells at ~half base. **The origin is the galaxy's only cheap source of its good.** - **hubPriceFactor** = 1.0 — hubs resell at standard (never below the origin — there is exactly one cheap place). - A world's floor is *only* its signature (its one product); a hub's floor is the hub-pool selection **plus** its own system's signature (same day's batch, same source — a station in the fuel system resells fuel at the origin price). ### 5.2 The SELL price (what you get paid) ``` sell(g, C, S) = baseValue(g) × distanceBand(hops(S → origin(g))) ← THE dominant term × (1 + demandBonus if C wants g) ← the secondary modifier × jitter , CAPPED at the shop's own BUY price for g ← the anti-loop rule ``` - **distanceBands** (banded, not linear — the trip pays in steps): | hops from origin | 0 | 2 | 4 | 7 | 11+ | |---|---|---|---|---|---| | factor | ×0.75 | ×1.15 | ×1.85 | ×2.75 | ×3.75 | | profit/unit (≈) | 0 → 0.25× | 0.65× | 1.35× | 2.25× | 3.25× | Profit is in base-value multiples (≈ (factor − 0.5) × base, before the +10% demand bonus and jitter); 0 hops nets ~0 — the anti-loop cap. HALVED-MARGIN curve (f′ = 0.25 + f/2 on the original ×1.0 / ×1.8 / ×3.2 / ×5.0 / ×7.0): every band's per-unit profit is half the original, distance stays the dominant term. The jump tree's diameter (~17 hops) is the natural ceiling. No cap: the band with the largest `minHops ≤ hops` wins. - **demandBonus** = +10% when the class wants the good (`g.demand` contains `C`) — planet type's entire price role. - **jitter** = ±10% seeded per (good, place) — no two shops feel formulated. - **THE ANTI-LOOP RULE** — a shop's sell price for a good is **capped at its own buy price for that good**. No shop may ever be a positive same-shop spread. This is the v1 exploit's tombstone (buy→sell→buyback at one shop used to print infinite credits); `dev/economy.test.mjs` sweeps every system × class and asserts **zero** positive same-shop spreads. ### 5.3 The daily batch (the economy's one stateful seam) Each origin world's signature good comes to market as a **daily batch of 6 units** (`economy.json → stock`): - `dayMs = 3 600 000` (an hour of wall-clock), `epoch = 2026-01-01T00:00:00Z` — the batch regenerates while the player is away. - **Buy side only** is batch-limited. The SELL side is unlimited (the market is a sink — selling never "uses up" anything). - This bounds the same-system loop: in the origin system you can buy at most 6 units a day at half price and flip them to the same system's station — a *bounded pocket change*, not infinite money. Long-haul selling stays unlimited, so the incentive clearly favors the trip. - **Save state** is tiny and save-clone-safe: `economyStock = { [systemId]: { day, left } }` — one integer pair per origin the player has drawn from (`js/economy/Stock.js` is a pure module; `spend()` returns a NEW map, `GameScene.spendStock` swaps the reference; `SaveData.js` captures/restores/cleans it). ### 5.4 Credits are the go-between — no barter Buying spends credits; selling earns them; profit = sell − buy. All fixed per location; none changes based on your inventory. **Concrete example (real numbers, test seed):** `Cryo Serum` (base 66) is Keth's signature (an ice world — thematic fit). At Keth it sells for `66 × 0.5 ≈ 31` (6 units a day). A rocky world 2 hops out, which wants cryo, pays `66 × 1.15 × 1.1 ≈ 84` → **≈ 53/unit profit on a 2-hop run.** Seven hops out: `66 × 2.75 × 1.1 ≈ 200` → **≈ 169/unit** — same hold-space, three times the money, because it was carried further. The same-system flip (Keth world → Keth station) nets ~0 per unit (the anti-loop cap) and is limited to 6 units a day — pocket change by design. --- ## 6. Shop floors (v2) | Shop | Floor | |---|---| | **A world** | EXACTLY its signature good — one row, origin price, the "N left" batch strip. | | **A station** | a seeded 10–16 from the hub pool (standard price) + its own system's signature (origin price, batch-limited). | | **A capital** | a seeded 12–16 from the hub pool (standard price) + its own system's signature. Its SELL list is the **entire catalog** (the universal sink). | Every world's BUYBACK list is its class's wanted goods (the `demand` rule) — a place that wants fuel buys back fuel; a capital buys back everything. The scout panel (tap a buy row — `js/ui/shop/ScoutPanel.js` + `TradeScout.js`) reports the good's **MADE IN** (its origin system, or "HUB RESALE"), its **SOUGHT ON** classes, and the top-3 charted exits ranked by sell price — with tap-to-**SET DESTINATION** on each exit. That is the player's strategy tool: the economy is a landscape, and this is the map of it. --- ## 7. How it hangs off the existing galaxy | Existing system | Economy uses it | |---|---| | **Jump network / hops** (`Galaxy.js`, `data/galaxy.json`) | the distance bands in pricing — hops ≈ map distance. Longer path ⇒ higher sell price. | | **Faction territories** (`data/factions.json`) | `capital` class = faction seats (universal premium buyers). Faction tariff/patron hooks (`economy.json → faction`). | | **Planet classes** (`data/planets.json`) | the six world classes — origin-fit (assignment) + demand bonus (pricing). | | **Settlements** (`data/settlements.json`, every planet settled) | each settled system is a shop; the origin assignment's candidate pool. | | **Stations** (`data/stations.json`) | the `station` class (hub-pool resellers, wider floors). | | **Zones** (`data/galaxy.json → zones`) | the origin assignment's tie-break (zone → tier fit). | | **Reputation** (`js/reputation/Reputation.js`) | standing with a place's owner faction nudges its prices (hook, `economy.json → reputation`). | | **SHOP button** (surface deck, `data/actionbar.json`) | the existing surface `shop` action opens the buy/sell UI. | | **cargoHold** (`data/ship.json`) | your carrying capacity for goods. | | **Credits** (builds, quest rewards already reference `credits`) | the currency. HUD next to minerals. | Determinism: all of this is a **pure function of the seed** (lazy and eager generation agree; saves stay tiny — we store only your credits, your carried goods, and the batch remainders, never the shops). --- ## 8. The credits currency (decided) - **Displayed next to the minerals HUD** (upper-right). Minerals are *bulk matter*; credits are *trade value*. - **You start at 0** (`economy.json → credits.startingCredits`). - **Earning credits** = selling goods + mission/quest credit rewards + the bounded local batch loop. **Spending** = builds, upgrades, and buying goods. - **The cargo bays are the first credit sink** — the hold expands 4 → 8 → 12 → 16 through three ship builds paid in credits + minerals (`data/builds.json → cargo-bay-8/12/16`: 500 cr + 200 min / 800 cr + 350 min / 1200 cr + 500 min). - **Trading is LOCKED until the third story quest.** Until then the economy exists in the world (shops, prices, batches) but the buy/sell UI is gated. --- ## 9. Interact, don't control (your requirement) What the player **does**: pick goods, buy at the origin, travel, sell far, spend. Nothing else. What the player **cannot do**: change prices, stock a shop, corner a market, or make a good appear that isn't made. The only "control" surface that exists is *light* and comes from the existing systems: reputation slightly moves a place's prices, and (later) factions may patron/tax specific goods. That's flavor, not management. The daily batch is the one place the economy has a memory — and it only ever *refills* in the player's favor. --- ## 10. Files (v2) | File | What it is | |---|---| | `data/goods.json` | **114 goods** across 9 categories, each with producer/demand/weight/tier/value/flavor. The catalog (unchanged by v2 — the assignment works on it). | | `data/economy.json` | All the tuning knobs (model v2: originDiscount, hubPriceFactor, demandBonus, distanceBands, jitter, the daily batch, cargo, credits). `version: 2`. | | `js/economy/Economy.js` | The pure price/stock engine: `originOf`/`signatureOf`/`isHubPool`, `buyPrice`/`sellPrice`/`shopFor`, `tradePreview` (with the stock-cap backstop), and the `buildEconomy` generation pass (classBySystem + the strict signatures/origins/hubPool assignment + producerHops). | | `js/economy/Stock.js` | **NEW (v2)** — the time-gated daily batch: `dailyBatch`, `economyDay`, `batchRemaining`, `spend` (pure, reference-swap; state = `{ [systemId]: { day, left } }`). | | `js/economy/TradeScout.js` | **The strategy layer** (pure): `scoutGood()` — the good's origin (MADE IN), demand, and the top-N charted exits ranked by sell price. | | `js/economy/CargoHold.js` | The trade-goods hold — a pure, framework-free class. One good per bay. | | `js/ui/shop/TradeGrid.js` | The shop's SELL side: a 4×4 grid of the player's cargo bays. | | `js/ui/shop/BuyList.js` | The shop's BUY side: the floor as wide rows — icon + name + flavor + price + stepper, plus the **"N left" / "SOLD OUT" batch strip** (v2) and the stepper clamp at the remaining batch. | | `js/ui/shop/ScoutPanel.js` | **The goods scout** (v2): the panel that replaces the cargo grid when a buy row is tapped — MADE IN / SOUGHT ON / BEST EXITS, with tap-to-set-destination rows. | | `js/ui/shop/ShopView.js` | The buy/sell (and buyback) screen: the two grids + the scout + a live NET preview + Confirm/Clear. Owns the batch wiring (`stockRemaining`/`stockCapsFor`, `confirm()` spends the batch, `_afterTrade` re-renders the "N left" strip). | | `js/galaxy/Galaxy.js` | Calls `buildEconomy` at the end of generation (the `galaxy.economy` seam). | | `js/entities/Ship.js` | `ship.credits` + the ledger + `ship.cargo` (the `CargoHold`). | | `js/save/SaveData.js` | Credits + cargo + **`economyStock`** (the batch remainders) captured/restored; `cleanEconomyStock` normalizes the shape; a new run clears it. | | `js/scenes/GameScene.js` | Owns `economyStock` (registry-backed), `spendStock()` (the reference swap), the credits HUD, quest payouts. | | `js/scenes/SurfaceScene.js` | Opens the `ShopView` over the shop clip; the SET DESTINATION offer from a scout exit. | | `data/builds.json` | The **cargo bays** (`cargo-bay-8/12/16`) — the first credit sink (credits + minerals). | | `dev/economy.test.mjs` | The engine's invariants: the strict 1:1 assignment, hub-pool completeness, determinism, origin-only sales, the loop, the distance bands, the shop floors, and the **anti-loop sweep** (zero positive same-shop spreads, every system × class). | | `dev/cargo.test.mjs` | The hold's rules + the `tradePreview` caps (credits/cargo/**stock**, sell→buy in one confirm, the buyback exemption from the batch). | | `dev/shop-ui.test.mjs` | The shop screen's LOGIC headlessly: sell, buy, the **daily batch** ("N left" → clamp → SOLD OUT → engine backstop), scroll/clip, the two caps, the buyback, the scout panel + set-destination. | | `dev/trade-scout.test.mjs` | The scout report's invariants (origin field, exits ranked, determinism). | | `dev/saves.test.mjs` | The save bank + the `economyStock` round-trip (capture/restore/reset/legacy/malformed). | --- ## 11. Decisions (locked) ### v2 decisions (this pass) 1. **ONE WORLD, ONE GOOD** — every tradeable good has exactly one origin system; every settled system sells exactly one product (its signature). Strict seeded 1:1; barren (jump-gate-only) systems host nothing. 2. **Distance is the dominant profit factor** — banded hops from the origin (×0.75 / ×1.15 / ×1.85 / ×2.75 / ×3.75 — the halved-margin curve, f′ = 0.25 + f/2), no cap (the tree's diameter is the ceiling). Planet type contributes at most +10% (demand bonus). 3. **The daily batch bounds the local loop** — 6 units per origin per wall-clock day (1h); sell side unlimited. Same-system trading is bounded pocket change, never infinite money. 4. **The anti-loop cap** — sell price ≤ own buy price at the same shop; no shop may ever be a positive same-shop spread (the v1 exploit, dead). 5. **The catalog is reused as-is** (114 goods) — the 36 remainder form the hub pool; no new goods, no per-class gems. ### v1 decisions (still in force) 1. **Initial credits** — trading gated until the third story quest; first credits from quest rewards + (later) asteroid gemstone mining. 2. **Cargo** — fixed bays, one good each; 4 unlocked → 16-slot grid. 3. **Reputation & faction prices** — deferred (hooks in `economy.json`). 4. **Zones** — hop-distance only (no per-zone price swing). 5. **Station vs. capital** — stations = wide variety (resellers); capitals = the universal buyer. 6. **Credits are the go-between** — no barter. --- ## 12. The shop screen (player-facing UI) Where the loop is actually played. You open it from the surface deck's **SHOP** button; the screen sits **over** the shop clip. One component — `js/ui/shop/ShopView.js` — composing a `TradeGrid` (the SELL side) + a `BuyList` (the BUY side) + the `ScoutPanel` (the goods scout). **Entry state (showcase the clip).** First open shows **only the tab sub-bar** (`[ BUY/SELL | BUYBACK ] │ [ CLEAR | CONFIRM ] │ NET 0 cr`) above the command deck. Clicking **Buy/Sell** reveals the trade panel: the **SELL grid** (your cargo bays, tap to check off) and the **BUY list** side by side, spanning the screen. **BUY list (v2).** A world's list is ONE wide row — its signature good: icon, name, one-line flavor, the **origin price**, a **"N left" strip** (the day's remaining batch — the stepper can't exceed it; 0 reads **SOLD OUT** in magenta), and the −/qty/+ stepper. A hub's list is its 10–16 hub-pool rows (unlimited — no strip) + its own signature (with the strip). Tapping a row's body (not the stepper) opens the **TRADE SCOUT**: MADE IN (the origin system, or HUB RESALE), SOUGHT ON (the demand classes), and the **BEST EXITS** ranked by sell price — tapping an exit row offers to **SET DESTINATION** to that system. Tapping anywhere else closes the scout. **NET readout** — the live credit delta on the sub-bar (green up, red spend, dim at zero), the same `preview()` the Confirm button is gated on. **SELL → BUY is atomic** — one Confirm executes credits `+ sale − buy`, cargo `− sold + bought`. **BUYBACK** (second tab) lists what you sold this visit, buyable back at the sale price — a *neutral* undo (sell→buyback nets zero; it never draws on the batch). **Determinism + the one stateful seam.** Prices and floors are **fixed** per (good, location) — pure functions of the seed; reopening a shop re-derives the same floor and prices. The **only** state is the origin's remaining daily batch (`economyStock` — save/restore in `SaveData.js`), and it only ever *regenerates* while you're away. So the economy stays "interact, don't control": you trade against a fixed landscape, and the only clock in it runs in your favor. **Tested headlessly** (`dev/shop-ui.test.mjs`, `dev/economy.test.mjs`, `dev/cargo.test.mjs`): the full logic — sell, buy, the daily batch (clamp → SOLD OUT → engine backstop), the two caps, the buyback neutral-undo, the scout + set-destination, the anti-loop sweep, and the save round-trip of the batch. Pixel layout and the slide-in animation are verified in the browser.