orbit/docs/ECONOMY.md

28 KiB
Raw Blame History

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 — the cyberpunk MenuButton pair [ BUY/SELL | BUYBACK ] (Buyback grayed out), extended at its right end by the live NET readout (dim 0 cr until you start picking), 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 panels slide in).

# first click — shop video + the tab sub-bar (rests above the deck):
┌─────────────────────────────────────────────────────────────────┐
│                          (clip plays)                            │
│             ┌──────────────────────────────────┐                │
│             │ BUY/SELL │ BUYBACK │ NET  0 cr    │  ← sub-bar    │
│             └──────────────────────────────────┘                │
├─────────────────────────────────────────────────────────────────┤
│  SHOP │ BUILD │ SHIP │ QUESTS │ TAKE OFF │ MENU   ← command deck│
└─────────────────────────────────────────────────────────────────┘

# after clicking Buy/Sell — the trade panel reveals (middle of the screen):
┌─────────────────────────────────────────────────────────────────┐
│  ┌SELL 4×4┐     [ CONFIRM ]    ┌──────────────────────────────┐ │
│  │ bays   │     [  CLEAR  ]    │[i] Fresh Produce   24 cr − 0 +│ │
│  │(cargo) │                    │    Crisp, clean, alive…       │ │
│  └────────┘                    │[i] Refined Fuel     41 cr − 1 +│ │
│                                │    The lifeblood of the fleet │ │
│                                │      … (scrolls) …            │ │
│                                └──────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│             ┌──────────────────────────────────┐                │
│             │ BUY/SELL │ BUYBACK │ NET +420 cr  │  ← sub-bar    │
│             └──────────────────────────────────┘                │
├─────────────────────────────────────────────────────────────────┤
│  SHOP │ BUILD │ SHIP │ QUESTS │ TAKE OFF │ MENU   ← command deck│
└─────────────────────────────────────────────────────────────────┘
  • Tab sub-bar — the Buy/Sell + Buyback buttons are MenuButtons (the deck's cyberpunk style: cut-corner panel, neon edge, hover sweep, press punch) in a 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 a hairline divider to carry the live NET readout — one glance sees the tabs and the pending trade's credit delta.

  • 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, in the center column between the panels; 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) — 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 one-line description (the good's flavor), 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, right of the tabs past the divider. 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).