29 KiB
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
- 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).
- 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.
- 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.
- 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.
- Local money is possible without crossing the galaxy. You never have to cross the map to make a living; you choose to, for more.
- 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 whereCproduces the good (its home region), standard where a hub resells it. - SELL price (what
ppays you): a premium whereCwants the good, growing with the hops betweenSand 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
producerrule). - 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 (BuildWindowaffordability covers both minerals + credits;GameScene.beginBuilddebitsship.spendCreditsfor 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
creditsfield —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)
- Initial credits — trading is gated until the third story quest; first credits come from
mission/quest rewards (the
reward.creditsfield already exists indata/quests.json) and, later, asteroid gemstone mining (mining-laser upgrades yield gems you sell). - 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. - Reputation & faction prices — deferred (the hooks stay in
economy.json; wired later). - Zones / regional bands — hop-distance only (no extra per-zone price swing).
- Station vs. capital — stations = wide variety (resellers), capitals = wide premium (the universal buyer). Locked.
- Gems — purely tradeable this pass (the asteroid mining faucet is later).
- 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
MenuButtons (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
MenuButtonstyle, 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
cargoHoldare 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'sflavor, 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).