diff --git a/apps/web/src/screens/Locations.tsx b/apps/web/src/screens/Locations.tsx index c6fdf63..7be1707 100644 --- a/apps/web/src/screens/Locations.tsx +++ b/apps/web/src/screens/Locations.tsx @@ -103,11 +103,11 @@ export function Locations() {
- Produces {location.produces.map(cargoName).join(", ") || "nothing"} · demands{" "} - {location.demands.map(cargoName).join(", ") || "nothing"} + Produces {location.cargoClassMix.produces.map(cargoName).join(", ") || "nothing"} · demands{" "} + {location.cargoClassMix.demands.map(cargoName).join(", ") || "nothing"}
{hasBay ? "Your HQ is here." : "No HQ here."}
diff --git a/memory-bank/progress.md b/memory-bank/progress.md
index 47f30e3..bbdbb04 100644
--- a/memory-bank/progress.md
+++ b/memory-bank/progress.md
@@ -202,3 +202,4 @@ Spec: `memory-bank/change-requests/CR-006-boards-that-fit.md`. New decisions rec
## Phase 10G — Cargo Goods, Priority Contracts and Shippers (CR-007)
Spec: `memory-bank/change-requests/CR-007-cargo-goods.md` (supplied as "CR-005 / Phase 10E"; renumbered, see its implementation notes). New decisions recorded as plan §0.5.54–0.5.59. Baseline class mix before the change (1,000 Earth boards, all discovered): Standard 76.79 %, High-Value 23.21 %.
+- [x] **Step 10G.1 — Content: goods catalog.** `goodSchema` (id, name, `cargoClassId`, `massTonnes {min,max}`, `payMultiplier`, optional `tier` 1–5, `priorityEligible`, optional `priorityName`, `producedAt`, `demandedAt`; min ≤ max refinement) and `data/goods.ts` with all 29 goods from CR-007 §3, plus `goodsProducedAt`/`goodsDemandedAt`. Locations: `produces`/`demands` now derive from the catalog (good ids); the old class lists are kept as `cargoClassMix` (decision 0.5.57) and the sim's class selection and the UI's produces/demands now read them, so nothing visible changes yet; `shippers` added (≥ 1 each). Config: `priorityChance` 0.15, `priorityPayoutMultiplier` 1.5, `priorityReferenceShipModelId` "freighter", `priorityWindowFactor` 1. Bundle: `goods`, `good(id)`. Tests: `goods.test.ts` (schema, references, tiers by class, Standard good everywhere, ≥ 2 Standard goods each way Earth↔Moon, location lists in sync, Priority config), schema tests for Good and the new fields.
diff --git a/packages/content/src/bundle.ts b/packages/content/src/bundle.ts
index 570a521..13a6b68 100644
--- a/packages/content/src/bundle.ts
+++ b/packages/content/src/bundle.ts
@@ -1,5 +1,6 @@
import { cargoTypes, skills } from "./data/cargo";
import { gameConfig } from "./data/config";
+import { goods } from "./data/goods";
import { locations } from "./data/locations";
import { onboardingGoals, scriptedTutorialContracts } from "./data/onboarding";
import { shipModels } from "./data/ships";
@@ -17,6 +18,7 @@ export const content = {
/** Ship models players can buy (excludes company ships such as the loaner). */
purchasableShipModels: shipModels.filter((s) => s.purchasable),
cargoTypes,
+ goods,
skills,
config: gameConfig,
scriptedTutorialContracts,
@@ -24,6 +26,7 @@ export const content = {
location: byId(locations),
shipModel: byId(shipModels),
cargoType: byId(cargoTypes),
+ good: byId(goods),
skill: byId(skills),
onboardingGoal: byId(onboardingGoals),
};
diff --git a/packages/content/src/data/config.ts b/packages/content/src/data/config.ts
index 9e617eb..4f97239 100644
--- a/packages/content/src/data/config.ts
+++ b/packages/content/src/data/config.ts
@@ -32,6 +32,12 @@ export const gameConfig: GameConfig = {
restGameHours: 10,
firstContactChance: 0.1,
firstContactPayoutMultiplier: 2,
+ // Priority contracts (CR-007): 15% of eligible goods, ×1.5 pay, a window as
+ // long as a Freighter's trip.
+ priorityChance: 0.15,
+ priorityPayoutMultiplier: 1.5,
+ priorityReferenceShipModelId: "freighter",
+ priorityWindowFactor: 1,
// One band per ship size, so every board has work for every ship (playtest fix):
// up to a Courier's 20 t, a Freighter's 100 t, and a Bulk Hauler's 400 t.
contractMassBands: [
diff --git a/packages/content/src/data/goods.ts b/packages/content/src/data/goods.ts
new file mode 100644
index 0000000..76a66ec
--- /dev/null
+++ b/packages/content/src/data/goods.ts
@@ -0,0 +1,338 @@
+import type { Good } from "../schemas";
+
+const EARTH = "earth";
+const MOON = "moon";
+const MARS = "mars";
+const CERES = "ceres-station";
+const JUPITER = "jupiter-cloud-platform";
+
+/**
+ * The goods catalog (CR-007, decision 0.5.54). Pay multipliers scale the
+ * class's base pay per tonne per million km. High-Value and Dangerous goods
+ * carry their own tier (0.5.55); Standard and Heavy goods don't.
+ */
+export const goods: Good[] = [
+ // Standard: no skill.
+ {
+ id: "mail",
+ name: "Mail & Parcels",
+ cargoClassId: "standard",
+ massTonnes: { min: 1, max: 8 },
+ payMultiplier: 1.2,
+ priorityEligible: true,
+ producedAt: [EARTH, MARS],
+ demandedAt: [MOON, MARS, CERES, JUPITER, EARTH],
+ },
+ {
+ id: "packaged-food",
+ name: "Packaged Food",
+ cargoClassId: "standard",
+ massTonnes: { min: 10, max: 60 },
+ payMultiplier: 1,
+ priorityEligible: false,
+ producedAt: [EARTH],
+ demandedAt: [MOON, MARS, CERES, JUPITER],
+ },
+ {
+ id: "consumer-goods",
+ name: "Consumer Goods",
+ cargoClassId: "standard",
+ massTonnes: { min: 5, max: 40 },
+ payMultiplier: 1,
+ priorityEligible: false,
+ producedAt: [EARTH],
+ demandedAt: [MOON, MARS, CERES, JUPITER],
+ },
+ {
+ id: "optical-glass",
+ name: "Lunar Optical Glass",
+ cargoClassId: "standard",
+ massTonnes: { min: 4, max: 25 },
+ payMultiplier: 1.1,
+ priorityEligible: false,
+ producedAt: [MOON],
+ demandedAt: [EARTH, MARS],
+ },
+ {
+ id: "moon-souvenirs",
+ name: "Moon Dust Souvenirs",
+ cargoClassId: "standard",
+ massTonnes: { min: 1, max: 6 },
+ payMultiplier: 0.9,
+ priorityEligible: false,
+ producedAt: [MOON],
+ demandedAt: [EARTH],
+ },
+ {
+ id: "hydroponic-produce",
+ name: "Hydroponic Produce",
+ cargoClassId: "standard",
+ massTonnes: { min: 5, max: 30 },
+ payMultiplier: 1.1,
+ priorityEligible: true,
+ producedAt: [MARS],
+ demandedAt: [MOON, CERES, JUPITER],
+ },
+ {
+ id: "salvaged-components",
+ name: "Salvaged Components",
+ cargoClassId: "standard",
+ massTonnes: { min: 5, max: 30 },
+ payMultiplier: 0.9,
+ priorityEligible: false,
+ producedAt: [CERES],
+ demandedAt: [MOON, MARS],
+ },
+ {
+ id: "bioplastics",
+ name: "Atmospheric Bioplastics",
+ cargoClassId: "standard",
+ massTonnes: { min: 5, max: 40 },
+ payMultiplier: 1,
+ priorityEligible: false,
+ producedAt: [JUPITER],
+ demandedAt: [EARTH, MARS, CERES],
+ },
+
+ // High-Value: Expensive Cargo skill; tier from the good.
+ {
+ id: "medical-supplies",
+ name: "Medical Supplies",
+ cargoClassId: "high-value",
+ massTonnes: { min: 1, max: 10 },
+ payMultiplier: 1,
+ tier: 1,
+ priorityEligible: true,
+ producedAt: [EARTH],
+ demandedAt: [MOON, MARS, CERES, JUPITER],
+ },
+ {
+ id: "electronics",
+ name: "Electronics",
+ cargoClassId: "high-value",
+ massTonnes: { min: 2, max: 15 },
+ payMultiplier: 1,
+ tier: 1,
+ priorityEligible: false,
+ producedAt: [EARTH],
+ demandedAt: [MOON, MARS, CERES, JUPITER],
+ },
+ {
+ id: "seed-stock",
+ name: "Seed Stock & Embryos",
+ cargoClassId: "high-value",
+ massTonnes: { min: 0.5, max: 4 },
+ payMultiplier: 1.3,
+ tier: 2,
+ priorityEligible: true,
+ producedAt: [EARTH],
+ demandedAt: [MOON, MARS, CERES],
+ },
+ {
+ id: "low-g-pharma",
+ name: "Low-G Pharmaceuticals",
+ cargoClassId: "high-value",
+ massTonnes: { min: 0.5, max: 5 },
+ payMultiplier: 1.4,
+ tier: 2,
+ priorityEligible: true,
+ producedAt: [MOON],
+ demandedAt: [EARTH, MARS],
+ },
+ {
+ id: "research-samples",
+ name: "Research Samples",
+ cargoClassId: "high-value",
+ massTonnes: { min: 0.2, max: 3 },
+ payMultiplier: 1.5,
+ tier: 2,
+ priorityEligible: true,
+ producedAt: [MOON, MARS, CERES],
+ demandedAt: [EARTH],
+ },
+ {
+ id: "rare-earths",
+ name: "Martian Rare Earths",
+ cargoClassId: "high-value",
+ massTonnes: { min: 5, max: 30 },
+ payMultiplier: 1.4,
+ tier: 3,
+ priorityEligible: false,
+ producedAt: [MARS],
+ demandedAt: [EARTH],
+ },
+ {
+ id: "platinum",
+ name: "Platinum Ingots",
+ cargoClassId: "high-value",
+ massTonnes: { min: 2, max: 20 },
+ payMultiplier: 1.8,
+ tier: 4,
+ priorityEligible: false,
+ producedAt: [CERES],
+ demandedAt: [EARTH],
+ },
+
+ // Heavy: Heavy Cargo skill; tier follows the generator's Heavy rule.
+ {
+ id: "machine-parts",
+ name: "Machine Parts",
+ cargoClassId: "heavy",
+ massTonnes: { min: 20, max: 90 },
+ payMultiplier: 1,
+ priorityEligible: true,
+ priorityName: "Emergency Parts",
+ producedAt: [EARTH, MARS],
+ demandedAt: [MOON, MARS, CERES, JUPITER],
+ },
+ {
+ id: "habitat-prefabs",
+ name: "Habitat Prefabs",
+ cargoClassId: "heavy",
+ massTonnes: { min: 40, max: 180 },
+ payMultiplier: 1,
+ priorityEligible: false,
+ producedAt: [EARTH],
+ demandedAt: [MOON, MARS, CERES],
+ },
+ {
+ id: "lunar-titanium",
+ name: "Lunar Titanium",
+ cargoClassId: "heavy",
+ massTonnes: { min: 30, max: 120 },
+ payMultiplier: 1.1,
+ priorityEligible: false,
+ producedAt: [MOON],
+ demandedAt: [EARTH, MARS],
+ },
+ {
+ id: "iron-ingots",
+ name: "Iron Ingots",
+ cargoClassId: "heavy",
+ massTonnes: { min: 50, max: 200 },
+ payMultiplier: 0.9,
+ priorityEligible: false,
+ producedAt: [MARS],
+ demandedAt: [MOON, CERES],
+ },
+ {
+ id: "nickel-iron-ore",
+ name: "Nickel-Iron Ore",
+ cargoClassId: "heavy",
+ massTonnes: { min: 80, max: 300 },
+ payMultiplier: 0.8,
+ priorityEligible: false,
+ producedAt: [CERES],
+ demandedAt: [EARTH, MARS, MOON],
+ },
+ {
+ id: "water-ice",
+ name: "Water Ice",
+ cargoClassId: "heavy",
+ massTonnes: { min: 60, max: 250 },
+ payMultiplier: 0.8,
+ priorityEligible: false,
+ producedAt: [CERES],
+ demandedAt: [MARS, MOON, JUPITER],
+ },
+
+ // Dangerous: Dangerous Cargo skill; tier is the hazard class.
+ {
+ id: "industrial-chemicals",
+ name: "Industrial Chemicals",
+ cargoClassId: "dangerous",
+ massTonnes: { min: 5, max: 40 },
+ payMultiplier: 1,
+ tier: 1,
+ priorityEligible: false,
+ producedAt: [EARTH],
+ demandedAt: [MOON, MARS, CERES, JUPITER],
+ },
+ {
+ id: "liquid-oxygen",
+ name: "Liquid Oxygen",
+ cargoClassId: "dangerous",
+ massTonnes: { min: 20, max: 100 },
+ payMultiplier: 1,
+ tier: 1,
+ priorityEligible: false,
+ producedAt: [MOON],
+ demandedAt: [MARS, CERES, JUPITER],
+ },
+ {
+ id: "mining-charges",
+ name: "Mining Charges",
+ cargoClassId: "dangerous",
+ massTonnes: { min: 2, max: 20 },
+ payMultiplier: 1.3,
+ tier: 2,
+ priorityEligible: false,
+ producedAt: [EARTH, MARS],
+ demandedAt: [MOON, CERES],
+ },
+ {
+ id: "methane-fuel",
+ name: "Methane Fuel",
+ cargoClassId: "dangerous",
+ massTonnes: { min: 20, max: 80 },
+ payMultiplier: 1.1,
+ tier: 2,
+ priorityEligible: false,
+ producedAt: [MARS],
+ demandedAt: [MOON, CERES],
+ },
+ {
+ id: "liquid-hydrogen",
+ name: "Liquid Hydrogen",
+ cargoClassId: "dangerous",
+ massTonnes: { min: 30, max: 150 },
+ payMultiplier: 1.1,
+ tier: 2,
+ priorityEligible: false,
+ producedAt: [JUPITER],
+ demandedAt: [MARS, CERES, EARTH],
+ },
+ {
+ id: "helium-3",
+ name: "Helium-3 Canisters",
+ cargoClassId: "dangerous",
+ massTonnes: { min: 1, max: 10 },
+ payMultiplier: 2,
+ tier: 3,
+ priorityEligible: true,
+ producedAt: [JUPITER],
+ demandedAt: [EARTH, MOON],
+ },
+ {
+ id: "reactor-cores",
+ name: "Fusion Reactor Cores",
+ cargoClassId: "dangerous",
+ massTonnes: { min: 10, max: 40 },
+ payMultiplier: 1.8,
+ tier: 4,
+ priorityEligible: false,
+ producedAt: [EARTH],
+ demandedAt: [CERES, JUPITER],
+ },
+ {
+ id: "antimatter-traps",
+ name: "Antimatter Containment Traps",
+ cargoClassId: "dangerous",
+ massTonnes: { min: 0.1, max: 1 },
+ payMultiplier: 4,
+ tier: 5,
+ priorityEligible: false,
+ producedAt: [JUPITER],
+ demandedAt: [EARTH],
+ },
+];
+
+/** Good ids produced at a location, in catalog order (a location's `produces`). */
+export function goodsProducedAt(locationId: string): string[] {
+ return goods.filter((g) => g.producedAt.includes(locationId)).map((g) => g.id);
+}
+
+/** Good ids demanded at a location, in catalog order (a location's `demands`). */
+export function goodsDemandedAt(locationId: string): string[] {
+ return goods.filter((g) => g.demandedAt.includes(locationId)).map((g) => g.id);
+}
diff --git a/packages/content/src/data/locations.ts b/packages/content/src/data/locations.ts
index d6dbc15..5751df9 100644
--- a/packages/content/src/data/locations.ts
+++ b/packages/content/src/data/locations.ts
@@ -1,9 +1,12 @@
import type { Location } from "../schemas";
+import { goodsDemandedAt, goodsProducedAt } from "./goods";
/**
* Base-game destinations (GDD §9). Cargo flows: Earth and Mars make goods, the
* Moon and Ceres mine ore (heavy), Jupiter refines fuel (dangerous); remote
- * outposts import high-value equipment.
+ * outposts import high-value equipment. `produces`/`demands` come from the
+ * goods catalog (CR-007); `cargoClassMix` keeps the class-level flows that set
+ * each board's class mix (decision 0.5.57).
*/
export const locations: Location[] = [
{
@@ -16,8 +19,15 @@ export const locations: Location[] = [
launchCostMultiplier: 3,
startsDiscovered: true,
bayLeasePrice: 1000,
- produces: ["standard", "high-value"],
- demands: ["heavy", "dangerous"],
+ produces: goodsProducedAt("earth"),
+ demands: goodsDemandedAt("earth"),
+ cargoClassMix: { produces: ["standard", "high-value"], demands: ["heavy", "dangerous"] },
+ shippers: [
+ "Pacific Orbital Logistics",
+ "Meridian Health",
+ "Atlas Heavy Industries",
+ "Hollis & Crane Consumer",
+ ],
},
{
id: "moon",
@@ -29,8 +39,13 @@ export const locations: Location[] = [
launchCostMultiplier: 0.5,
startsDiscovered: true,
bayLeasePrice: 800,
- produces: ["standard", "heavy"],
- demands: ["standard", "high-value", "dangerous"],
+ produces: goodsProducedAt("moon"),
+ demands: goodsDemandedAt("moon"),
+ cargoClassMix: {
+ produces: ["standard", "heavy"],
+ demands: ["standard", "high-value", "dangerous"],
+ },
+ shippers: ["Shackleton Mining Co.", "Tranquility Labs", "Copernicus Glassworks"],
},
{
id: "mars",
@@ -42,8 +57,10 @@ export const locations: Location[] = [
launchCostMultiplier: 1.5,
startsDiscovered: false,
bayLeasePrice: 20000,
- produces: ["standard", "high-value"],
- demands: ["heavy", "dangerous"],
+ produces: goodsProducedAt("mars"),
+ demands: goodsDemandedAt("mars"),
+ cargoClassMix: { produces: ["standard", "high-value"], demands: ["heavy", "dangerous"] },
+ shippers: ["Jezero Agricultural Co-op", "Red Line Metals", "Tharsis Chemical"],
},
{
id: "ceres-station",
@@ -55,8 +72,10 @@ export const locations: Location[] = [
launchCostMultiplier: 0.3,
startsDiscovered: false,
bayLeasePrice: 40000,
- produces: ["heavy"],
- demands: ["standard", "high-value", "dangerous"],
+ produces: goodsProducedAt("ceres-station"),
+ demands: goodsDemandedAt("ceres-station"),
+ cargoClassMix: { produces: ["heavy"], demands: ["standard", "high-value", "dangerous"] },
+ shippers: ["Belt Ore Consolidated", "Piazzi Station Authority"],
},
{
id: "jupiter-cloud-platform",
@@ -69,7 +88,9 @@ export const locations: Location[] = [
launchCostMultiplier: 2,
startsDiscovered: false,
bayLeasePrice: 80000,
- produces: ["dangerous"],
- demands: ["standard", "high-value", "heavy"],
+ produces: goodsProducedAt("jupiter-cloud-platform"),
+ demands: goodsDemandedAt("jupiter-cloud-platform"),
+ cargoClassMix: { produces: ["dangerous"], demands: ["standard", "high-value", "heavy"] },
+ shippers: ["Galilean Fuel Combine", "Zephyr Gas Works"],
},
];
diff --git a/packages/content/src/index.ts b/packages/content/src/index.ts
index 1cad3bc..7762b72 100644
--- a/packages/content/src/index.ts
+++ b/packages/content/src/index.ts
@@ -2,6 +2,7 @@ export * from "./schemas";
export * from "./data/locations";
export * from "./data/ships";
export * from "./data/cargo";
+export * from "./data/goods";
export * from "./data/config";
export * from "./data/onboarding";
export * from "./bundle";
diff --git a/packages/content/src/schemas.ts b/packages/content/src/schemas.ts
index a689ed0..a74ea80 100644
--- a/packages/content/src/schemas.ts
+++ b/packages/content/src/schemas.ts
@@ -41,10 +41,17 @@ export const locationSchema = z.object({
startsDiscovered: z.boolean(),
/** Credits. */
bayLeasePrice: nonNegativeInt,
- /** Cargo type ids. */
+ /** Good ids made here (CR-007), derived from the goods catalog. */
produces: z.array(idSchema),
- /** Cargo type ids. */
+ /** Good ids wanted here (CR-007), derived from the goods catalog. */
demands: z.array(idSchema),
+ /**
+ * Cargo class ids that steer a board's class mix (decision 0.5.57): a
+ * contract's class is one produced here and demanded at its destination.
+ */
+ cargoClassMix: z.object({ produces: z.array(idSchema), demands: z.array(idSchema) }),
+ /** Flavor company names for contracts from here (decision 0.5.58). */
+ shippers: z.array(z.string().min(1)).min(1),
/** Map art asset id (CR-003), e.g. `body-earth`; see the web app's art manifest. */
sprite: idSchema,
/** Optional map badge asset id drawn on the body's edge (CR-003). */
@@ -90,6 +97,30 @@ export const cargoTypeSchema = z.object({
});
export type CargoType = z.infer