docs(@projects/@magic-civilization): 🛤️ Rail-1 design — narrow the dual-model fork (cities ~done, units are the hold-out)

Verified the live city-projection path: api-gdext/city_slot.rs is a full ops
module over presentation_cities (rich mc_city::City) + GdCity wraps it; CityScript
is a hybrid proxy already routing to the Rust slot. So cities are largely
Rust-authoritative — the GDScript residue is just the city-centre queue +
placed_buildings. UNITS are the real Phase-1 hold-out (UnitScript fully
GDScript-authoritative, no Rust slot). Rails: bench CityState/MapUnit and the live
game are sanctioned-separate contexts (code-layering #3), so the live view must
project the RICH city / a new live-unit store — NOT the bench types. Refines the
Phase-1 plan accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-27 08:29:44 -04:00
parent 8c3e7b8a27
commit 081cddcab3

View file

@ -95,9 +95,28 @@ field; animation deltas / VFX (render-only, never sim state).
### Phase 1 — Rust holds the LIVE state (the SOT flip; render-gated)
Make `GdGameState.inner` (+ `presentation_cities`) the authoritative live state, synced on every
mutation — i.e. route live input through `act()` so there is no GDScript-only mutation to diverge.
Widen the bench model where the live game needs fields it lacks (unit XP, per-building queues,
placed-buildings) so Phase-0 projections carry real live data. This is the dual-model unification
(p3-25 step 6, p3-26 B7) generalised from cities to units.
**Refinement (verified 2026-06-27 — the dual-model "fork" is narrower than p3-25 framed):**
- **CITIES are already largely Rust-authoritative.** `api-gdext/src/city_slot.rs` is a full
operations module over `presentation_cities: Vec<Vec<mc_city::City>>` (the *rich* city: spawn,
growth, culture, production, per-building queues, citizens, borders, yields, focus, take_damage),
and `GdCity` (lib.rs:2056) wraps it. The GDScript `CityScript` (`city.gd`) is a **hybrid proxy**
`get_population`/`get_hp`/`owned_tiles` already route to the Rust slot. The GDScript-only city
residue is small: the **city-centre `construction_queue`/`production_progress`** + `placed_buildings`
tile positions. So city work = move that residue into `mc_city::City` + project the rich city for
the live view (NOT "unify two city types" — `presentation_cities` IS the rich type).
- **UNITS are the real hold-out.** `UnitScript` (`unit.gd`) is **fully GDScript-authoritative** with
**no Rust slot parallel** (audit). There is no live Rust unit store; the bench `MapUnit` is a
separate (self-play) context. The heavy Phase-1 lift is giving the live game a **Rust-authoritative
unit store** (the units analogue of `presentation_cities`) so unit state (position, hp, movement,
XP, posture) lives in Rust and the GDScript `UnitScript` becomes a proxy/view.
- **Rails note:** the lean bench `CityState`/`MapUnit` (fast self-play) and the live game are
*sanctioned-separate contexts* (code-layering principle 3 — different programs sharing logic
crates, not state). So "widen the bench model" is the WRONG framing for the live view: the live
view projects the **rich** `mc_city::City` / the new live unit store, not the bench types. The
Phase-0 bench-`CityState``CityView` projection I enriched serves the headless harness; the live
game needs a rich-City + live-unit projection. (Bench unit XP is still worth porting so the
headless sim has veterancy — tracked separately, fork-independent.)
### Phase 2 — Live turn = `end_turn()` (render-gated; p3-29 steps 3-5)
`turn_manager.end_turn()` calls `GdTurnProcessor.step(GdGameState)` once instead of the per-player