Spaces:
Paused
Paused
|
Download docs/logistics-design.md from Almaatla/SpaceCities: direct link, hf CLI and curl.
- Browser
- Download file 26 kB
-
https://huggingface.co/spaces/Almaatla/SpaceCities/resolve/d19cbc4fab45274de920705d8b5b01da170c7e89/docs/logistics-design.md
- Command line
-
hf download hf://spaces/Almaatla/SpaceCities@d19cbc4fab45274de920705d8b5b01da170c7e89/docs/logistics-design.md
-
curl -L -o logistics-design.md https://huggingface.co/spaces/Almaatla/SpaceCities/resolve/d19cbc4fab45274de920705d8b5b01da170c7e89/docs/logistics-design.md
26 kB
| # Logistics overhaul β brainstorm & phased design | |
| Goal (from the request): make **storage finite** so collection and production | |
| buildings can fill up and stall, and make **workers haul** goods between them, so | |
| logistics stays a live, mindful process that keeps workers relevant to the end of | |
| the game. Plus **energy efficiency by Reactor proximity** with placement cues. | |
| **Status:** the *energy* half is **done and merged** (see below). The *finite-storage | |
| + haulage* half is a core-economy reshape and is designed here for a dedicated, | |
| phased implementation β this doc is the plan of record. | |
| --- | |
| ## 1. What's already shipped β energy grid efficiency | |
| Energy is a per-tick **flow**, not a hauled good, so its "logistics" is a *placement* | |
| decision β no workers needed. Implemented in `engine/industry.js`: | |
| - `POWER_TIERS` β distance-to-nearest-Reactor bands: on-grid Γ1.0 β near Γ1.3 β | |
| far Γ1.7 β isolated Γ2.3 (draw multiplier). | |
| - `powerEfficiency(state, owner, x, y)` β the tier for a spot. | |
| - `powerDraw` scales every consumer (factory, rig, charging Gate) by its tier, so a | |
| remote building bleeds capacity to transmission loss and throttles the whole grid; | |
| clustering around a Reactor lets the same Reactors run more of it. | |
| Visual cues mirror the Spaceport's jump radius (`render.js`): a **selected Reactor** | |
| shows concentric efficiency zones; a **build ghost** for a power consumer shows the | |
| nearest Reactor's zones, a connector, and the tier it would land in ("Far Β· draw | |
| Γ1.7"); a **selected factory/rig** gains a colour-keyed "Grid: β¦ Β· draws ΓN" line | |
| (`hud.js`). Pure distance math β deterministic, DOM-free, no new stored state. | |
| --- | |
| ## 2. Finite storage + haulage β the hard half | |
| ### 2.1 The core tension (why this is a real reshape, not a patch) | |
| Today there is ONE global stockpile per side: `player.resources` (a commodityβfloat | |
| map). Everything reads/writes it: | |
| | System | Uses `player.resources` for | | |
| | --- | --- | | |
| | `gather.js` | workers bank hauls here | | |
| | `industry.js` | factories draw inputs here, bank outputs here | | |
| | `rig.js` | banks dug raws here | | |
| | `market.js` | buy/sell price + settle against it | | |
| | `production.js` | `canAfford` / `payCost` for units, buildings, upgrades | | |
| | `wonder.js` | the Gate's `feed` is drawn from here | | |
| | `ai.js` | every AI economic decision reads it | | |
| | `hud.js` | the resource topbar shows it | | |
| "Finite per-building storage" means goods no longer live in one global pool β they | |
| live in **building buffers** and only become *spendable* once hauled to a Command | |
| Center. That touches every row above. It must be phased, determinism-first, or it | |
| breaks the byte-identical replay law and the AI at once. | |
| ### 2.2 Scope insight β who builds what | |
| The **AI builds none of the industry chain** (no smelter/assembler/chipfab/ | |
| machineworks/reactor/plasmarig/β¦/torpedoworks). So **production-side** storage + | |
| haulage is **player-only and self-contained**. The **collection-side** (Command | |
| Center + Refinery/Foundry/Arsenal drop-offs) IS used by the AI, so finite collection | |
| storage cascades to AI worker logic β the riskier half. | |
| ### 2.3 Forgotten aspects surfaced by the brainstorm | |
| 1. **The AI must not deadlock.** If drop-offs can fill, the AI's gather loop must | |
| reroute or it starves. Simplest safe rule: the AI's Command Center keeps an | |
| effectively huge cap (tier-scaled), so the AI never softlocks while the player | |
| opts into tighter forward logistics. Alternatively, exempt the AI entirely in | |
| phase 1 and only make the *player's* buildings finite. | |
| 2. **What is "spendable"?** Decide up front: are build/unit/upgrade costs and the | |
| market paid from (a) the global pool as today, fed by haulage into the CC, or | |
| (b) summed across all buildings? Recommendation: **(a)** β the CC pool is the | |
| treasury; buffers are in-transit inventory. Keeps `canAfford`/market/AI intact. | |
| 3. **Determinism.** Any "nearest full/needy building" or worker task-assignment must | |
| be order-independent and tie-broken deterministically (by id/hash, never by Map | |
| iteration + wall clock). Reuse `gather.js`'s `nearestDropoff` pattern + `hashStr`. | |
| 4. **Save/load.** New `store` / `input` buffers on buildings must be coerced in | |
| `persist.js cleanEntity` (COM-valid keys, β₯0, clamped to cap) exactly like | |
| `freight` and the rig's `digProgress`. | |
| 5. **Auto vs manual haulage.** Workers should auto-haul (RTS players won't micro | |
| every crate), but the player should be able to *prioritise* a building. Phase 1: | |
| fully automatic, nearest-worker/nearest-target, so it "just works". | |
| 6. **Idle-worker sourcing.** Haulage needs a labour pool. Reuse idle workers (no | |
| gather order); if none, production simply stalls β which is the intended pressure, | |
| not a bug. Surface it ("output full β assign a hauler"). | |
| 7. **Deadlock & starvation UX.** A stalled factory (output full / input starved) | |
| already has a status line; extend it with the *reason* and a one-click "haul now". | |
| 8. **Balance / caps.** Buffer sizes set the rhythm. Too big β no logistics pressure; | |
| too small β constant micro. Start generous (minutes of runtime per buffer) and | |
| tune. CC cap scales with a `storeTier` (Capital upgrade β bigger treasury). | |
| 9. **Market side-effect.** If output must be hauled before it's sellable, a player | |
| with full rig buffers but no haulers can't sell β intended, but must read clearly | |
| in the HUD, not as "the market is broken". | |
| 10. **Freighters.** Interplanetary freight already exists (`unit.freight`); keep it | |
| orthogonal β freighters move goods *between worlds*, workers move goods *within* a | |
| world. Don't conflate. **Superseded by Phase D below**, which deliberately DOES | |
| connect the two: workers can now physically load/unload a landed freighter's hold | |
| (the ship becomes a mobile collection point *within* a world), and a researched | |
| freighter can join the *within-world* haul/service chain itself. The panel-driven | |
| load/unload (this section) and the interplanetary jump (`unit.freight`, moved | |
| *between* worlds) are untouched β Phase D adds a THIRD, physical path onto the same | |
| hold, it doesn't replace the first two. | |
| ### 2.4 Phased plan (each phase is its own merge, suite-green + determinism-verified) | |
| **Phase A β Output buffer + worker-haulage foundation on the Plasma Rig. β DONE.** | |
| - Added `building.store` (commodityβqty) + `def.storeCap`, with pure helpers | |
| `storeTotal`/`storeRoom`/`storeCapOf` (`entities.js`). | |
| - `updatePlasmaRig` banks each dig into `store` (topping off the final dig to exactly | |
| `storeCap`, overflow spills) and **stalls** the moment the buffer is full β an | |
| unlimited *source*, no longer an unlimited *sink*. | |
| - New worker task `haul` (`engine/haul.js`): an idle **player** worker auto-assigns to | |
| the nearest own producer whose buffer is β₯34% full (β€2 haulers each, tie-broken by | |
| id), walks there, loads a cargo, carries it to the nearest drop-off, and banks it | |
| into the treasury **1:1** (no gather multiplier β the goods were already extracted). | |
| A per-tick hauler tally (`countHaulers`) is frozen before assignment for determinism. | |
| - `render.js`: a gold/red output-buffer gauge under a producer's hull. | |
| - `hud.js`: rig panel shows "Output NN/CAP (N%)" + a "buffer full β needs a hauler" | |
| stall reason. | |
| - `persist.js`: coerce `store` (COM-valid, β₯0, clamped to cap); strip the transient | |
| `haulers` tally on serialize (like a unit's `_gi`). | |
| - **Player-only & AI-safe:** the auto-haul call is gated to `owner === "player"`, and | |
| no skirmish building has a buffer, so the AI and the byte-identical skirmish replay | |
| are untouched. Tests: `test/haul.test.js` (+ rewritten `test/rig.test.js`). | |
| - **Why the Rig first:** it's a *leaf* producer of raw goods β its output doesn't feed | |
| another factory, so gating it doesn't half-break the two-hop chain. This proves the | |
| whole haulage subsystem (store, task, auto-assign, render, persist, determinism) on | |
| the safest surface before it touches the coupled factory chain. | |
| **Phase B β Factory output + INPUT buffers = a real supply network. β DONE.** | |
| - Factories now get a default output buffer AND a local `input` larder (both cap 80, | |
| via `storeCapOf`/`inputCapOf` β any building with a `recipe`). `updateProduction` | |
| draws inputs from the **local larder**, banks output to the **local store**, and stalls | |
| when either the larder is empty or the output buffer is full β so a factory only runs | |
| while it's been *supplied* and has *room*. | |
| - Haulage gained a `supply` leg (`engine/haul.js`): an idle player worker picks the | |
| nearest own factory that's low on an input the treasury can provide, carries a cargo | |
| from the treasury (the CC/warehouse) into the factory's larder. Output is drained by | |
| the Phase A haul path, which now covers factories automatically. Idle workers try haul | |
| first (clear backlogs / refill the treasury) then supply (feed the starving). | |
| - The chain is now a real network routed through the treasury: raws β CC β supply β | |
| smelter β metals β haul β CC β supply β assembler β alloys β haul β CC. Verified | |
| end-to-end in the live sim + browser (alloys reach the treasury purely via worker | |
| logistics). | |
| - `persist.js` coerces the `input` buffer and strips the transient `suppliers` tally; | |
| `hud.js` factory panel shows the larder + output buffer and a "needs it carried in" | |
| starve reason; the map store bar (Phase A) now covers factories too. | |
| - Rewrote the `industry.js` + `strategic.js` chain tests to the buffered model; added | |
| supply tests to `test/haul.test.js`. Player-only & AI-safe as in Phase A. | |
| **Balance note (both phases):** default caps are generous (80 / rig 120). Tune later β | |
| smaller = more logistics pressure, larger = less micro. | |
| **Phase C β Finite COLLECTION storage. β€Ί REVERTED β Refinery/Foundry/Arsenal are no longer | |
| drop-offs.** The forward-collection capability described below shipped, then was later | |
| removed: the Refinery/Foundry/Arsenal no longer carry `dropOff`/`storeCap`, `isGatherDropOff` | |
| now resolves to the Command Center alone, and the AI's forward-Refinery placement logic | |
| (`aiEconomy.js`) was deleted along with it. Every gather/haul trip is a straight run to a | |
| Command Center again. Left below for the historical record of what Phase C built and why. | |
| - The forward drop-offs (Refinery/Foundry/Arsenal β the pure `dropOff` buildings, via | |
| the new `isGatherDropOff`) get a finite `storeCap` intake buffer. A PLAYER gatherer | |
| banks its haul into that buffer (capped); a full one is skipped by `nearestGatherDrop` | |
| so the gatherer reroutes to the next collection point (the Command Center never fills). | |
| Phase A haulage clears the buffer to the CC automatically. Factories are excluded from | |
| gather drop-offs (their `store` is refined OUTPUT, not a raw-intake bin). | |
| - The **Command Center is the treasury/warehouse** β effectively unbounded ("very large | |
| storage"): haulage delivers there and supply picks up there (`nearestCommandCenter`), | |
| so goods flow *through* the CC, never sideways between drop-offs. | |
| - **AI-safe by construction:** the finite-intake path is gated to `owner === "player"`. | |
| An AI drop-off banks straight to the treasury as before, and the AI builds no | |
| producers so its workers never haul/supply β so the AI economy and the byte-identical | |
| `determinism`/`determinism-roster` replays are untouched (verified: full suite green). | |
| - `render.js` store bar (Phase A) covers forward drop-offs; `hud.js` shows a selected | |
| drop-off's intake buffer + "FULL: gatherers reroute" state. Tests in `test/gather.test.js` | |
| (finite intake + reroute) and `test/haul.test.js` (AI drop-off stays bufferless). | |
| **Known pre-existing issue (surfaced, not caused, by Phase C):** two+ workers rapidly | |
| cycling a drop-off planted *very close* to a node can separation-deadlock in the `toNode` | |
| walk (confirmed identical on the pre-Phase-C code). It's a movement/separation bug, out | |
| of scope here; a real placement keeps the drop-off far enough from the node to avoid it. | |
| **Phase D β Freighters join the local logistics chain. β DONE (player-only, Odyssey-only).** | |
| Revisits item 10 above: a landed freighter (`hauler`/`heavyhauler`/`bulkfreighter`) is now a | |
| *physical* object on the world map, not just a menu-driven hold, in two ways: | |
| - **Workers can load/unload it β FERRY (`engine/haul.js updateFerry`).** Right-click a | |
| friendly freighter with workers selected (`engine/commands.js issueFerryFreighter`, | |
| `input.js`) to assign them to it, the same idiom as `issueServiceBuilding`. A ferry | |
| worker keeps carrying the nearest own producer's backlog straight onto the freighter's | |
| `freight` hold β the freighter becomes its own **collection point**, no detour through | |
| the Command Center β and once there's nothing left to load, draws some of the hold back | |
| out and banks it at the treasury instead. Any freighter qualifies, no research needed: | |
| this is a worker loading a crate onto a ship, not the ship acting on its own. Shares | |
| `nearestBacklogProducer` with `assignHaul` (parameterised: a fresh auto-haul still waits | |
| for a β₯34%-full buffer; a worker already dedicated to a freighter takes any backlog) and | |
| the same per-producer `haulers` cap, so it can't out-compete regular haulage for labour. | |
| - **A freighter can BE the logistics chain β AI-LOGISTICS (`FREIGHTER_AI_TECH`).** A new | |
| Datacenter tech, `freighterai` (requires `aicores`, costs AI Cores to research), unlocks | |
| a per-freighter toggle (`unit.aiLogistics`, HUD button, `engine/commands.js | |
| issueSetAILogistics`). Toggled on, a freighter offers itself to the SAME | |
| `assignHaul`/`assignService` idle-worker logic every tick (`engine/sim.js`) β it grabs | |
| a haul or service job exactly like a worker would, but at its own trip capacity: with no | |
| `cargoCap` of its own, `tripCapacity()` falls back to the ship's whole `cargoHold` | |
| (250/650/1600 vs. a worker's 10), so one autonomous run clears a backlog a worker would | |
| need many trips for. Running this way isn't free: every tick an autonomous job is active | |
| it burns AI Cores from the treasury (`payAIUpkeep`, rate scaled by the ship's own | |
| `cargoHold` β a bigger hold costs more to run unmanned). Out of AI Cores, it just pauses | |
| in place for the tick (order and cargo untouched) rather than stranding anything; | |
| toggling AI-logistics off stands it down immediately. A freighter with no AI Cores in | |
| stock also never *claims* a fresh job in the first place, so it doesn't sit there hogging | |
| one of a producer's β€2 hauler slots while unable to move. | |
| - Both paths reuse the SAME `loadFrom`/cargo machinery workers already use (generalised to | |
| take a plain commodityβqty store instead of a building, so it works against either a | |
| building's `store` or a freighter's `freight`) β no parallel implementation to keep in | |
| sync. `freightUsed`/`freightRoom` moved from `engine/galaxy.js` to `engine/entities.js` | |
| (re-exported from galaxy.js for every existing caller) so `engine/haul.js` can read a | |
| freighter's room without an import cycle (`galaxy.js` β `sim.js` β `haul.js`). | |
| - Player-only and Odyssey-only by construction (freighters are `odysseyOnly`, the tech is | |
| gated behind the Datacenter/`aicores`, and the sim only ever offers this to | |
| `owner === "player"`) β the AI never builds a freighter, so the skirmish AI and its | |
| byte-identical replay are untouched. Tests in `test/ferry.test.js`. | |
| --- | |
| **Phase E β Command Center zone affinity + a worker REPAIR job. β DONE.** | |
| Revisits a gap the original four phases didn't cover: on a multi-base empire, every "nearest" | |
| job-pick above (`nearestBacklogProducer`, `assignService`'s factory scan, `assignFerry`'s | |
| freighter scan) searched the WHOLE empire from the idle worker's current position with no notion | |
| of "which base do I belong to." Once a busy base's own β€2-per-target caps (`MAX_HAULERS`/ | |
| `MAX_SERVERS`) filled up, its surplus idle labour got "poached" by whichever OTHER base's job | |
| happened to be nearest in that flat global search β a routine long, arbitrary cross-map commute, | |
| not just an occasional loan. The same gap existed in the auto-repair Mender's roam target pick | |
| (`engine/sim.js autoRepairRoam`), which also scanned globally by worn-first/distance with no | |
| zone awareness. | |
| - **Zone affinity (`engine/gather.js zoneFirst`).** A building/unit's "zone" is simply whichever | |
| of its owner's Command Centers sits nearest to it β computed on demand, never stored/cached, so | |
| founding or losing a CC instantly redraws every boundary with nothing to invalidate. `zoneFirst` | |
| runs a caller-supplied scan TWICE: first restricted to the searcher's own zone, and only if that | |
| comes back empty, once more with no restriction β today's plain global search, now a last | |
| resort instead of the routine case. A one-CC game (nearly every skirmish, and Odyssey before a | |
| player expands) has exactly one zone, so behaviour there is byte-identical to before this | |
| existed β verified by a dedicated test alongside the cross-zone ones. | |
| `nearestBacklogProducer`/`assignService`/`assignFerry` (`engine/haul.js`) and the Mender's | |
| `autoRepairRoam` (`engine/sim.js`, via the new `pickRepairTarget` below) all adopt it. | |
| - **A worker REPAIR job (`engine/repair.js`).** Buildings could only be healed passively by a | |
| Mender in range, or by an `autoRepair` Mender roaming to the worst-off one β nothing stationed | |
| labour there when no Mender was around, even though Odyssey structures wear down on their own | |
| (`updateDecay`). A generalist worker can now be assigned a `repair` order β auto (idle workers | |
| offer themselves the same zone-first way haul/service already do, after ferry/haul/service in | |
| priority) or manual (right-click a damaged own building with workers selected, | |
| `issueRepairBuilding` β any completed building qualifies, not just a logistics-buffered one). It | |
| walks to the building and patches it at a flat `WORKER_REPAIR_RATE` (free, like a Mender's heal β | |
| time, not resources), capped at `MAX_REPAIRERS` per building so labour spreads out, same shape as | |
| the haul/service caps. Buildings only β a Mender already covers wounded mobile units, and a | |
| stationary "post a request to the nearest CC" model fits a fixed structure, not a unit that | |
| might have moved by the time labour arrives; that's a deliberate v1 scope line, not an oversight. | |
| `pickRepairTarget` (zone-first, worst-hp-fraction-first, the shared `NEEDS_REPAIR`/`HEALED` | |
| hysteresis thresholds) is exported so the Mender's own roam target-pick reuses the exact same | |
| selection logic instead of a second copy. | |
| - **AI-safe and deterministic by the same construction as Phases A-D:** zone affinity and the | |
| repair job are pure functions of state (id-tiebroken, no wall clock), and the Odyssey AI's own | |
| worker logistics (`engine/aiWorkers.js assignAiLogistics`, endless-only) picks up both the zone | |
| affinity and the new repair job for free by calling the same `engine/haul.js`/`engine/repair.js` | |
| functions the player's workers use β its skirmish behaviour is untouched (`assignAiLogistics` is | |
| never invoked outside Odyssey). Tests: `test/zones.test.js` (cross-base zone preference for | |
| haul/service/ferry, plus the single-CC byte-identical case) and `test/repairJob.test.js` | |
| (auto/manual assignment, the repair cap, and zone preference). | |
| **Phase F β home-base override + repairing mobile units too. β DONE.** | |
| Closes the two items Phase E deliberately left for later. | |
| - **A player-assigned home base (`unit.homeCC`, `engine/commands.js issueSetHomeBase`).** Zone-first | |
| (Phase E) still GUESSED a unit's home zone from raw nearest-CC distance β usually right, but the | |
| player had no way to override it. Right-clicking a completed own Command Center with eligible | |
| units selected (worker, Mender, or a freighter β anything that ever consults a zone) now pins | |
| `unit.homeCC` to it; `engine/gather.js zoneFirst` takes an optional `homeId` and, when it resolves | |
| to a live owned CC, uses it INSTEAD of the distance guess β the player decides which base's | |
| territory a unit's jobs stay loyal to, not just proximity (directly answering "the player would | |
| be in charge of where sufficient resources are, per area"). It's passive: setting it never | |
| interrupts whatever order the unit is already running, only what its NEXT idle job search | |
| prefers. A stale override (its CC destroyed, or never valid) is ignored, falling straight back to | |
| the distance guess β self-healing, nothing to clean up. Threaded through every zone-first call | |
| site: `nearestBacklogProducer`/`assignService`/`assignFerry`/`updateFerry`'s replan (`haul.js`), | |
| `pickRepairTarget` (`repair.js`, so both the worker repair job AND the Mender's own `autoRepairRoam` | |
| honor it). HUD: a selected unit with a home base shows a "π Home base assigned" note and a Clear | |
| button; a selected Command Center shows how many units call it home. | |
| - **Refinement β an explicit assignment is a HARD boundary, not just a tie-break.** The first cut of | |
| `zoneFirst` fell back to a global search whenever the (overridden or guessed) home zone came up | |
| empty, no matter which way `home` was picked. That's right for the DEFAULT distance guess (a mere | |
| heuristic β widen the search rather than sit idle) but wrong once the player has deliberately | |
| assigned a base: a hauler pinned to Command Center A should keep cycling through WHATEVER other | |
| jobs A's own zone has to offer, and simply wait when it has none β never reach into Command Center | |
| B's territory on its own. `zoneFirst` now tracks whether `home` came from a valid override or the | |
| distance guess, and only widens to the whole empire in the latter case; a pinned unit with an empty | |
| zone gets `null` (no job, stays idle) until the player reassigns it to a different CC (right-click | |
| it β `issueSetHomeBase` overwrites the old assignment) or clears it outright (the HUD's "Clear home | |
| base" button, or setting `homeCC = null` β "assign to nothing"). This is also exactly why a | |
| building-level manual assignment (`issueServiceBuilding`/`issueRepair`/`issueFerryFreighter`, | |
| `order.manual`) was left untouched: it was ALREADY a hard, single-target lock (the worker loops on | |
| that one building/freighter forever once done, never auto-reassigned) β the contrast is | |
| intentional. A CC assignment is a *territory* (flexible among whatever's inside it); a building | |
| assignment is a *target* (one specific thing, no substitutes). Tests: `test/zones.test.js` (a | |
| pinned empty zone stays idle rather than poaching; reassigning to a different CC immediately | |
| redraws the boundary; clearing the override restores the old lenient default). | |
| - **A worker can now repair a wounded mobile UNIT, not just a building.** `pickRepairTarget` already | |
| supported `includeUnits`; `assignRepair` now leaves it at the default (true) instead of forcing | |
| buildings-only, so a worker's auto-assigned or manually-issued (`issueRepair`, renamed from | |
| `issueRepairBuilding` β now takes either a building or unit id) repair job can chase and patch a | |
| hurt Ranger/Bastion/etc. the same way it patches a turret β walking toward the target's LIVE | |
| position each tick, the same idiom the Mender's own roam already uses for a moving target. The | |
| order's `buildingId` field became a generic `targetId` (resolved via `state.js getEntity`, building | |
| OR unit) to match. A worker still never repairs itself (`exclude: unit`). This doesn't replace the | |
| Mender β a Mender still heals passively/for free in an AoE and can roam on its own β it just means | |
| a damaged unit isn't stranded with nothing to do about it when no Mender is nearby. | |
| - Tests: `test/zones.test.js` (a home-base override wins over both raw proximity and the usual | |
| nearest-CC guess; a stale override falls back gracefully), `test/repairJob.test.js` (a wounded | |
| unit is now a valid auto-assigned AND chased-and-healed target; a worker never targets itself), | |
| and `test/commands.test.js` (`issueSetHomeBase`'s role gating and its passivity; `issueRepair`'s | |
| gating). Same AI-safety/determinism shape as every phase above β no wall clock, no unseeded | |
| randomness, skirmish AI untouched. | |
| ## Outcome | |
| All six phases shipped (Phase C was later reverted β see above: the Refinery/Foundry/Arsenal | |
| are no longer drop-offs, and all collection now runs straight to the Command Center). Storage | |
| is finite end-to-end β production output (rig + factories) and factory inputs β and workers | |
| move every good between them (gather β CC β supply β factory β haul β CC). A landed | |
| freighter now sits IN that chain too β a physical collection point workers can ferry to | |
| directly, or (once teched) a large-capacity autonomous hauler in its own right, paid for | |
| in AI Cores while it runs. Logistics is a standing demand on labour (and, for an automated | |
| fleet, on AI Cores) to the end of the game, with the Command Center as the one bottomless | |
| warehouse. On a multi-base empire, every job (haul, service, ferry, repair) is discovered | |
| zone-first β and the player can pin a unit's home base outright, overriding the distance | |
| guess β so labour stays loyal to its own base instead of routinely commuting across the map. | |
| A worker can now actively repair a damaged building OR a wounded mobile unit, not just a | |
| passing Mender. Energy stays a placement decision (grid efficiency), no workers. Everything | |
| player-only (or, for zone affinity and repair, ALSO extended to the Odyssey AI's own logistics | |
| workers, by construction never the skirmish AI) and deterministic; the AI and skirmish replays | |
| are byte-identical. | |
| ### 2.5 Recommended order & risk | |
| `Phase A` (low risk, player-only, self-contained, immediately satisfying) β | |
| `Phase B` (medium, builds the supply network) β `Phase C` (higher, touches the AI & | |
| market, needs the most careful determinism pass). Ship and play-test each before the | |
| next; do not attempt all three in one pass. | |