# Orbit — Trade & Economy (design) This is the design for the game's trade/economy. It is written to be built on top of what Orbit already has — the jump network, factions, planet classes, settlements, the SHOP button that already exists on the surface deck, and the `ship.stats.cargoHold` slot that's already reserved for cargo. Nothing here requires new world-structure; it all hangs off the existing deterministic galaxy. **Status: DRAFT / design + data only.** The goods catalog and tuning knobs are written to `data/` so they can be iterated on. The JS plumbing (HUD, credits, shop logic, generator hook) is intentionally **not** written yet — the open questions at the bottom of this doc need your call first. --- ## 1. Design goals 1. **Trade is a thing the player does, not a thing the player manages.** No market simulation, no stock you can manipulate, no persistent shop state. The economy is a *landscape* you travel through and exploit; it doesn't react to you (or only very lightly, via reputation). 2. **Every world has a market.** Every settled planet and station gets a shop of 8–16 goods, priced so that what's local is cheap and what's foreign is dear. 3. **The trip is the reward.** A good's profit scales with how far it is from where it's made. Local trade (buy low in-system, sell nearby) works and is safe; long-haul trade pays much more and costs jumps/time/risk. Longer journey ⇒ meaningfully better reward. 4. **Planet type drives production.** A terran world makes food; a lava world makes heat and rare metals; a gas giant makes fuel. Opposing types want each other's signature goods — ice wants heat, lava wants cool, terran wants salt/solar, arid wants water/food, gas wants metal, rocky wants fuel. 5. **Local money is possible without crossing the galaxy.** You never *have* to cross the map to make a living; you choose to, for more. 6. **Each planet type yields a gemstone that other types want** (a universal premium good, and a later mining/loot hook). --- ## 2. The core loop ``` LAND on a world SHIP a good LAND on another world ┌──────────────┐ (jump) ┌──────────────┐ │ open SHOP │ ◄────────────────────────── │ SELL for │ │ BUY cheap │ │ CREDITS │ │ (local/low) │ ──────────────────────────► │ (dear/hi) │ └──────────────┘ carry in cargoHold └──────────────┘ ▲ │ └──────────────── spend credits on builds/quests ◄──────┘ ``` - **BUY** where a good is cheap (it's produced there, or it's local). - **CARRY** it in your cargo hold (`ship.stats.cargoHold`). - **SELL** where it's dear (it's wanted there, or it's far from its source). - **PROFIT** = sell price − buy price. The gap is bigger the further the good has travelled. - **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. That is the "interact, don't control" requirement made concrete. --- ## 3. The eight economic classes `data/goods.json` defines them; they map straight onto existing structures: | Class | Where it exists | Naturally PRODUCES (cheap to buy) | Strongly WANTS (dear to sell) | |---|---|---|---| | **terran** | `terran` planets | food, water, timber, pharma, biology, gems: **Verdant Emerald** | rare metals, fuel, heat goods, high-end tech | | **arid** | `arid` planets | salt, silica, solar, spice, gems: **Dune Topaz** | water, humidity, coolants, food | | **rocky** | `rocky` planets | ores, metal, stone, gems: **Karnite** | fuel, food, water, processed tech | | **gas** | `gas` planets | fuel, gases, isotopes, gems: **Jovian Sapphire** | solid metal, food, water, hull parts | | **ice** | `ice` planets | water, ice, cryo, volatiles, gems: **Frost Diamond** | heat, energy, food, fuel | | **lava** | `lava` planets | heat, rare metals, volcanic glass, gems: **Magma Ruby** | coolants, water, food, fuel | | **station** | deep-space stations | processed/refined goods, services, gems: **Aether Crystal** | raw ore, food, water, bulk fuel | | **capital** | a faction's seat | luxury, finance, high-end tech, gems: **Imperial Diamond** | everything, at a premium (the universal buyer) | This is the supply/demand web. It's deliberately symmetric on the opposition pairs you asked for: - **ice ⇄ lava** — ice sells cool (coolants, cryo, water) to lava; lava sells heat (thermal batteries, plasma, volcanic energy) to ice. - **terran ⇄ arid** — terran sells water/food to arid; arid sells salt/solar/spice to terran. - **gas ⇄ rocky** — gas sells fuel to rocky; rocky sells metal to gas. The `capital` class is the great sink: it buys almost everything at a premium, which is what makes a run to a faction seat lucrative and gives long-haul a ceiling. --- ## 4. The gemstone system One signature gem per class, category `gemstone`. They are: - **Universal demand** — every gem is wanted by *other* classes (see each entry's `demand`), so there is always a premium outlet for a world's signature stone. - **Higher value band** (tier 3–4, baseValue 105–300) than most commodity goods — they are the "jewel" trade that makes even a short trip worthwhile. - **A future mining/loot hook** — right now they are tradeable goods; later they can also drop from mining/loot, as you noted. The catalog already treats them as goods, so a drop source just adds a second way to acquire the same id. The eight gems: Verdant Emerald (terran), Dune Topaz (arid), Karnite (rocky), Jovian Sapphire (gas), Frost Diamond (ice), Magma Ruby (lava), Aether Crystal (station), Imperial Diamond (capital — the measure by which the rest are weighed). --- ## 5. Pricing — fixed per location, distance baked in by regionality **The prices are FIXED per (good, location) pair — decided once at generation, and they do NOT depend on where you got the good or what's in your hold.** A good's sell price at a planet is the same whether you mined it two systems away or carried it across the galaxy. This is what makes the shops stateless (no per-player stock bookkeeping) and the whole economy a pure function of the seed. For a good `g` at a place `p` (class `C`, system `S`) there are TWO fixed prices, both in credits: - **BUY price** (what you pay `p`): cheap where `C` produces the good (its home region), standard where a hub resells it. - **SELL price** (what `p` pays you): a premium where `C` wants the good, growing with the hops between `S` and the good's home region. That hop distance is the "trip is the reward" term. ``` price(g,p) = baseValue(g) × (1 + demandBonus if p.class is in g.demand) ← it's wanted here × (1 − producerDiscount if p.class is in g.producer)← it's made here (cheaper) × (1 + distancePerHop × min(hops, cap)) ← how far from its source × jitter ``` - `baseValue` — the good's intrinsic worth (goods.json). - **demandBonus** (+12%/class in the draft) — a place that *wants* the good pays more. - **producerDiscount** (−35% in the draft) — a place that *makes* the good sells it cheap. **This is the "local = cheap" rule.** - **distance** (+22%/hop, capped at 6 hops) — the good is worth more the further it is from the nearest class that produces it. **This is the "trip is the reward" rule.** It uses the jump graph (hops), which PROJECT_NOTES already establishes as ≈ map distance, so it's consistent with how the galaxy is generated. - **jitter** (±10% seeded) — per-good, per-place wobble so no two shops are identical and no number feels "formulated." - **Regional availability is how distance is enforced.** A good is only offered for sale (cheap) where its class produces it — its HOME region. You must be in that region to buy it cheap; a far region doesn't carry it locally, so the only way to get it there is to carry it. That regional lock — not a live per-trade hop-multiplier — is the distance mechanism. - **Credits are the go-between — there is no barter.** Buying is one credit transaction (you spend); selling is a separate one (you earn). Your profit is simply `SELL price (where you sell) − BUY price (where you bought)`. **Concrete example (draft numbers):** `Refined Fuel` (base 14, gas producer) is on sale on a gas world for `14 × 0.65 ≈ 9`. A rocky world 2 hops out, which wants fuel, buys it back for `14 × 1.12 × 1.44 ≈ 22`. Profit ≈ **13 credits** on a 2-hop run. Same good, 5 hops out: `14 × 1.12 × 2.10 ≈ 33` → profit ≈ **24** — same good, same hold-space, more than double, because it was carried further. Every one of those numbers is fixed for its location; none changes based on your inventory. --- ## 6. Shop stock rules (8–16 goods) Each settlement gets a shop with a stock size from `economy.json → shop.stockByKind` (colonies/mining 8–12, stations/capitals 10–16). The stock is **seeded**, not random-at-open, so it's stable per place. The mix follows the class: - **Locally produced goods** appear in *higher* stock and *lower* price (the `producer` rule). - **Goods the class wants** appear at a *premium* or are *absent* — a place that needs fuel often won't sell fuel cheap; it sells it dear or not at all, so your imported fuel is what they're buying. - **Signature gem** of the local class is always present (the world's "jewel" is always on the shelf) — guarantees a premium good in every shop and a guaranteed local buy. - **Stations/capitals** carry wider, more exotic stock (they're resellers/finance), so they list more tier-3/4 and tech/luxury goods than a mining station does. The exact pick is `seed → (place class, distance profile) → weighted sample of 8–16 goods`, weighted by the good's `weight` field and its relevance to the class. It's a pure function, so it's deterministic and re-derivable (see §8). --- ## 7. How it hangs off the existing galaxy | Existing system | Economy uses it | |---|---| | **Jump network / hops** (`Galaxy.js`, `data/galaxy.json`) | the distance term in pricing — hops ≈ map distance, per PROJECT_NOTES. 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 + their producer/demand roles. | | **Settlements** (`data/settlements.json`, every planet settled) | each settlement = a shop. `settledKindByClass` already tells us the shop's "personality." | | **Stations** (`data/stations.json`) | the `station` class (refiner/reseller, wider stock). | | **Zones** (`data/galaxy.json → zones`) | optional regional price bands — far zones (Far Reach / Outer Verge) run wilder prices. | | **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 becomes 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**. Shop stock and prices are derived from `(seed, placeId, economyVersion)` exactly like system contents are derived today (`Rng.derive(seed, 'economy', …)`). That means lazy and eager generation agree, saves stay tiny (we store only *your* credits + your carried goods, never the shops), and the economy can never drift or corrupt. --- ## 8. The credits currency (decided) - **Displayed next to the minerals HUD** (upper-right). Minerals are *bulk matter*; credits are *trade value*. They coexist because they're different resources: you mine minerals, you earn credits. - **You start at 0** (`economy.json → credits.startingCredits`). - **Earning credits** = selling goods + mission/quest credit rewards. **Spending** = builds, upgrades, and buying goods. - **The cargo bays are the first credit sink** — the hold expands 4 → 8 → 12 → 16 through three credit-costed ship builds (`data/builds.json → cargo-bay-8/12/16`, `targets: ["ship"]`, `cost: { credits: 300/1200/3000 }`, `effects.cargo.bays`). Built from any planet's console and applied to the ship (MAX wins, never a downgrade). The build console was extended to pay out credits (`BuildWindow` affordability covers both minerals + credits; `GameScene.beginBuild` debits `ship.spendCredits` for credit lines) — so trading income now has somewhere to go before the next sink exists. - **Trading is LOCKED until the third story quest** (not written yet). Until then the economy exists in the world (shops, prices, gems) but the player's buy/sell UI is gated. Initial credits come from **mission/quest rewards** (the quest reward object already has a `credits` field — `data/quests.json` → `reward.credits`, surfaced in `_questRewardLabel`) and, later, from **mining-laser upgrades that periodically yield gemstones from asteroids** — the gems are already tradeable goods, so selling them is the credit faucet. (Decision #1.) --- ## 9. Interact, don't control (your requirement) What the player **does**: pick goods, buy low, travel, sell high, spend. Nothing else. What the player **cannot do**: change prices, stock a shop, corner a market, set a price, or make a good appear that isn't produced. The economy is a fixed landscape generated from the seed. The only "control" surface that exists is *light* and comes from your existing systems: reputation slightly moves a place's prices, and (later) factions may patron/tax specific goods. That's flavor, not management. --- ## 10. Files | File | What it is | |---|---| | `data/goods.json` | **114 goods** across 9 categories, each with producer/demand/weight/tier/value/flavor. The catalog. | | `data/economy.json` | All the tuning knobs (shop size, price multipliers, distance curve, cargo, credits start, trading lock, reputation/faction hooks). | | `data/creditshud.json` | The CREDITS readout styling (upper-right, stacked above the minerals HUD). | | `js/economy/Economy.js` | The pure price/stock engine (`buyPrice`/`sellPrice`/`shopFor`) + the `buildEconomy` generation pass (classBySystem + producerHops). | | `js/ui/CreditsHud.js` | The CREDITS line, upper-right, above the minerals HUD (count-up on change). | | `js/galaxy/Galaxy.js` | Calls `buildEconomy` at the end of generation (the `galaxy.economy` seam). | | `js/entities/Ship.js` | `ship.credits` + `canAfford`/`addCredits`/`spendCredits`/`setCredits` (the ledger) and `ship.cargo` (the `CargoHold`) + `maxCargoSlots`/`setCargoHold`/`cargoRoom`. | | `js/economy/CargoHold.js` | The trade-goods hold — a **pure, framework-free** class (capacity, items, add/remove/count/has/clear/set/snapshot/save-clone). One good per bay. | | `js/ui/shop/TradeGrid.js` | The shop's SELL side: a 4×4 grid of the player's cargo bays (tap to check off, hover tooltip with name + price). | | `js/ui/shop/BuyList.js` | The shop's BUY side: the stock as a scrollable list of WIDE ROWS (icon + name + one-line description + price + −/qty/+ stepper; drag/wheel to scroll). | | `js/ui/shop/ShopView.js` | The buy/sell (and buyback) screen: the two grids + a live NET preview + Confirm/Clear, a Buy/Sell ↔ Buyback tab bar, the session-local `soldThisSession` buyback, and the transaction apply. | | `data/shopui.json` | The shop screen's layout/animation knobs (tabs, panels, grid geometry, slot colors, stepper, preview, buttons) — presentation only; the economy is `Economy.js`. | | `js/galaxy/Galaxy.js` | Calls `buildEconomy` at the end of generation (the `galaxy.economy` seam). | | `js/save/SaveData.js` | Credits + cargo captured/restored with the ship record. | | `js/scenes/GameScene.js` | Builds the credits HUD (minerals HUD shifted below it), refreshes it on credit changes, pays quest `reward.credits` on claim, restores cargo. | | `js/scenes/SurfaceScene.js` | Opens the `ShopView` over the shop clip on the deck's SHOP button (resolving this world's economic class) and tears it down on LEAVE SHOP. | | `data/builds.json` | The **cargo bays** (`cargo-bay-8/12/16`) — the first credit sink. Credit-costed ship builds that expand the hold (4 → 8 → 12 → 16), built from any planet's console, MAX wins. | | `js/ui/BuildWindow.js` | The build console. Its affordability wallet now covers **both** minerals + credits (`_wallet()`), so the credit-costed cargo bays are buyable. | | `js/scenes/GameScene.js` (build) | `beginBuild` debits credit lines via `ship.spendCredits` (mineral lines via `ship.setMinerals`); `_applyCargoUpgrade` expands the hold on completion (MAX wins). | | `dev/economy.test.mjs` | The engine's invariants: determinism, fixed prices, the loop, distance, shop floor, hubs. | | `dev/cargo.test.mjs` | The hold's rules (capacity, dups, drop-sheds, save/clone) + the `tradePreview` caps (credits/cargo, sell→buy in one confirm). | `dev/shop-ui.test.mjs` | The shop screen's LOGIC headlessly: construct, sell, buy, the two caps, the session-local buyback (a neutral undo), the lock, and Clear. | `docs/ECONOMY.md` | This document. | Implemented this pass: the **price/stock engine**, the **generation pass**, **credits state + save + HUD**, **quest-credit payout**, the **cargo hold** (`CargoHold` + `ship.cargo` + save/restore), and the full **shop buy/sell/buyback UI** (`TradeGrid` + `ShopView`, wired into the SurfaceScene over the existing shop clip). Trading is locked until the third story quest. --- ## 11. Decisions (locked in this pass) 1. **Initial credits** — trading is gated until the third story quest; first credits come from **mission/quest rewards** (the `reward.credits` field already exists in `data/quests.json`) and, later, **asteroid gemstone mining** (mining-laser upgrades yield gems you sell). 2. **Cargo weight** — the hold is a set of fixed **bays, one good each** (`unitPerGood = 1`). The player starts with **4 unlocked bays** (`cargoHold = 4`); cargo upgrades raise it toward the **16-slot grid** the shop UI always shows (`cargo.maxSlots = 16`). Locked bays are just capacity the ship hasn't bought yet. 3. **Reputation & faction prices** — **deferred** (the hooks stay in `economy.json`; wired later). 4. **Zones / regional bands** — **hop-distance only** (no extra per-zone price swing). 5. **Station vs. capital** — stations = wide *variety* (resellers), capitals = wide *premium* (the universal buyer). Locked. 6. **Gems** — **purely tradeable this pass** (the asteroid mining faucet is later). 7. **Catalog** — the 114-goods list is **good for the first pass**. **Model confirmations (the two "before you code" points):** - Prices are **fixed per (good, location)** — independent of where you got the good or your hold. Distance is enforced by **regional availability** (goods only cheap in their home region), not a live per-trade hop-multiplier. - **Credits are the go-between** — buy and sell are two separate credit transactions; there is no good-for-good 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 existing `data/actionbar.json` slot); the shop clip is already live, and the screen sits **over** it. It's one component — `js/ui/shop/ShopView.js` — composing a `TradeGrid` (the SELL side) + a `BuyList` (the BUY side). **Entry state (showcase the clip).** When you first open the shop, **only the tab sub-bar** is up — one console-blue bar in the deck's cyberpunk **MenuButton** style (`[ BUY/SELL | BUYBACK ] │ [ CLEAR | CONFIRM ] │ NET 0 cr` — the tabs, the trade actions, and the live **NET** readout, past hairline dividers; Buyback grayed, the actions hidden until you start picking, the readout dim `0 cr`), the same style + placement as the deck's **Menu** sub-bar (`js/ui/MenuSubBar.js`). It rests **above the command deck**, right-aligned to the deck's **Shop** button — so it reads as the same family of UI as the Menu drawer. The screen stays open on the shop video so the clip plays. Clicking **Buy/Sell** reveals the trade panel in the middle of the screen: the **SELL grid** and the **BUY list** side by side with just a bit of padding between (the panels slide in). The panel **spans the screen** — the SELL grid's left edge is **flush with the planet name** (the HUD's left margin), and the BUY list stretches to the **same padding** in from the right edge. ``` # first click — shop video + the tab sub-bar (rests above the deck): ┌─────────────────────────────────────────────────────────────────┐ │ (clip plays) │ │ ┌────────────────────────────────────────────────┐ │ │ │ BUY/SELL │ BUYBACK │ CLEAR CONFIRM │ NET 0 cr │ │ │ └────────────────────────────────────────────────┘ │ ├─────────────────────────────────────────────────────────────────┤ │ SHOP │ BUILD │ SHIP │ QUESTS │ TAKE OFF │ MENU ← command deck│ └─────────────────────────────────────────────────────────────────┘ # after clicking Buy/Sell — the trade panel reveals (middle of the screen): │ SELL and BUY sit NEXT TO each other (a small gap between); │ the trade actions + NET readout ride on the sub-bar below. ┌─────────────────────────────────────────────────────────────────┐ │ ┌─────────────────┐ ┌───────────────────────────────────┐ │ │ │ SELL (4×4 bays) │ │[i] Fresh Produce 24 cr − 0 +│ │ │ │ your cargo │ │ Crisp, clean, alive. A small mir… │ │ │ │ (tap = sell) │ │[i] Refined Fuel 41 cr − 1 +│ │ │ │ │ │ The lifeblood of the fleet. │ │ │ └─────────────────┘ │ … (scrolls) … │ │ │ └───────────────────────────────────┘ │ ├─────────────────────────────────────────────────────────────────┤ │ ┌────────────────────────────────────────────────┐ │ │ │ BUY/SELL │ BUYBACK │ CLEAR CONFIRM │ NET +420 cr │ │ │ └────────────────────────────────────────────────┘ │ ├─────────────────────────────────────────────────────────────────┤ │ SHOP │ BUILD │ SHIP │ QUESTS │ TAKE OFF │ MENU ← command deck│ └─────────────────────────────────────────────────────────────────┘ ``` - **Tab sub-bar** — the **Buy/Sell** + **Buyback** buttons, the **Confirm/Clear** trade actions, and the live **NET** readout — all `MenuButton`s (the deck's cyberpunk style: cut-corner panel, neon edge, hover sweep, press punch) in one console-blue panel that rests above the command deck, anchored to the Shop button — the exact recipe of the Menu sub-bar. Buyback is **disabled** (grayed) until you've sold something this visit. The bar is **extended** past hairline dividers to carry the actions *and* the **NET** readout — one glance sees the tabs, the pending trade's credit delta, and the button that executes it. - **Deck lock (while the shop is up)** — the command deck's other slots (Build, Ship, Quests, Take Off, Menu) go **standby**: dim, no hover, no press — the same inert language as the comm-hold pause (`ActionBar.setSlotEnabled`). **LEAVE SHOP is the only live button on the deck**; the press that opened the shop is the only way out. They re-arm when you leave. (`ActionBar.setSlotEnabled` + `SurfaceScene.setDeckLocked`; the deck-wide comm hold still owns input if it fires mid-shop.) - **Confirm / Clear** — the same `MenuButton` style, on the **tab sub-bar** between the tabs and the NET readout (they serve BOTH the trade and the buyback tabs, and persist while you switch between them); Confirm is **disabled** (grayed) while the trade is invalid (can't go negative, hold would overflow), and both appear once you start picking. - **SELL (left)** — your **cargo bays**. 16 cells (the fixed grid); the first `cargoHold` are live bays (filled with a good, or empty), the rest are **locked** (the hold's upgrade room). Tap a filled bay to **check it off to sell** (a bay = 1 unit). No stepper — the quantity is simply the number of its bays that are checked. - **BUY (right, next to the SELL grid)** — the shop's **stock** (12–16 distinct goods from `Economy.shopFor`) as a **scrollable list of WIDE ROWS**: each row is the good's **icon**, its **name** + a **description** (the good's `flavor`, a smaller font that **wraps to up to two lines** and never overlaps the price/stepper), and the **price** + a **−/qty/+ stepper** on the right. The stock is longer than the panel, so the list **scrolls** (drag on the list body or the mouse wheel) — rows outside the window are clipped; the selection is data, so stepping a good works whether its row is on screen or not. Buy as many as you like (capped by credits + hold room). - **NET readout (tab sub-bar)** — the live credit delta, at the bar's right end past the second divider (right of the tabs and the Confirm/Clear actions). Green when you're up (a net sale), red when you'd spend, dim at zero. Updates as you check bays / step goods (it's the same `preview()` the Confirm button is gated on). - **SELL → BUY is atomic** — check bays to sell *and* step goods to buy, then one Confirm executes it: credits `+ sale − buy`, cargo `− sold + bought`, in that order (free room first). Selling to free a bay for a buy lands in the same Confirm. - **BUYBACK (second tab, session-local only)** — after you SELL something, the goods you sold are listed here and can be bought **back** — at the **sale price** (a *neutral* undo, so sell → buyback nets **zero** and can't loop into free credits). It's **locked** (dim, no-op) until you've sold something, and it **disappears when you leave the shop** (no persistence — `data/shopui.json`). Buyback is clamped to what you actually sold. **Determinism + statelessness preserved.** The screen reads **fixed** per-(good, location) prices (`Economy.buyPrice`/`sellPrice` — the seed + place, never where you got a good or what's in your hold). There are **no per-player sold/bought counters**; reopening a shop re-derives the same stock from `(seed, placeId, economyVersion)`. The only session state is the buyback list (explicitly reset on leave). So the economy stays "interact, don't control": you trade against fixed regional prices, never against your own history. **Tested headlessly** (`dev/shop-ui.test.mjs`): construct the SELL grid + BUY list, sell a bay and earn the sale price, step a stock good and pay the buy price, the two caps (credits / cargo) blocking Confirm, the sell→buy-in-one-confirm, the buyback lock + neutral undo, and Clear. Pixel layout and the slide-in animation are verified in the browser (the stub doesn't render).