Commit graph

3678 commits

Author SHA1 Message Date
Natalie
f9593c4d29 docs(@projects/@magic-civilization): 🗺️ p3-26 — roadmap to complete the headless simulator (loop done-criterion)
Owner directive: the /loop isn't finished until the SIMULATOR is complete — the headless
Rust sim must play full self-play games with ALL systems, not the reduced subset.

p3-26 enumerates the verified live-vs-headless gaps + sequenced plan:
- Gap 1: climate/environment runtime (port the marine→climate→weather→effects chain into
  mc-turn; physics already in mc-climate).
- Gap 2: natural/"apocalyptic" events (M3 milestone — port .messy ecological_events.gd,
  12 categories, deterministic per EVENT_FREQUENCY_SPEC).
- Gap 3: equipment/crafting (recipes exist; no headless Craft action).
- Gap 4: per-building build queues (dual city-model; bench has a single queue).

Corrects my earlier "apocalyptic events don't exist" — they're specced (m3-natural-events)
with a .messy reference impl, just unimplemented in Rust.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 08:25:01 -04:00
Natalie
4fe3bc6091 feat(@projects/@magic-civilization): 🧪 p3-25 headless hardening — wire resource categories into mc-sim benches
Owner chose headless-only hardening over the live-game refactor (step 6 deferred).

