From 15eaf26c03cc2cf11d82cca879394d95197652ac Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Mon, 14 Sep 2026 19:27:55 -0600 Subject: [PATCH] Change cargo bay builds to cost credits + minerals - Rebalance the three cargo hold upgrades (8/12/16) from a credits-only sink to a mixed cost of credits and minerals, so expanding the hold draws on both trade income and the mining economy. - Update research descriptions, build data, tests, docs, and code comments to reflect the new two-resource costs. --- data/builds.json | 15 +++++++++------ data/research/cargo.json | 6 +++--- dev/builds.test.mjs | 18 +++++++++++------- docs/ECONOMY.md | 6 ++++-- js/scenes/GameScene.js | 6 +++--- js/ui/BuildWindow.js | 4 ++-- 6 files changed, 32 insertions(+), 23 deletions(-) diff --git a/data/builds.json b/data/builds.json index 09e533a..3a8f062 100644 --- a/data/builds.json +++ b/data/builds.json @@ -222,12 +222,13 @@ }, "cargo-bay-8": { "label": "Cargo Bay \u2014 8", - "description": "Expands the ship's cargo hold from 4 to 8 bays \u2014 roughly 2\u00d7 the goods per run, and the trade income to match. A ship upgrade: built from any planet's console, paid in credits, counted wherever the ship lands (never re-bought).", + "description": "Expands the ship's cargo hold from 4 to 8 bays \u2014 roughly 2\u00d7 the goods per run, and the trade income to match. A ship upgrade: built from any planet's console (500 credits + 200 minerals, 20 s), counted wherever the ship lands (never re-bought).", "icon": "storage", "category": "cargo", "targets": ["ship"], "cost": { - "credits": 300 + "credits": 500, + "minerals": 200 }, "duration": 20, "requires": ["cargo/cargo_bay_8"], @@ -243,12 +244,13 @@ }, "cargo-bay-12": { "label": "Cargo Bay \u2014 12", - "description": "Expands the ship's cargo hold to 12 bays \u2014 three quarters of a full hold in a single run. Supersedes the 8-bay bay (the biggest hold always wins). A ship upgrade, paid in credits.", + "description": "Expands the ship's cargo hold to 12 bays \u2014 three quarters of a full hold in a single run. Supersedes the 8-bay bay (the biggest hold always wins). A ship upgrade, paid in credits + minerals (800 credits + 350 minerals, 30 s).", "icon": "storage", "category": "cargo", "targets": ["ship"], "cost": { - "credits": 1200 + "credits": 800, + "minerals": 350 }, "duration": 30, "requires": ["cargo/cargo_bay_12"], @@ -264,12 +266,13 @@ }, "cargo-bay-16": { "label": "Cargo Bay \u2014 16", - "description": "The full 16-bay cargo hold \u2014 a freighter's worth of goods in one run, the biggest the ship can carry. Supersedes every smaller bay. A ship upgrade, paid in credits.", + "description": "The full 16-bay cargo hold \u2014 a freighter's worth of goods in one run, the biggest the ship can carry. Supersedes every smaller bay. A ship upgrade, paid in credits + minerals (1200 credits + 500 minerals, 40 s).", "icon": "storage", "category": "cargo", "targets": ["ship"], "cost": { - "credits": 3000 + "credits": 1200, + "minerals": 500 }, "duration": 40, "requires": ["cargo/cargo_bay_16"], diff --git a/data/research/cargo.json b/data/research/cargo.json index 470085d..4f75acb 100644 --- a/data/research/cargo.json +++ b/data/research/cargo.json @@ -4,7 +4,7 @@ "nodes": { "cargo_bay_8": { "label": "Cargo - 8 Bays", - "description": "The blueprint for the ship's first trade upgrade: the 8-bay hold. Once researched, the 8-bay bay can be built from any planet's console (300 credits, 20 s) — roughly 2× the goods per run, and the trade income to match.", + "description": "The blueprint for the ship's first trade upgrade: the 8-bay hold. Once researched, the 8-bay bay can be built from any planet's console (500 credits + 200 minerals, 20 s) — roughly 2× the goods per run, and the trade income to match.", "icon": "storage", "duration": 60, "requires": [], @@ -14,7 +14,7 @@ }, "cargo_bay_12": { "label": "Cargo - 12 Bays", - "description": "The blueprint for the 12-bay hold. Once researched, the 12-bay bay can be built (1200 credits, 30 s) — three quarters of a full hold in a single run, supersedes the 8-bay bay.", + "description": "The blueprint for the 12-bay hold. Once researched, the 12-bay bay can be built (800 credits + 350 minerals, 30 s) — three quarters of a full hold in a single run, supersedes the 8-bay bay.", "icon": "storage", "duration": 60, "requires": ["cargo_bay_8"], @@ -24,7 +24,7 @@ }, "cargo_bay_16": { "label": "Cargo - 16 Bays", - "description": "The blueprint for the full 16-bay hold — a freighter's worth of goods in one run, the biggest the ship can carry. Once researched, the 16-bay bay can be built (3000 credits, 40 s), superseding every smaller bay.", + "description": "The blueprint for the full 16-bay hold — a freighter's worth of goods in one run, the biggest the ship can carry. Once researched, the 16-bay bay can be built (1200 credits + 500 minerals, 40 s), superseding every smaller bay.", "icon": "storage", "duration": 60, "requires": ["cargo_bay_12"], diff --git a/dev/builds.test.mjs b/dev/builds.test.mjs index 473a0c8..49a4469 100644 --- a/dev/builds.test.mjs +++ b/dev/builds.test.mjs @@ -79,13 +79,17 @@ const miningCat = loadBuilds('mining'); check('model: loadBuilds("planet") holds the three tether builds', !!planet.builds['tether-l1'] && !!planet.builds['tether-l2'] && !!planet.builds['tether-l3']); check('model: loadBuilds("planet") keeps registry entry (label/accent)', planet.label === 'Planet' && typeof planet.accent === 'string'); check('model: loadBuilds("cargo") holds the three ship-hold bays (8/12/16)', !!cargo.builds['cargo-bay-8'] && !!cargo.builds['cargo-bay-12'] && !!cargo.builds['cargo-bay-16']); -// The cargo bays are a CREDIT sink (the trade income buys the hold) — so the -// affordability wallet must cover credits, not just minerals. -check('model: cargo bays are credit-costed (the first credit sink)', cargo.builds['cargo-bay-8'].cost.credits === 300 && cargo.builds['cargo-bay-12'].cost.credits === 1200 && cargo.builds['cargo-bay-16'].cost.credits === 3000); -check('model: cargo bays cost CREDITS (not minerals)', costLines(cargo.builds['cargo-bay-8']).every((l) => l.res === 'credits')); -check('model: a credits wallet affords the 8-bay (300 cr) build', canAfford(cargo.builds['cargo-bay-8'], { minerals: 0, credits: 300 })); -check('model: a credits wallet BELOW the cost does not afford it', !canAfford(cargo.builds['cargo-bay-8'], { minerals: 100000, credits: 299 })); -check('model: a minerals-only wallet (old economy) does NOT afford a credit build', !canAfford(cargo.builds['cargo-bay-8'], 100000)); +// The cargo bays cost BOTH resources — trade income (credits) + the mining +// hold (minerals) — so the affordability wallet must cover both lines. +check('model: cargo bays cost credits + minerals (8: 500 cr / 200 min, 12: 800 cr / 350 min, 16: 1200 cr / 500 min)', + cargo.builds['cargo-bay-8'].cost.credits === 500 && cargo.builds['cargo-bay-8'].cost.minerals === 200 && + cargo.builds['cargo-bay-12'].cost.credits === 800 && cargo.builds['cargo-bay-12'].cost.minerals === 350 && + cargo.builds['cargo-bay-16'].cost.credits === 1200 && cargo.builds['cargo-bay-16'].cost.minerals === 500); +check('model: cargo bays cost BOTH lines (credits AND minerals)', costLines(cargo.builds['cargo-bay-8']).some((l) => l.res === 'credits') && costLines(cargo.builds['cargo-bay-8']).some((l) => l.res === 'minerals')); +check('model: a full wallet affords the 8-bay build (500 cr + 200 min)', canAfford(cargo.builds['cargo-bay-8'], { minerals: 200, credits: 500 })); +check('model: a credits wallet BELOW the cost does not afford it', !canAfford(cargo.builds['cargo-bay-8'], { minerals: 100000, credits: 499 })); +check('model: a minerals wallet BELOW the cost does not afford it', !canAfford(cargo.builds['cargo-bay-8'], { minerals: 199, credits: 100000 })); +check('model: a minerals-only wallet (old economy) does NOT afford a credits+minerals build', !canAfford(cargo.builds['cargo-bay-8'], 100000)); check('model: loadBuilds("mining") holds the five ship-upgrade builds', !!miningCat.builds['mining-arm-improved'] && !!miningCat.builds['mining-arm-advanced'] && !!miningCat.builds['mining-arm-premium'] && !!miningCat.builds['mining-storage-improved'] && !!miningCat.builds['mining-storage-advanced']); check('model: loadBuilds("mining") keeps registry entry (label/accent)', miningCat.label === 'Mining' && typeof miningCat.accent === 'string'); check('model: an unknown category falls back to the default', loadBuilds('nope').id === builds.defaultCategory); diff --git a/docs/ECONOMY.md b/docs/ECONOMY.md index ffd272d..2226797 100644 --- a/docs/ECONOMY.md +++ b/docs/ECONOMY.md @@ -267,7 +267,9 @@ goods, and the batch remainders, never the shops). - **Earning credits** = selling goods + mission/quest credit rewards + the bounded local batch loop. **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`). + through three ship builds paid in credits + minerals + (`data/builds.json → cargo-bay-8/12/16`: 500 cr + 200 min / 800 cr + 350 min / + 1200 cr + 500 min). - **Trading is LOCKED until the third story quest.** Until then the economy exists in the world (shops, prices, batches) but the buy/sell UI is gated. @@ -306,7 +308,7 @@ memory — and it only ever *refills* in the player's favor. | `js/save/SaveData.js` | Credits + cargo + **`economyStock`** (the batch remainders) captured/restored; `cleanEconomyStock` normalizes the shape; a new run clears it. | | `js/scenes/GameScene.js` | Owns `economyStock` (registry-backed), `spendStock()` (the reference swap), the credits HUD, quest payouts. | | `js/scenes/SurfaceScene.js` | Opens the `ShopView` over the shop clip; the SET DESTINATION offer from a scout exit. | -| `data/builds.json` | The **cargo bays** (`cargo-bay-8/12/16`) — the first credit sink. | +| `data/builds.json` | The **cargo bays** (`cargo-bay-8/12/16`) — the first credit sink (credits + minerals). | | `dev/economy.test.mjs` | The engine's invariants: the strict 1:1 assignment, hub-pool completeness, determinism, origin-only sales, the loop, the distance bands, the shop floors, and the **anti-loop sweep** (zero positive same-shop spreads, every system × class). | | `dev/cargo.test.mjs` | The hold's rules + the `tradePreview` caps (credits/cargo/**stock**, sell→buy in one confirm, the buyback exemption from the batch). | | `dev/shop-ui.test.mjs` | The shop screen's LOGIC headlessly: sell, buy, the **daily batch** ("N left" → clamp → SOLD OUT → engine backstop), scroll/clip, the two caps, the buyback, the scout panel + set-destination. | diff --git a/js/scenes/GameScene.js b/js/scenes/GameScene.js index 241b248..8603bc4 100644 --- a/js/scenes/GameScene.js +++ b/js/scenes/GameScene.js @@ -5175,9 +5175,9 @@ export class GameScene extends Phaser.Scene { } // cost — the full amount, paid up front. The cost is a { res: amount } // map (data/builds.json → cost): the planet builds pay MINERALS, the - // cargo bays pay CREDITS (the trade income buys the hold expansion). - // Each line is checked + debited from its own wallet (ship.minerals / - // ship.credits). + // cargo bays pay CREDITS + MINERALS (trade income + the mining hold buy + // the expansion). Each line is checked + debited from its own wallet + // (ship.minerals / ship.credits). const cost = def.cost ?? null; if (cost && typeof cost === 'object') { const mineralsCost = Number(cost.minerals ?? 0); diff --git a/js/ui/BuildWindow.js b/js/ui/BuildWindow.js index 3d458ba..285c17b 100644 --- a/js/ui/BuildWindow.js +++ b/js/ui/BuildWindow.js @@ -167,8 +167,8 @@ export class BuildWindow extends Phaser.GameObjects.Container { this.credits = typeof o.credits === 'function' ? o.credits : () => 0; // Affordability wallet: the build's cost is a { res: amount } map // (data/builds.json → cost) — the planet builds pay MINERALS, the cargo - // bays pay CREDITS (the trade income). canAfford() checks every line - // against this wallet, so both currencies are covered. + // bays pay CREDITS + MINERALS. canAfford() checks every line against + // this wallet, so both currencies are covered. this._wallet = () => ({ minerals: this.minerals(), credits: this.credits() }); this.tetherLevel = typeof o.tetherLevel === 'function' ? o.tetherLevel : () => 0; this.researchUnlocked = typeof o.researchUnlocked === 'function' ? o.researchUnlocked : () => false;