From 081cddcab32f8e25bd0aa1d643e014a6e5cecae2 Mon Sep 17 00:00:00 2001 From: Natalie Date: Sat, 27 Jun 2026 08:29:44 -0400 Subject: [PATCH] =?UTF-8?q?docs(@projects/@magic-civilization):=20?= =?UTF-8?q?=F0=9F=9B=A4=EF=B8=8F=20Rail-1=20design=20=E2=80=94=20narrow=20?= =?UTF-8?q?the=20dual-model=20fork=20(cities=20~done,=20units=20are=20the?= =?UTF-8?q?=20hold-out)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../p3-rail1-ui-pure-view-migration-design.md | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/.project/designs/p3-rail1-ui-pure-view-migration-design.md b/.project/designs/p3-rail1-ui-pure-view-migration-design.md index ec3cea98..bb9c8b85 100644 --- a/.project/designs/p3-rail1-ui-pure-view-migration-design.md +++ b/.project/designs/p3-rail1-ui-pure-view-migration-design.md @@ -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>` (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