403 lines
29 KiB
Markdown
403 lines
29 KiB
Markdown
# 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).
|