File size: 25,984 Bytes
6dbb379
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f39bc5
 
 
 
 
 
 
6dbb379
 
 
91ad84b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
832f494
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6dbb379
4ed9122
 
 
 
 
 
89beffa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f39bc5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89beffa
 
32d2559
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
06de929
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ee2f17b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
06de929
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32d2559
89beffa
 
4ed9122
 
 
 
6f39bc5
 
 
 
32d2559
06de929
 
 
 
 
 
 
6dbb379
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
# 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.