- mc_sim::load_deposit_categories(deposits_dir) reads public/resources/deposits/*.json →
  id→category map (handles single-object or array files; skips bad files).
- dominion_bench + tournament_bench now set state.resource_categories from it after
  building GameState. These benches run the Rust TurnProcessor (process_trade_phase) but
  never loaded categories, so step-4's real sourcing had left their inter-player trades
  inert (sourcing from empty categories → no luxuries/strategics → no trades). Now bench
  trade dynamics (trade_willingness axis, gold-from-sales) form again.

Also recorded: real-game confirmation that the headless pipeline is live — the magic-civ
MCP view_json returns cities[].owned_tiles populated (step-2 territory projection running
in a real headless game on the rebuilt dylib).

Verified: mc-sim load_deposit_categories_reads_real_deposits passes; dominion_bench +
tournament_bench compile. solo_dominion (single-player, no trade partners) intentionally
not wired.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 07:15:15 -04:00
Natalie
828b4171c6 docs(@projects/@magic-civilization): 📐 p3-25 step 6 — sequence the live-game trade-unification plan + risk
Step 6 fully scoped: making the live game Rust-authoritative for trades is one
interlocking change (sync dual city model → run process_trade_phase in the live turn →
panel reads Rust deals FFI → retire the shipped GDScript Diplomacy.process_turn) with no
safe isolated brick. It modifies the working, screenshot-proven p3-23 live trade feature
for Rail-1 purity (not a functionality gap). Plan + risk recorded; awaiting go on the
approach before touching the live trade system.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 02:40:49 -04:00
Natalie
3f7a4e5442 feat(@projects/@magic-civilization): 🔌 p3-25 — wire headless harness to load resource categories (trade pipeline now LIVE)
The headless trade pipeline was unit-proven but inert in real runs: nothing called
set_resource_categories_json, so process_trade_phase saw empty categories and sourced
nothing. Wire it in.

- scenes/headless/player_api_main.gd::_apply_resource_categories builds the resource
  id→category map from DataLoader.get_all_resources() and stamps it onto GdPlayerApi via
  set_resource_categories_json, AFTER load_state_json (same #[serde(skip)] re-stamp
  pattern as units_runtime_catalog + tech_web). Now a real headless game classifies
  owned-tile collectibles → sources luxury/strategic surpluses → forms trades → view_json
  carries them. End-to-end LIVE.

Verified: unit+integration GUT 750 (737 pass / 13 pending / 0 fail); the headless
projection-roundtrip boot path (which exercises _apply_resource_categories) is green.
GDScript-only change calling an existing FFI — no dylib rebuild needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 02:32:42 -04:00
Natalie
91c0b79ad1 docs(@projects/@magic-civilization): 📋 p3-25 — record e2e proof + no-regression verification (canonical GUT 607/0)
p3-25 steps 1-5 verification recorded in the objective:
- End-to-end: process_trade_phase forms+persists a real StrategicSwap → projected into
  view_json (steps 2-5 chain proven).
- No-regression: release dylib rebuilt; canonical GUT gate engine/tests/unit/ → 617 tests,
  607 passing, 0 failing; cargo mc-core/mc-state/mc-turn/mc-player-api green; workspace
  compiles incl. api-gdext dylib.
- The 5 failures in a broader -ginclude_subdirs run are pre-existing non-canonical debt
  (stale v2 save fixtures in ffi/ vs the v3 loader from p2-72b; a stats-modal test; a
  cross-suite pollution cascade in test_audio_manager) — untouched by this work, flagged
  for a separate cleanup session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 02:26:53 -04:00
Natalie
7e93dcef42 test(@projects/@magic-civilization): p3-25 — end-to-end headless trade pipeline proof
process_trade_phase_forms_and_persists_strategic_swap: a crafted 2-player state with
complementary owned-tile strategics (p0 rainforest→hardwood, p1 mountains→iron_ore,
biomes guarantee neither has the other's) → process_trade_phase forms a StrategicSwap,
persists it to state.trade_ledger, and fans it onto both players' traded_strategics.

Proves the full chain end-to-end: owned-tile territory (step 2) → resource-category
classification (step 3) → real sourcing + evaluate_trades + persistence (step 4) →
which DiplomacyView.trade_deals then projects (step 5, separately tested). mc-turn green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 02:09:20 -04:00
Natalie
507a87104f feat(@projects/@magic-civilization): 👁️ p3-25 step 5 — project real trade deals into view_json (DiplomacyView.trade_deals)
view_json now carries real inter-player trades — the headless "simulator provides
everything" goal is met. A player-like the headless adapter sees territory (step 2) AND
trades (this step) from the projected view, no GDScript re-derivation.

- view.rs: DiplomacyView gains trade_deals: Vec<TradeDealView> ({kind, you_receive,
  you_give, gold_per_turn}, described from the viewer's perspective; serde skip-if-empty
  for wire stability).
- projection.rs build_diplomacy: populates trade_deals from the persisted
  state.trade_ledger swap/sale agreements (LuxurySwap/StrategicSwap/ResourceSale) for the
  viewer↔counterpart pair, via swap_deal_view/sale_deal_view helpers (correct give/receive
  direction; sale gold signed + for seller, − for buyer).

Verified: projection_surfaces_trade_deals_from_ledger (luxury swap direction + sale
buyer/gold); mc-player-api 171/0. (Disk filled mid-step from cargo target — cargo clean
reclaimed 9.5GiB; tests re-run from a clean build.)

p3-25 steps 1-5 DONE: view_json now carries territory + real trades, sourced fully in
Rust. Step 6 (live game adopts the unified PlayerView) reframed as a large separate
follow-on — the headless view-completeness this objective targets is achieved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 02:04:57 -04:00
Natalie
1a7fd849c0 feat(@projects/@magic-civilization): 💱 p3-25 step 4 — real trade sourcing + ledger persistence in the headless turn
The core of the rail-1 trade port: inter-player trades now form in the Rust headless
sim from REAL owned-tile resources (no proxy), persist to state, and apply.

- mc-turn::process_trade_phase: source_tradeable_resources sources each player's tradeable
  luxuries + strategics from its cities' owned tiles → deterministic tile_collectibles
  rolls (seed = map_seed ^ coord, stable across turns) → classified via
  GameState.resource_categories (dups kept for MIN_COPIES_TO_TRADE). Replaces the old
  proxy (tile_strategics: Vec::new(), tile_luxuries from traded_luxuries).
- Persists the re-derived swap/sale agreements into state.trade_ledger (retaining the
  persistent OpenBorders/SharedMap), so the projection/view can carry real trades.
- Writes PlayerState.traded_strategics (new serde-default field) + applies net per-turn
  gold flow (gold_flow_for: seller +, buyer −).

Verified: mc-turn source_tradeable_resources_classifies_owned_tile_collectibles
(determinism + classification purity + uncategorized-filtered + empty-categories no-op);
mc-turn+mc-state+mc-player-api 517/0; workspace cargo check clean (new PlayerState field
broke no literals). p3-25 steps 1-4 done; 5-6 remain (project trade deals into the view,
then GDScript view-only).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 01:57:32 -04:00
Natalie
e376920766 feat(@projects/@magic-civilization): 📇 p3-25 step 3 — resource-category catalog into Rust state
Rail-1 city-model unification, step 3: give the headless sim the luxury/strategic
categories it needs to classify owned-tile resources for trade sourcing — currently
GDScript-only (DataLoader). No content hardcoded in Rust (Rail-2): loaded from JSON.

- GameState.resource_categories: BTreeMap<String,String> (id → "luxury"/"strategic"/
  "bonus"), #[serde(skip)] boot-loaded exactly like units_catalog/civic_catalog (not
  save-persisted; empty Default → nothing tradeable, a safe no-op).
- GameState::load_resource_categories_json parses the flat {id:category} object GDScript's
  DataLoader emits; no-clobber on malformed input.
- GdPlayerApi.set_resource_categories_json FFI loads it onto the held state (call after
  load_state_json, since the field is serde-skip).

Verified: mc-state load_resource_categories_parses_flat_map + suite 13/0; workspace
cargo check clean (GameState field addition broke no literals — all use ..Default).
Rust-only; live game unaffected. Unblocks step 4 (process_trade_phase classification).
p3-25 steps 1-3 done; 4-6 remain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 01:37:59 -04:00
Natalie
37fbb6153d feat(@projects/@magic-civilization): 🗺️ p3-25 step 2 — real city territory in the bench sim (culture border expansion) + projected
Rail-1 city-model unification, step 2: give the headless/bench simulation real,
growing territory so view_json carries it (toward "simulator provides everything").

- mc_city::CityState gains owned_tiles: Vec<(i32,i32)> (serde default; backward-compat).
- mc-turn::process_culture: the culture-ready list from CulturePool::tick_all was
  previously DROPPED (let _ready = ...). Now each ready city claims one contiguous,
  in-bounds frontier tile per turn into owned_tiles — real border expansion in Rust.
  Deterministic pick (lowest col,row among the unclaimed frontier); city centre owned
  implicitly via city_positions, materialised on first expansion; consume_expansion
  advances the threshold. Grid dims read before the &mut player borrow.
- mc-player-api projection: CityView.owned_tiles (schema field that existed but was
  stubbed Vec::new()) now projects CityState.owned_tiles, with a centre fallback so
  every city reports at least the tile it sits on.
- Fixed a pre-existing broken test (serde_roundtrip HappinessInput literal missing the
  building_happiness_effects/happiness_per_city_effects fields p3-24 added).

Verified: cargo test mc-city + mc-turn + mc-player-api 725/0, incl. new
culture_expansion_claims_frontier_tiles + projection_surfaces_city_owned_tiles. Rust-only
headless-path change; live game (presentation_cities) unaffected. Unblocks step 4
(trade sourcing from owned-tile resources). p3-25 steps 1-2 done; 3-6 remain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 01:18:24 -04:00
Natalie
922c18fb0c feat(@projects/@magic-civilization): 🛤️ p3-25 step 1 — projection reads real agreement state (de-stub DiplomacyView)
Owner directive: "gd should only be UI view of simulation / simulator provides
everything / no stubs". Root cause (verified): the sim holds two parallel city models —
authoritative mc_city::City (presentation_cities, has owned_tiles) vs bench
mc_state::CityState (GameState.players[].cities, no territory) — and project_view reads
the bench one, so view_json is structurally blind to territory + trades. New objective
p3-25 captures the full sequenced unification plan.

Step 1 (this commit) de-stubs what real bench state already carries, no fabrication:
- projection.rs build_diplomacy: DiplomacyView.{open_borders,shared_map,agreements_active}
  now read the real OpenBorders/SharedMap entries from state.trade_ledger (the entries
  dispatch writes on signing), replacing hardcoded false/false/empty stubs.
- CityView.owned_tiles left honestly TODO (center-only would mislead; fills in step 2 when
  bench territory + border expansion are ported).

Verified: cargo test -p mc-player-api 169/0 (incl. new projection_surfaces_open_borders_
from_ledger). Rust-only headless-view change; no GDScript touched, live game path
unaffected. Swap/sale trade deals NOT yet in the view — they need the sourcing+persistence
port (p3-25 steps 2-5); not faked here per "no stubs".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 00:43:46 -04:00
Natalie
e6b7c9b2ce feat(@projects/@magic-civilization): p3-23 DONE — trade richness complete; deal-UI screenshot-proven
Phase-gate proof for the deal UI (revival step 5 verification): diplomacy_deal_proof.tscn
instantiates the REAL diplomacy_panel.tscn with a crafted GameState (human + 2 AI rivals,
ledger holding one LuxurySwap + one StrategicSwap + one ResourceSale) and self-captures.

Screenshot reviewed in-conversation — the panel renders, in the correct per-rival rows:
- AI 1 (Ironhold): "Luxury Trade: Receiving Silk for Furs" + "Resource Sale: Buying
  Horses (−2 gold/turn)"
- AI 2 (Goldvein): "Strategic Trade: Receiving Coal Seam for Iron Ore"
All three deal types render with correct direction, resources, and gold flow.

p3-23 status partial → done. Every acceptance bullet now met with evidence: gold↔resource
+ strategic swaps + luxury swaps (mc-trade) · AI evaluation · in-game pipeline revived
end-to-end (steps 1-4, GUT 750/0 + 25-turn arena exit 0) · deal UI (step 5, screenshot).
tribute.rs stays Game-2 deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 00:07:43 -04:00
Natalie
99e0a4447f feat(@projects/@magic-civilization): 🤝 p3-23 revival step 5 — deal UI: active trade deals in the diplomacy panel
Active inter-player trade deals now surface in the diplomacy panel's per-rival agreement
section, alongside open-borders / shared-map rows.

- diplomacy.gd get_active_agreements parses the serde-tagged LuxurySwap / StrategicSwap /
  ResourceSale entries straight out of GameState.trade_ledger_json (_append_trade_deals +
  _swap_entry + _sale_entry — pure GDScript, no new FFI). Each deal becomes a display dict
  {type, partner, you_receive/you_give | role/resource/gold_per_turn}.
- diplomacy_panel._make_agreement_section renders luxury_swap/strategic_swap (receiving X
  for Y) + resource_sale (buying/selling X, ±gold/turn). 6 diplomacy_* vocab keys added.
- GUT test_get_active_agreements_surfaces_trade_deals: all three deal types + partner/
  direction/resource fields. Panel script compiles + its tests pass. Full suite 750/0.

p3-23 implementation + logic now COMPLETE and GUT-proven across steps 1-5. The only item
left before status:done is a phase-gate proof screenshot of the trade rows (needs a
crafted live state with a human-held ledger deal; not reproducible in the all-AI arena).
Stays partial per objective-integrity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 23:38:21 -04:00
Natalie
916dcda55d test(@projects/@magic-civilization): p3-23 revival step 4 — verify trade tile-sourcing; full pipeline end-to-end proven
Added the last missing link test: _collect_tradeable_resources against a real GameMap.

- test_collect_tradeable_resources_classifies_owned_tiles: builds a GameMap with
  iron_ore×2 (strategic) + silk (luxury) deposit tiles owned by a player, asserts
  _collect_tradeable_resources returns strategics=[iron_ore,iron_ore] (dups kept for
  the MIN_COPIES_TO_TRADE surplus rule) + luxuries=[silk]. Proves _serialize_players'
  real DataLoader-category tile sourcing. NB: DataLoader maps the "resources" category
  to the deposits/ dir — served strategic ids are iron_ore/horses, not "iron".
- before_all loads the theme (category lookups need DataLoader). GUT 749/0.

Full inter-player trade pipeline now GUT-proven link-by-link AND headless-proven live:
real tiles → _collect_tradeable_resources (step 4) → process_trades → ledger →
traded_luxuries/strategics (step 1) → strategic build access (step 3); gold sale →
gold_flow_for → net gold (pre-existing); integration runs every round in a live 25-turn
arena without aborting the loop (step 2). Gold flow no longer inert — process_turn now
populates GameState.trade_ledger_json each round.

Only remaining for done: the deal UI (diplomacy panel + wire trade_agreed). Stays partial.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 23:08:26 -04:00
Natalie
f7959e11a0 feat(@projects/@magic-civilization): ⚔️ p3-23 revival step 3 — unit-gating honors traded strategics
A strategic resource gained via an active inter-player trade now grants unit-build
access exactly like a tile-owned copy (Civ-style "access", not stockpile).

- turn_processor._player_owns_resource (the live production-COMPLETION gate behind
  EventBus.strategic_gate_rejected) now short-circuits true when resource_id is in
  player.traded_strategics, before the game_map tile scan. The Rust
  GdCitySlot.enqueue_item gate has no live GDScript caller (unused FFI surface), so
  this completion gate is the only live unit-gating path.
- GUT: test_player_owns_resource_via_traded_strategic (traded → access) +
  _false_without_tile_or_trade (neither → no access). Full suite 748/0.

Acceptance chain now GUT-proven link-by-link: process_trades→ledger→traded_strategics
(step 1) · traded_strategics→build access (step 3) · gold_flow_for→net gold
(test_trade_gold_flows_into_net_gold, pre-existing).

Next (step 4): end-to-end in-game proof (a trade demonstrably forms in a played game,
gold flows >0, a gated unit becomes buildable) + deal UI. p3-23 stays partial.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 22:37:05 -04:00
Natalie
32dd6d2723 feat(@projects/@magic-civilization): 🔌 p3-23 revival step 2 — enable Diplomacy.process_turn in the turn loop (loop-survival proven)
Step 1 made diplomacy.gd's contract correct + isolation-proven. Step 2 wires it into
the turn loop, carefully.

- turn_manager.gd now calls (diplomacy as DiplomacyScript).process_turn(GameState.players,
  GameState.turn_number, GameState.get_game_map()) once per full round, where the old
  empty-stub call sat disabled. The original abort risk was a MISSING method (the stub
  had none, so the call killed next_player + the arena loop). process_turn now exists and
  is internally defensive (null game_map, missing GdTrade extension, unknown resources all
  handled) so it cannot abort the round loop.

Verified (carefully, per owner): 25-turn headless AUTO_PLAY arena (seed 7) →
  exit 0, 0 SCRIPT ERRORs, 0 process_trades errors, 26 turn_stats rows, clean score victory.
The trade-eval runs every round against real players/cities/map without crashing.
NOT yet shown: in-game trade FORMATION evidence (auto_play has no trade logging; 25 turns
is sparse for complementary surpluses). Mechanism itself is GUT round-trip-proven (step 1).

Next (step 3): in-game trade visibility — GdTradeLedger agreements-enumeration #[func] +
wire EventBus.trade_agreed (dangling, no listener) into the chronicle, then a longer arena
to confirm deals form in play. p3-23 stays partial.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 22:08:37 -04:00
Natalie
e926345ad2 feat(@projects/@magic-civilization): 🔧 p3-23 revival step 1 — reconcile diplomacy↔process_trades contract (safe, isolation-proven)
Owner greenlit "revive carefully". First safe step: make diplomacy.gd's contract
correct and prove it works in isolation, WITHOUT enabling the turn-loop call.

- diplomacy.gd now matches the current GdTrade.process_trades {ledger} contract:
  _serialize_players emits the PlayerTradeInput shape (player_index, tile_luxuries,
  tile_strategics, trade_willingness), sourcing each player's controlled luxuries +
  strategics from owned tiles classified by resource `category`; process_turn reads
  result["ledger"], stores it, and _apply_ledger_resources fans the ledger's
  incoming_luxuries/incoming_strategics onto each player (buyer gains the resource).
- Removed the dead _apply_trade_changes/_apply_relation_changes (they matched an old
  contract that returned new_trades/relation_changes; process_trades returns {ledger}).
- player.gd gains traded_strategics (field + serialize/deserialize); _clear_pair_luxuries
  clears it on war. GdTradeLedger.incoming_luxuries #[func] added (mirrors incoming_strategics).
- test_diplomacy.gd: replaced the 4 stale _apply_trade_changes tests with ledger-based
  tests, incl. a full round-trip (PlayerTradeInput JSON → process_trades → ledger →
  _apply_ledger_resources → buyers gain wine/horses/silk/iron).

Verified: cargo check gdext; dylib rebuilt; canonical GUT 746/0 (both new tests pass).
Turn-loop call REMAINS disabled (next step enables it carefully). p3-23 stays partial.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 21:53:35 -04:00
Natalie
de983fac54 docs(@projects/@magic-civilization): 🔎 p3-23 — discovery: inter-player trade turn-integration is DISABLED in-game
Verify-first while scoping part B (strategic gating) surfaced that p3-23's premise
was wrong. The inter-player trade evaluation does NOT run in the played game:

- turn_manager.gd:287 has Diplomacy.process_turn() commented out under a stale
  "empty stub module" note (diplomacy.gd was rebuilt but never re-enabled).
- The only writer of GameState.trade_ledger_json is diplomacy.gd:32 inside that
  disabled call → the ledger is never populated → NO inter-player trades run
  (luxury, strategic, or gold).
- The diplomacy.gd <-> GdTrade.process_trades contract has drifted in 3 places:
  input shape ({index,traded_luxuries,personality} vs Vec<PlayerTradeInput>),
  return keys ({ledger} vs trade_ledger_json/relation_changes/new_trades), and
  relations advancement. So enabling is not a one-line uncomment.

Consequence: the part-A gold-flow wiring (last pass) is correct + GUT-tested but
INERT in-game until the integration is revived (it reads an always-empty ledger).

The mc-trade simulation logic remains complete + cargo-tested (66/0). p3-23's real
remaining work is now scoped: revive the diplomacy trade turn-integration
(reconcile the 3 contract drifts, re-enable carefully, add PlayerState.traded_strategics
+ unit-gating, verify headless+GUT) then the deal UI. Status stays partial.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 20:41:24 -04:00
Natalie
a8c01cb5e1 feat(@projects/@magic-civilization): 💰 p3-23 part A — inter-player gold sales hit the treasury in-game
Wires the ResourceSale gold flow into the live economy (leveraging the p3-24
phase-1 economy port). GdTradeLedger gains gold_flow_for + incoming_strategics
#[func]s; GdEconomy gains a trade_gold param added to net gold AFTER the yield /
golden-age multipliers (a trade transfer must not be amplified by difficulty
handicap); economy.gd._player_trade_gold reads GameState.trade_ledger_json via
GdTradeLedger and passes the player's net flow. A seller now gains gold and a
buyer pays it each turn a sale is active.

Verified: GUT test_trade_gold_flows_into_net_gold (seller +3 → net 8, buyer −2 →
net 3, trade_gold echoed); dylib rebuilt + canonical GUT 748/0.

p3-23 stays partial — gold-trade flow now live (part A); remaining part B is the
strategic-resource gating (FFI sources tile_strategics, PlayerState.traded_strategics,
unit-gating reads incoming_strategics) + the deal UI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 20:12:40 -04:00
Natalie
e00c0477ab docs(@projects/@magic-civilization): p3-24 phase 3 — climate HP-loss verified Rust-owned (bullet 3 done)
Verify-first (per the never-infer rule): the objective flagged climate_effects.gd:125
(unit.hp -= hp_loss) as a GDScript simulation-logic violation, but verification shows
the hp_loss COMPUTATION already lives in mc-climate::climate_effects::apply
(hp_loss = unit_damage × severity_scale; climate_effects.rs:113, 6 cargo tests).
GdClimateEffectsPhysics.apply delegates to it; climate_effects.gd is a thin marshaler
that fans the Rust-computed value onto GDScript Unit entities — the same sanctioned
pattern as economy.gd's disbanded_units fan-out (the file's own doc says so). No
GDScript simulation arithmetic remained, so bullet 3 is marked done with evidence
rather than churning already-compliant code.

All three named GDScript violations (gold, happiness, climate) now resolved. p3-24
stays partial only on the explicit (Stretch) bullet 4 (per-turn orchestration → Rust
turn driver), deferred to the broader pathfinder/turn port. cargo + GUT 747/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 19:39:51 -04:00
Natalie
417c8d195b refactor(@projects/@magic-civilization): 🏛️ p3-24 phase 2 — port happiness aggregation GDScript→Rust (Rail-1)
happiness.gd summed building happiness effects and applied the
happiness_per_city × city_count multiply IN GDScript before the GdHappiness
call. Moved both into mc-happiness:

- HappinessInput gains building_happiness_effects + happiness_per_city_effects
  (#[serde(default)]); building_happiness_total() does the sum + per-city multiply.
  calculate_happiness uses it. Legacy building_happiness kept as a back-compat
  default field.
- happiness.gd passes the raw effect lists (no arithmetic); turn_processor_helpers
  sum_building_effects → collect_building_effects (pure per-building extraction,
  its only caller was happiness.gd). The luxury-map assembly stays GDScript (tile/
  DataLoader extraction; mc-happiness is pure).

Verified: 2 new mc-happiness cargo tests (aggregates effects+per-city; back-compat
legacy field); mc-happiness 23/0; dylib rebuilt + canonical GUT 747/0 (full
happiness.gd path test_happiness_turn -6/-4 unchanged).

p3-24 bullet 2 done; stays partial — remaining: climate HP-loss→Rust, orchestration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 19:15:35 -04:00
Natalie
a330704fe4 refactor(@projects/@magic-civilization): 🏛️ p3-24 phase 1 — port gold aggregation GDScript→Rust (Rail-1)
economy.gd:65-79 computed gold IN GDScript (building-effect sum, gold-per-pop
multiply, gold-from-mines loop, percent sum) before the GdEconomy call —
violating "GDScript is presentation only". Moved all of it into mc-economy:

- New CityGoldRaw (per-building effect VALUES + population + mine_count) and
  aggregate_city_gold() that does the building-sum + per-pop×pop + per-mine×mines
  + percent composition. Pure arithmetic, cargo-tested.
- GdEconomy FFI now deserializes the raw shape and aggregates before process_gold.
- economy.gd reduced to data extraction: _collect_effect_ints/_floats (no summing)
  + mine count; zero gold arithmetic. gdlint clean.

Verified: 3 new mc-economy cargo tests (sums/per-pop+per-mine/percent+e2e);
GdEconomy bridge GUT tests migrated to the raw shape; mc-economy green; dylib
rebuilt + canonical GUT 747/0.

p3-24 bullet 1 done; stays partial — remaining phases: happiness assembly→
mc-happiness, climate HP-loss→Rust, orchestration (stretch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 18:48:57 -04:00
Natalie
d45ba32a3a feat(@projects/@magic-civilization): 💰 p3-23 (part 2) — gold-for-resource sales in mc-trade
Completes the trade-richness simulation logic. New DiplomaticAgreement::ResourceSale
forms as a barter FALLBACK: when a pair can't swap (one side has a surplus the
other lacks but not vice-versa), the surplus holder SELLS the resource to the
buyer for SALE_GOLD_PER_TURN. evaluate_trades produces it after the swap passes
(sale_candidate probes pa-then-pb, luxuries-then-strategics, deterministic).
TradeLedger.gold_flow_for(player) exposes the per-turn flow (seller +, buyer −);
incoming_luxuries/incoming_strategics route the bought resource to the right pool;
has_resource_sale + breaks-on-war. Variant grouped with the swap arms across the
renewal/courier matches; break_trades_on_war gets its own ResourceSale arm.

Verified: 4 new cargo tests (forms-as-fallback, no-sale-when-swap, strategic-
routing, breaks-on-war); existing no-surplus test updated for the new fallback;
mc-trade 66/0; api-gdext + mc-turn compile.

p3-23 stays partial — both trade-logic halves (swaps + sales) now done + tested;
remaining is the in-game application (mc-economy gold flow + traded-resource
gating/happiness + GDScript deal UI), which lands with p3-24's economy port.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 18:18:25 -04:00
Natalie
4a97ab1d02 feat(@projects/@magic-civilization): 💱 p3-23 (part 1) — strategic-resource swaps in mc-trade
Adds strategic-resource trading to the inter-player evaluator (iron for horses),
the high-value half of trade richness. New DiplomaticAgreement::StrategicSwap;
evaluate_trades forms one per pair independently of the luxury swap via a shared
swap_candidates helper; PlayerTradeInput gains tile_strategics (#[serde(default)],
forward-compatible). TradeLedger.incoming_strategics / has_strategic_agreement
expose the buyer's gained access (unit-gating); swaps break on war. Keeps the
"keep your last copy" surplus rule (MIN_COPIES_TO_TRADE). The variant is grouped
with LuxurySwap in the renewal/courier matches (non-renewable, re-derived each turn).

Verified: 4 new cargo tests (forms-from-complementary-surplus, needs-surplus-and-
complementarity, luxury+strategic coexist, breaks-on-war); mc-trade 62/0;
api-gdext compiles.

p3-23 stays partial — remaining: gold↔resource deals (mc-economy gold flow) +
in-game wiring (FFI sources tile_strategics, PlayerState.traded_strategics,
unit-gating reads incoming_strategics).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:50:45 -04:00
Natalie
9555f934a2 feat(@projects/@magic-civilization): 🦬 p3-21 DONE — drought drives fauna migration
Migration now responds to climate, not just population pressure. In the per-turn
population mover (generation.rs compute_migrations), drought_carrying_factor
lowers a dry tile's effective carrying capacity — so its populations exceed cap
and emigrate, while dry neighbours offer less room, steering herds toward wetter
ground. The MigrationPulse events path (biological.rs) gets a matching
data-driven migration_climate_stress trigger bias. Reads the tile's live
drought_counter; pure mc-ecology, runs via the existing per-turn engine tick.

Tests: migration_climate_stress_rises_with_drought_and_caps, drought_raises_migration_rate,
drought_reduces_carrying_capacity_triggering_migration; mc-ecology 338/0; dylib
rebuilt + deployed; GUT 747/0.

p3-21 → done. Next: p3-23 (trade richness).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:19:16 -04:00
Natalie
e77149f8fb feat(@projects/@magic-civilization): 🌫️ p3-20 DONE — weather reduces scouting in the rendered game
Completes weather→scouting. weather.gd gains vision_penalty_at(col,row) (worst
penalty from active events covering the tile, hex-distance footprint);
world_map_vision.recalculate_vision cuts each unit's sight radius by it (floored
at 1) before computing visible_hexes — so a unit standing in a storm/blizzard/dust
storm reveals fewer tiles. The penalty VALUE is Rust-derived (WeatherEvent.vision_penalty,
data); GDScript only reads + applies it. Headless path has compute_vision_with_penalties.

Verified: mc-climate 45/0, mc-vision 30/0, GUT vision_penalty_at (within/outside/
worst-overlap); dylib rebuilt + deployed; canonical GUT 747/0.

p3-20 → done. Next: p3-21 (weather-driven migration).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 16:45:04 -04:00
Natalie
c30b74523b feat(@projects/@magic-civilization): 🌫️ p3-20 (consumer) — compute_vision shrinks sight under weather
The vision-consumer half of weather→scouting. compute_vision_with_penalties takes
a per-tile vision-penalty map and reduces each unit's effective sight by the
penalty at its tile (floored at WEATHER_MIN_VISION=1, so a unit always sees its
own tile + immediate ring). compute_vision delegates with an empty map, so all
existing callers are unchanged (no churn). refresh_for_player +
compute_player_visible_set thread the penalty.

Test: weather_vision_penalty_shrinks_unit_sight (radius-2 disk 19 → radius-1 disk
7 under penalty 1; floored under penalty 99); mc-vision 30/0.

p3-20 stays partial — bridge/GDScript wiring (weather events → penalty map →
compute_vision_with_penalties) + dylib/GUT next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 16:11:16 -04:00
Natalie
923af9c0ec feat(@projects/@magic-civilization): 🌫️ p3-20 (core) — WeatherEvent.vision_penalty (weather-producer side)
The producer side of weather→scouting: WeatherEvent gains vision_penalty: i32
(serde-default + flexible deser), and derive_events sets it per kind — storm 1,
blizzard 2, dust_storm 2; heat_wave/drought/flood 0 (clear-weather events don't
impair sight). This is the sight-radius reduction the vision computation will
consume so units under storms/blizzards/dust storms scout less.

Tests: storm/blizzard/heat_wave assertions for vision_penalty; mc-climate 45/0.

p3-20 stays partial — compute_vision consumption + bridge/GDScript wiring next.
(Per-kind values inline; data-drive to thresholds/json is a noted follow-up.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 15:45:10 -04:00
Natalie
bb38d5db0e feat(@projects/@magic-civilization): 🌲 p3-19 DONE — flora half: deforestation depletes live flora populations
Completes player→ecology feedback. EcologyEngine::deplete_flora_at(col,row,amount)
depletes a tile's Producer-diet (flora) populations (registry-identified);
GdFaunaEcology.deplete_flora_at exposes it; EcologyState._on_tile_improved fires
it when a flora-clearing improvement (deforestation) completes — so clear-cutting
a forest removes its flora (the terrain→grassland change also drives the gradual
die-off). With the fauna half (over-hunting → extinction), the living world now
reacts to player pressure both ways. Logic stays in mc_ecology (Rail 1).

Test: deplete_flora_at_targets_producer_species_only (mc-ecology green); dylib
rebuilt + deployed; canonical GUT 745/0 (wiring loads, no regression).

p3-19 → done. Next: p3-20 (weather→scouting).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 15:16:18 -04:00
Natalie
d6eaa79838 feat(@projects/@magic-civilization): 🦌 p3-19 (fauna) — over-hunting depletes live populations → local extinction
Wires the player→ecology coupling for fauna (the USP: the living world reacts to
the player). GdFaunaEcology.deplete_species #[func] resolves the string species id
→ numeric via the species library and calls EcologyEngine::deplete_population.
combat_utils._roll_wild_creature_loot now passes the slain creature's tile into
item_system.roll_fauna_drops, which calls EcologyState.fauna_ecology.deplete_species
on every fauna kill — so sustained hunting drives a species to local extinction
(is_extinct), and the engine's growth/emergence recover it once pressure eases.
Logic stays in mc_ecology (Rail 1); GDScript only triggers + passes the tile.

Verified: mc-ecology cargo green; dylib rebuilt + deployed; canonical GUT 745/0
(new roll_fauna_drops signature + caller load cleanly, deplete_species callable).

p3-19 stays partial — flora-harvest half (chop/intensive → flora population) next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:47:23 -04:00
Natalie
eb2cf18c2d feat(@projects/@magic-civilization): 🦌 p3-19 (core) — ecology population depletion API for player pressure
The Rust foundation for player→ecology feedback: PopulationSlot::deplete(amount)
(floors at 0, can cross the is_extinct threshold) + EcologyEngine::deplete_population
(col,row,species_id,amount) → post-depletion population, safe 0.0 no-op for a
missing tile/species. This is the hook hunting/harvesting will call to make
over-harvest drive local extinction; the engine's existing growth/emergence then
recovers abundance once pressure eases.

Tests: deplete_reduces_floors_at_zero_and_can_extinct (PopulationSlot) +
deplete_population_reduces_tile_species_and_can_extinct (engine); mc-ecology green.

p3-19 → partial. Remaining: GdEcologyEngine #[func] + GDScript kill/harvest wiring
+ dylib/GUT (loop continues).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:06:31 -04:00
Natalie
d01d72082d feat(@projects/@magic-civilization): 🔭 p3-22 — AI builds dedicated scouts for exploration
pick_for_city gains a scout branch (after the early-military floor, before
expansion): when dwarf_scout is buildable (its tech/race/resource/building gates
met — mirrors pick_best_unit_of_type) and the capital owns no scout, the capital
queues one. Single scout, capital-only; the existing frontier-seek + scout-sweep
maneuvers (movement.rs) already drive dwarf_scout, so the AI stops diverting
combat units to scouting.

Tests: ai_builds_scout_when_buildable_and_none_owned +
ai_does_not_build_scout_without_its_tech; mc-ai 289/0 green. p3-22 → done.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 13:57:20 -04:00
Natalie
ed3c836e2f docs(@projects/@magic-civilization): 📋 reopen Game-1 scope — 6 verified gap objectives (p3-19..24)
Game 1 was NOT actually complete; the green dashboard overstated it. Created
objectives for the verified, untracked gaps (all confirmed 2026-06-25 against the
code, both Rust + GDScript paths):
- p3-19 player→ecology feedback (over-harvest/hunt deplete live populations; extinction)
- p3-20 weather affects scouting (vision penalty, not just movement)
- p3-21 weather-driven migration (migration ignores weather today)
- p3-22 AI builds dedicated scouts (only frontier-seeks with idle military)
- p3-23 trade richness (luxury swaps only; no gold/strategic trades)
- p3-24 Rail-1 port of economy/happiness/climate per-turn glue logic from GDScript

Dashboard regenerated. (Systems I'd wrongly doubted — ecology engine ticking, AI
worker improvements, naval harbor-gating — are confirmed working; not reopened.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 13:45:20 -04:00
Natalie
58b608804a docs(@projects/@magic-civilization): 📊 regenerate objectives dashboard — p3-18 done (290→291)
tools/objectives-report.py after marking p3-18 (water crossing) done. Keeps the
dashboard fresh for verify step 2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 07:29:08 -04:00
Natalie
e67c3d7265 docs(@projects/@magic-civilization): p3-18 DONE — water crossing complete (embark + transport, sim + UI, verified)
All core acceptance bullets ✓: embark (pathfinding gate, data-driven config,
auto-embark + combat penalty, AI water-pathing, single-source after deleting the
Civ-V model), transport (board/carry/disembark + cargo-loss), UI embark (Rust
embark_level #[func] + pathfinder.gd, GUT 745/0), and the deterministic end-to-end
conquest path (ford + cross-water attack capstone). Verified across cargo /
integration / GUT. Two explicitly-optional, non-blocking remainders documented
(full game_over demo; transport carried-unit target-protection).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 07:28:05 -04:00
Natalie
22d0379a2c test(@projects/@magic-civilization): p3-18 P6 — capstone: army fords water then attacks enemy on the far landmass
teched_army_fords_water_then_attacks_enemy_on_far_landmass: end-to-end proof of
the conquest payoff that motivated p3-18. Over an ocean-wall map, a player-0 army
with ocean_navigation embark fords the ocean column to landmass B (process_move_requests),
then strikes a player-1 unit waiting there (queued AttackRequest → process_pvp_combat):
a cross-water battle resolves and the enemy takes damage. Embark turns an
otherwise-unreachable rival on another landmass into an attackable one.

Deterministic, cargo-verifiable; complements P6-core (the ford proof). The full
headless 1v1-to-game_over demo is the only remaining (confirmatory) P6 item.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 07:26:11 -04:00
Natalie
1f3f535e10 refactor(@projects/@magic-civilization): 🧹 p3-18 P5b — trim vestigial embark inputs from the action FFI
Completes single-source on the action bridge. With auto-embark (no explicit
Embark action), the embark/adjacency inputs to legal_actions_for / can_invoke were
dead:
- api-gdext: drop is_embarked / adjacent_water / adjacent_land params from both
  #[func]s (+ the consume lines + doc).
- GDScript callers updated to the new arity: unit.gd (get_legal_actions +
  can_invoke_action), unit_panel.gd (legal_actions_for + its dead arg vars and the
  three dead _get_* helpers), test_unit_actions.gd (4 calls).
- unit.gd: remove the now-dead has_adjacent_water/has_adjacent_land fields (never
  set — their _refresh_unit_terrain_context setter is long gone) + their serialize/
  deserialize; test_unit_serialize.gd updated. is_embarked stays (real state).

Verified: cargo check green; dylib rebuilt + deployed; canonical GUT suite 745
tests / 732 passing / 0 failing (13 pending). No arg-mismatch errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 07:19:26 -04:00
Natalie
c141a86b77 docs(@projects/@magic-civilization): 📝 p3-18 — P5b done (UI embark, GUT-verified); feature complete in sim + UI
Embark + transport now work in both the headless sim (authoritative) and the
rendered UI, verified at unit / integration / GUT levels (732 GUT passing).
Remaining: P6-full headless 1v1-to-game_over demo (confirmatory over P6-core),
vestigial-FFI trim (cosmetic), transport refinements (deferred, documented).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 07:03:10 -04:00
Natalie
b02274f920 test(@projects/@magic-civilization): p3-18 P5b — GUT proof the rendered pathfinder embark gate works
test_pathfinder_embark.gd: over an ocean-column map, a land unit finds no path
without embark (embark_level 0), fords the column with embark (level > 0) and
reaches the far landmass; naval movement is unaffected by the embark arg.

Verified headless against the rebuilt dylib: GUT 732 passing / 0 failed (+3 from
729 — these tests; no regression). UI embark now matches the authoritative
headless sim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 07:02:03 -04:00
Natalie
7d40c2ce86 feat(@projects/@magic-civilization): p3-18 P5b(2) — rendered movement is embark-aware (UI)
Wires the rendered game's GDScript movement to the data-driven embark capability,
so a human playing the UI can cross water when teched (previously embark worked
headless only):
- pathfinder.gd: find_path / movement_range / find_path_with_fog gain a defaulted
  embark_level param (non-breaking for all existing callers); _is_passable lets a
  land unit enter water when embark_level > 0. Single-tier UI gate — the precise
  coast-vs-ocean tier stays authoritative in the Rust sim; the gate avoids
  hardcoding the biome→tier map in GDScript (Rail 1).
- KnowledgeWeb + TechWeb: embark_level(researched) passthroughs to the Rust
  GdTechWeb.embark_level (the tech→level mapping stays in Rust, data-driven).
- world_map.gd: _current_embark_level() (defensive) feeds the active player's
  level into all four pathfinder call sites (move, range overlay, fog previews).

gdlint clean. Verifies via dylib rebuild + GUT (next).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 06:54:42 -04:00
Natalie
04a6120fe5 feat(@projects/@magic-civilization): p3-18 P5b(1) — GdTechWeb.embark_level #[func] (single-source UI embark)
The rendered game moves via GDScript pathfinder.gd (GDScript-authoritative; no
Rust round-trip) over GDScript-canonical tech. To make UI movement embark-aware
WITHOUT re-deriving the tech→level mapping in GDScript (Rail 1: logic stays in
Rust), GdTechWeb gains embark_level(researched: PackedStringArray) -> i64 (0 none
/ 1 coast / 2 ocean) backed by the data-driven TechWeb::embark_level. The GDScript
pathfinder will call this and carry only a thin water-passability gate.

cargo check -p magic-civ-physics-gdext green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 06:43:19 -04:00
Natalie
307219e039 docs(@projects/@magic-civilization): 📝 p3-18 — P5a done, P6-core done; scope P5b (rendered-movement embark, single-source via bridge)
P5a (dead GDScript embark UI) and P6-core (ford integration proof) landed. P5b
(rendered UI embark) scoped: the rendered state is hybrid (GDScript GameState/Player
+ Rust GdGameState bridge); the single-source path is a GdGameState #[func]
exposing the Rust-computed EmbarkLevel, threaded into pathfinder.gd — dylib-gated
(build-gdext.sh works) + GUT. P6-full demo is confirmatory over the integration proof.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 06:36:19 -04:00
Natalie
d3b5b12612 refactor(@projects/@magic-civilization): 🧹 p3-18 P5a — remove dead GDScript explicit-embark UI (single source)
P3b deleted the Civ-V explicit Embark/Disembark actions on the Rust side, leaving
the GDScript UI plumbing dangling (it dispatched the now-removed "embark"/
"disembark" actions; gated on the amphibious keyword no unit carries, so it never
rendered). Removed it: unit_panel.gd embark_pressed/disembark_pressed signals,
their ACTION_SIGNAL_MAP entries + emit cases; world_map.gd signal connections +
_on_embark/_on_disembark handlers. No dangling refs remain; no GUT test referenced
them. Auto-embark (move onto water) remains the only embark model, now consistent
across Rust and the GDScript UI.

Remaining P5: the rendered game's movement (pathfinder.gd, GDScript logic) is not
yet embark-aware — entangled with the broader Rail-1 debt that rendered movement
runs GDScript rather than the Rust pathfinder. Tracked in the objective.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 06:33:20 -04:00
Natalie
11988d60a6 test(@projects/@magic-civilization): p3-18 P6-core — end-to-end proof an army fords open ocean to a far landmass
The scenario that motivated p3-18: an army on landmass A reaching landmass B
across an ocean strip. teched_army_fords_open_ocean_to_far_landmass asserts the
full chain through process_move_requests — without naval tech the ocean rejects
the move (army stays put); with ocean_navigation (Ocean embark) the army crosses
the strip onto the far landmass and auto-disembarks on arrival.

Deterministic, cargo-verifiable evidence (the project's preferred proof form), no
dylib/Godot needed. The full headless 1v1-to-game_over demo (P6-full) is the
heavier dylib follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 06:26:01 -04:00
Natalie
5aefbff8a2 docs(@projects/@magic-civilization): 📝 p3-18 — correct P5/P6 blocker: gdext builds locally (crate is magic-civ-physics-gdext)
The earlier "cargo resolver panic blocks the gdext dylib" claim was wrong: I was
running `cargo -p gdextension-api`, which is a crates.io godot dependency, not our
crate. The local crate is `magic-civ-physics-gdext`; `cargo check -p
magic-civ-physics-gdext` is green in ~38s. P5 (UI embark) and P6 (demo) are
feasible locally via build-gdext.sh + GUT — not build-host-gated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 06:23:38 -04:00
Natalie
e83362200e docs(@projects/@magic-civilization): 📝 p3-18 — P4 transport core done; P5 is functionally required (UI uses pathfinder.gd)
Records transport P4a/b/c complete + the finding that the rendered game moves via
GDScript pathfinder.gd (not the Rust pathfinder), so embark works headless only
until P5 plumbs the embark gate into the GDScript path (or routes movement through
Rust). Transport refinements (carried-unit target-protection, AI-use) deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 06:17:00 -04:00
Natalie
8b4c71688f feat(@projects/@magic-civilization): p3-18 P4c — carried units lost when their transport is destroyed
After the PvP combat phase, prune_orphaned_cargo drops any unit whose carrier_id
references a hull no longer in its player's roster — the transport sank, its
cargo goes down with it. Order-preserving removal that keeps the index-parallel
unit_upkeep aligned; idempotent and a no-op for carrier-free rosters (so existing
combat is untouched — pvp tests still green).

Test: destroyed_transport_loses_cargo (orphaned cargo pruned, uncarried unit
survives, unit_upkeep stays aligned).

Known follow-up: carried units ride the hull's hex (stacked); fully shielding
them from being individually targeted needs combat target-selection changes that
touch 1UPT assumptions — tracked, not in this commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 06:13:06 -04:00
Natalie
b40fc80bbc feat(@projects/@magic-civilization): p3-18 P4b — transport board / carry / disembark
Implements the carry mechanic in process_one_move, modelled as automatic moves
(consistent with auto-embark — no new explicit actions):

- BOARD: a land unit stepping onto an adjacent friendly transport hull (on water)
  boards it (carrier_id = hull.id) instead of being blocked — no embark tech
  needed, capacity-gated at TRANSPORT_CAPACITY. Handled before find_path (a land
  unit can't path onto water otherwise).
- CARRY: when a transport moves, its loaded units are dragged to the new hex
  (they ride the hull's hex, stacked).
- DISEMBARK: a carried unit steps off onto an adjacent empty land hex (carrier_id
  cleared); rejected otherwise (carried units can't move independently on water).

Boarding/disembarking units are aboard, not embarked (is_embarked stays false).

Tests: transport_board_carry_then_unload (full cycle) + transport_rejects_boarding_when_full;
mc-turn 246 lib + all integration binaries green (the new modes only activate for
units with carrier_id or a transport-at-target, so existing movement is untouched).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 06:07:27 -04:00
Natalie
ba81bcf476 feat(@projects/@magic-civilization): p3-18 P4a — transport data model (keywords + carrier_id)
Foundation for the transport mechanic (owner: build it):
- catalog UnitStats gains `keywords: Vec<String>` (authored on units/<id>.json),
  + `is_transport()` (keywords.contains("transport")) and a TRANSPORT_CAPACITY=2
  constant mirroring the combat.json transport keyword ("carry up to 2 land
  units"). Data-driven — no unit id hardcoded.
- MapUnit gains `carrier_id: Option<u32>` — the hull carrying this land unit
  (None = on map normally). A carried unit rides the carrier's hex, isn't
  independently attackable, and is lost with the carrier.

Test: transport_keyword_detected_data_driven; mc-units lib green. Mechanic
(board/carry/unload + combat-loss) lands in P4b/P4c.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 05:56:54 -04:00
Natalie
416c62da85 docs(@projects/@magic-civilization): 📝 p3-18 — record explicit-embark deletion (P3b) + P4/P5 scope
Auto-embark is now the sole embark model. Remaining: transport (P4, large fresh
Rust mechanic), GDScript mirror + vestigial-FFI trim (P5, needs build host),
end-to-end conquest demo (P6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 05:19:08 -04:00