orbit/docs/ECONOMY.md

403 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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).