Commit graph

3728 commits

Author SHA1 Message Date
Natalie
dd31de9acb docs(@projects/@magic-civilization): 🔎 p3-29 — audit: the DRY fix's bulk is event-surfacing for UI parity, not the call-swap
TurnResult.events_emitted is replay-thin (founded/captured/killed/created); live UI needs
city_grew/building_completed/border_expanded/culture_researched/flora_succession. Real p3-29
work: enrich Rust event surface (headless-safe) → surface in dict → GDScript translates →
swap+delete → render-proof. Steps 1-2 carry no live-game risk.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 02:16:39 -04:00
Natalie
1a4c9c7d36 docs(@projects/@magic-civilization): 🏛️ p3-29 — Rail-1 turn unification objective (the real DRY fix)
Owner flagged the duplication: live game runs GDScript turn orchestration (turn_processor.gd
_process_* + EcologyState.tick) while headless runs mc-turn::step — two turn orchestrations.
This session built mc-turn::step into the complete single source of truth; p3-29 is the capstone:
switch turn_manager to GdTurnProcessor.step (bridge already exists at lib.rs:6354), render the
TurnResult for UI, delete the GDScript orchestration. HIGH-STAKES live-game rewrite — needs a
render proof before merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 02:13:36 -04:00
Natalie
e8e58fb278 docs(@projects/@magic-civilization): ☀️🧊 p3-26 B8 — events 9/12 (solar+glacial added)
9 mc-climate event categories live; pandemic/ecological fauna via disease applier; marine via
process_step; magical→G3. Event system effectively complete.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 02:03:23 -04:00
Natalie
8022722a33 feat(@projects/@magic-civilization): ☀️🧊 p3-26 B8 — solar + glacial event categories (9/12)
Two climate disaster categories via the existing magic_heat_delta forcing field (which the
climate physics adds to temperature each step, then decays — so the effect is persistent +
transient like the GDScript duration, no new grid field/physics change):
- solar: global warming — every tile's magic_heat_delta += global_heat.
- glacial (cold): cools a hex disk (magic_heat_delta += negative temp_delta) + dries it
  (moisture_loss), skipping open water. (warm-T5 runaway + river-freeze deferred.)
apply_solar/apply_glacial + dispatch + match arms + tests. mc-climate 65/0.

Events: 9/12 live (wildfire/drought/volcanic/seismic/impact/tsunami/plague/solar/glacial);
pandemic/ecological fauna handled by the disease applier; marine via process_step; magical→G3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 02:03:02 -04:00
Natalie
245a0af95a docs(@projects/@magic-civilization): p3-26 B6 DONE — equipment craft→equip→combat live; loot=B5 parity-optional
B6 complete: B6a (recipe refinement) + B6b core (MapUnit.equipped + combat-read + CraftEquipment
action). Full chain — refine raw→processed → craft (consume materials) → equip → unit fights
harder. Loot drop-on-death/decay (B5) is parity-optional (disabled in the live game too).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 23:46:41 -04:00
Natalie
5288555bdb feat(@projects/@magic-civilization): ⚔️ p3-26 B6b (4/4) — CraftEquipment action (populate path; gear reachable in play)
Closes the equipment loop — units can now obtain gear in headless play:
- ItemCombatBonus extended to a full item def (+ category + materials from production.materials);
  the loader stores ALL items (craftable), not just combat-bonus ones.
- PlayerAction::CraftEquipment { unit_id, item_id } + dispatch::craft_equipment — looks up the
  item def, verifies + consumes its raw materials from strategic_ledger (the refined outputs
  B6a produces — recipe→craft chain), then pushes an EquippedItem onto the unit. Rejects on
  unknown item / insufficient materials.

End-to-end: B6a refines raw→processed → CraftEquipment consumes materials → equipped gear →
B6b combat-read adds attack/defense. Tests: craft consumes+equips, rejects insufficient.
mc-player-api 138/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 23:44:33 -04:00
Natalie
a44ba951bd feat(@projects/@magic-civilization): ⚔️ p3-26 B6b (3/3 boot) — FFI + harness boot the item combat table
- GdPlayerApi::set_item_combat_json — loads item combat bonuses onto GameState.
- player_api_main._apply_item_combat — stamps DataLoader.get_all_items() via the FFI at boot,
  emitting item_combat_api_loaded.

Equipped gear now boosts units in real headless combat (table booted → combat-read live).
gdext compiles. Remaining for full B6b: a Craft/Equip action to populate units' gear + loot
drop-on-death/decay (B5).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 21:15:09 -04:00
Natalie
a3dac211e3 feat(@projects/@magic-civilization): ⚔️ p3-26 B6b (2/3) — equipment combat bonuses (units fight harder with gear)
The combat-read half of equipment:
- GameState += item_combat: BTreeMap<String, ItemCombatBonus{attack,defense}> (#[serde(skip)]
  boot) + load_item_combat_json (sums items.json stats: melee_bonus+ranged_bonus → attack,
  armor_bonus → defense).
- processor::equip_combat_bonus(unit, table) sums a unit's equipped-item bonuses; injected into
  BOTH combat paths (process_pvp_combat formation params + the click/MCTS resolve helper) —
  added to attacker/defender attack + defense.

Safe by construction: unequipped units (empty `equipped`) + empty table → (0,0) → combat
unchanged, so all existing combat tests + the 20-turn golden are unaffected (verified:
mc-turn 284/0). Tests: equip_combat_bonus sums stats / zero for unequipped/unknown.
Remaining (3/3): FFI+harness to boot the item table + a Craft/Equip action to populate gear.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 21:13:16 -04:00
Natalie
4c41ecf66e feat(@projects/@magic-civilization): ⚔️ p3-26 B6b (1/3) — MapUnit.equipped (unit equipment state)
MapUnit += equipped: Vec<EquippedItem> (#[serde(default)]) — items a unit has equipped, to
drive combat bonuses + charge consumption + loot-on-death. mc-state now deps mc-items
(standalone crate, no cycle). Foundation for the combat-read (B6b/2) + Craft action (B6b/3).
mc-state builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 21:04:30 -04:00
Natalie
95e289891e docs(@projects/@magic-civilization): ⚒️ p3-26 B6a DONE — resource refinement live; B6b (equipment+combat) remaining
B6a (mc_city::recipes wired: refine raw→processed into strategic_ledger each turn) complete +
verified. B6b (unit equipment + combat reads gear + loot decay) is the combat-spanning
remainder, inert without combat integration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 20:52:48 -04:00
Natalie
5edd20ced6 feat(@projects/@magic-civilization): ⚒️ p3-26 B6a (2/2) — FFI + harness boot the recipe bundle
- GdPlayerApi::set_recipes_json — stores the recipe bundle JSON on GameState. Mirrors
  set_improvement_defs_json.
- player_api_main._apply_recipes — reads public/resources/recipes/recipes.json (the
  {recipes:[…]} bundle, 12 recipes) at boot and stamps it via the FFI, emitting
  recipes_api_loaded.

Resource refinement now runs end-to-end in real headless play: boot recipes → recipe phase
consumes raw + produces refined into strategic_ledger each turn. B6a (recipe refinement)
complete. gdext compiles; dylib rebuild in progress.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 20:51:11 -04:00
Natalie
dd1a537ab5 feat(@projects/@magic-civilization): ⚒️ p3-26 B6a (1/2) — resource-refinement (recipe) tick in the headless turn
Wires the previously-unwired mc_city::recipes system into the turn:
- GameState += recipes_json (#[serde(skip)] boot) + load_recipes_json (mc-state holds the raw
  bundle JSON; no mc-city dep).
- recipe_phase::process_recipe_phase — per player, loads strategic_ledger into a
  ResourceStockpile, runs tick_recipes over the player's processing buildings (consume raw →
  produce refined), writes the transformed ledger back. Registered in END_OF_TURN_PHASES
  (ecology, healing, improvement_build, recipe_refine).

Refined resources land in strategic_ledger (which trade/economy already use), so it's a real
economic transform, not inert. Tests: refines, idle-on-shortage, no-op-without-recipes.
mc-turn recipe_phase 3/3. Remaining (2/2): FFI + harness to boot recipes.json.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 20:49:04 -04:00
Natalie
45d52c8ce6 docs(@projects/@magic-civilization): 🔎 p3-26 B6 — equipment/crafting scoped (largest subsystem, spans combat)
mc-items (item logic) + mc-city/recipes (resource refinement) exist but unwired; bench has no
ResourceStockpile, units carry no equipped items, no Craft/Equip action, combat ignores gear.
Recorded the 5-step interlocking plan (stockpile → recipe tick → unit equip + Craft → combat
reads gear → loot decay [closes B5]). No clean sub-slice — interlocks across crates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 20:37:22 -04:00
Natalie
14c4de8f85 docs(@projects/@magic-civilization): p3-27 — disease applier complete (o2+lair); ocean-collapse prereqs scoped
Disease applies the full tier spec now (fauna/canopy/tier/o2/lair). Ocean-collapse remains
unwired with precise prereqs recorded (global_fish_stock aggregation + registry/has_tag
reconciliation) — a scoped real task, code+tests exist in isolation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:53:20 -04:00
Natalie
668ab7d152 feat(@projects/@magic-civilization): 🦠 p3-27 — complete the disease applier (o2 depletion + lair kill, no stubs)
Reverses the two deferrals in apply_disease_events — every EventTierData field now applies:
- o2_delta: atmospheric (global) depletion of grid.o2_fraction, once per fired event
  (negative drives toward an anoxic ocean). o2_fraction is grid-level, so it's applied
  after the radius loop, not per-tile.
- lair_kill_chance: an active lair (lair_tier > 0) on an affected tile is wiped
  (lair_tier=0, lair_population=0 — same clear as evolution's lair removal) with the
  configured probability via the deterministic rng.

Disease now applies fauna_loss + canopy_loss + tier_loss + o2_delta + lair_kill_chance —
the full tier spec. mc-ecology events 11/0 (+1 test).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:46:35 -04:00
Natalie
59742674b8 docs(@projects/@magic-civilization): p3-26 B3 — improvement subsystem DONE (build + yield live headless)
Full chain wired + tested: BuildImprovement → pending (build_turns) → build-tick →
city_improvements → process_improvement_yields. State (cb451832e) + logic (f4e9d0211) +
FFI/harness (5d12b4bbe).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:42:32 -04:00
Natalie
5d12b4bbee feat(@projects/@magic-civilization): 🏗️ p3-26 B3 (3/4) — FFI + harness boot the improvement defs
Runtime wiring so improvements actually build + yield in a real headless game:
- GdPlayerApi::set_improvement_defs_json — loads the improvement defs (JSON array of
  {id, build_turns, yields}) onto GameState. Mirrors set_ecology_species_json.
- player_api_main._apply_improvement_defs — stamps DataLoader.get_all_improvements() via the
  FFI at boot (after load_state_json), emitting improvement_defs_api_loaded.

With this, the full B3 chain is live: BuildImprovement → pending (build_turns) → build-tick
completes → city_improvements → process_improvement_yields folds food/production in. gdext
compiles; dylib rebuild in progress.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:42:08 -04:00
Natalie
f4e9d02115 feat(@projects/@magic-civilization): 🏗️ p3-26 B3 (2/4) — improvement subsystem logic (build-tick + dispatch + yields)
De-stubs the headless improvement pipeline:
- improvement_phase::process_improvement_build_phase — ticks pending_improvements down,
  completes at 0 → appends the improvement id to the owning city's city_improvements (so it
  yields). Registered in END_OF_TURN_PHASES (ecology, healing, improvement_build).
- dispatch::build_improvement — replaces the stub: validates via the action gate, finds the
  worker's owning city (CityState.owned_tiles, else first city), queues a PendingImprovement
  with turns_remaining from improvement_defs[id].build_turns. (improvement_id, previously
  dropped by the dispatcher, is now plumbed through.)
- apply_end_turn repopulates the fresh processor's improvement_yield_table from
  state.improvement_defs each turn, so process_improvement_yields actually folds food/
  production into cities (was a no-op — table never loaded in real play).

Tests: build-tick (3), dispatch queue (1), registry order. mc-turn 279/0, mc-player-api 136/0.
Remaining (3/4): FFI + harness to boot improvement_defs from public/resources/improvements.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:40:08 -04:00
Natalie
cb451832e0 feat(@projects/@magic-civilization): 🏗️ p3-26 B3 (1/4) — improvement subsystem state model
Groundwork for the headless improvement subsystem (wired in the next increments):
- PlayerState.pending_improvements: Vec<PendingImprovement> (#[serde(default)]) — tile
  improvements under construction.
- GameState.improvement_defs: BTreeMap<String, ImprovementDef> (#[serde(skip)] boot) +
  load_improvement_defs_json (parses public/resources/improvements/*.json → build_turns +
  food/production yields).
- ImprovementDef + PendingImprovement structs.

mc-state 14/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:31:26 -04:00
Natalie
57725d0088 docs(@projects/@magic-civilization): 🔎 p3-26 B3 — improvement subsystem is FULLY absent headless (re-scoped)
Verified: improvement_yield_table never populated in apply_end_turn (yields no-op),
handle_build_improvement is a stub (no placement, improvement_id dropped), no GameState
improvement data. B3 is a full subsystem build (state+boot+FFI+handler+build-tick+yields),
not a build-tick tweak. Precise ecology-phase-shaped plan recorded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:28:47 -04:00
Natalie
00a349a388 docs(@projects/@magic-civilization): 🐟 p3-27 — marine ecology core confirmed live (ticks via process_step)
Per-tile fish/reef/mangrove dynamics already run inside EcologyEngine::process_step (headless
ecology phase); ocean_dead_fraction via climate; no live MarineHarvestScript remains. Global
ocean-collapse (tick_ocean_state) is the one unwired refinement (needs a populated
BiomeTagRegistry).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:12:24 -04:00
Natalie
05b550232b docs(@projects/@magic-civilization): 🦠 p3-27 — fauna disease applier done (plague/pandemic mortality live)
EcologyEngine::apply_disease_events strikes fauna populations in the ecology phase from the
boot-loaded event configs. Remaining bio-events: marine (fish/reef), lair_kill_chance, o2 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:06:56 -04:00
Natalie
daf00bbee8 test(@projects/@magic-civilization): 🔧 re-pin 20-turn golden after the B2 healing phase
The full_turn_golden values were frozen 2026-05-13, before the p3-26 B2 healing phase joined
step()'s end-of-turn sequence. Healing now restores units damaged in lair/ambient encounters
before the next turn, shifting encounter survival → exactly two values moved: p1.gold 320→319
and p0 unit count 13→14 (p0.gold, culture, pop, city counts all unchanged). Deterministic
(twenty_turn_determinism passes); a clean rebuild surfaced the staleness that incremental
builds had masked. Sanctioned golden update per the test's own "sequencing changes must update
these" contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:04:06 -04:00
Natalie
c0c1652034 docs(@projects/@magic-civilization): 🏛️ p3-28 — modular turn architecture objective (cycle + registry done)
Records the foundation refactor: dep-cycle break (5ee312e45) + end-of-turn phase registry
(af41ea10a) done; boot-config DRY (3 layers → 1 Rust-native loader) + registry-widening
remaining.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:16:56 -04:00
Natalie
a3e26c6ed2 chore(@projects/@magic-civilization): sync Cargo.lock after mc-ecology dep moves
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:16:24 -04:00
Natalie
af41ea10a9 refactor(@projects/@magic-civilization): 🧩 unify turn phases — ecology into mc-turn + end-of-turn phase registry (OCP)
With the dependency cycle broken (5ee312e45), mc-turn can finally depend on mc-ecology, so
the ecology tick moves out of its mc-player-api::apply_end_turn special-case and into the turn
itself — all phases now live in one crate.

Introduces `mc-turn::sim_phases` — the end-of-turn world-sim phases declared as an ordered DATA
registry (`END_OF_TURN_PHASES = [ecology, healing]`, uniform `fn(&mut GameState)`), run after
climate inside step(). Extending the living world (marine, fauna disease, …) is now one line in
the registry + a phase module — no edit to step()'s body, sequence visible in one place.

- Moved ecology_phase.rs → mc-turn; added mc-turn → mc-ecology dep.
- step(): replaced the direct healing call with `run_end_of_turn_phases` (ecology → healing).
- Removed from mc-player-api: the apply_end_turn ecology call, module, file, and mc-ecology dep.

mc-turn 276/0 (incl. ecology + sim_phases tests), mc-player-api 135/0. Behavior identical
(ecology still ticks once per turn, now inside step). Dylib rebuild pending (functionally
equivalent; FFI surface unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:16:12 -04:00
Natalie
5ee312e452 refactor(@projects/@magic-civilization): 🔗 break the mc-turn↔mc-ecology dependency cycle at its root (DIP)
mc-ecology depended on the entire mc-mapgen crate solely to reach `mc_mapgen::seed::*` —
which is itself just a 13-line re-export of `mc_core::seed`. So a low-level ecology crate
pulled in the high-level map generator to use a foundational RNG utility that already lives
in mc-core (the WORLDGEN_RNG PCG64/SeedDomain/derive contract).

Repoint fauna_select.rs + flora_select.rs to `mc_core::seed` directly and drop the mc-mapgen
dependency. This cuts the mc-ecology → mc-mapgen edge, breaking the
mc-turn → mc-ecology → mc-mapgen → mc-turn cycle that forced the ecology tick out of
mc-turn::step into mc-player-api::apply_end_turn.

Proven: `cargo check -p mc-turn` with an mc-ecology dep added now compiles (no cyclic error);
reverted the probe pending the TurnPhase-registry step that will move ecology back into the
unified phase list. mc-ecology 338/0 — determinism intact (seed code is byte-identical, was
already mc-core via the re-export).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:07:58 -04:00
Natalie
135a0e81b9 docs(@projects/@magic-civilization): 📊 p3-26 B8 7/12 (plague) + p3-27 fauna-disease-is-unwired finding
- B8 events now 7/12 (plague terrain blight added).
- Recorded that mc_ecology's disease system is config-only (no applier, only a validate-bin
  caller) — fauna plague/pandemic are a feature to write, not just wiring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:50:59 -04:00
Natalie
d14ba0b006 feat(@projects/@magic-civilization): 🦠 p3-26 B8 — plague (terrain blight) event category
Seventh event category, the terrain side of plague (GDScript process_plague): on
target_terrain tiles in a hex disk, drop quality by tier_loss (min 1) + downgrade the biome
per terrain_downgrade (e.g. enchanted_forest → forest). apply_plague + dispatch_plague +
match arm + tests. mc-climate 63/0. 7/12 categories live.

Note: the FAUNA side of plague/pandemic (population mortality) is NOT this — that belongs to
mc_ecology's disease system, which is currently config-only (load_event_categories + structs,
no apply fn, only caller is disease_validate bin). Recorded as a real gap in p3-27.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:50:36 -04:00
Natalie
6530233048 docs(@projects/@magic-civilization): p3-27 — ecology population tick done (headless biosphere lives)
EcologyEngine now ticks every headless turn (apply_end_turn), seeds genesis + persists via
continuation-JSON, FFI + harness wired. Remaining: confirm flora-succession coverage, marine
ecology port, bio-targeting events.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:18:03 -04:00
Natalie
2a0777e183 feat(@projects/@magic-civilization): 🦌 p3-27 — FFI + harness to boot the headless biosphere
Runtime wiring so the ecology phase actually ticks in a real headless game (was a no-op
until the species library was supplied):
- GdPlayerApi::set_ecology_species_json (api-gdext) — loads the fauna species library (JSON
  array of per-species file contents) onto GameState. Mirrors set_events_config_json; call
  after load_state_json (the field is #[serde(skip)]).
- player_api_main._apply_ecology_species — reads public/resources/ecology/fauna/species/*.json
  into an array + stamps it via the FFI at boot (right after _apply_events_config), emitting
  ecology_species_api_loaded. Mirrors the live EcologyState species load.

gdext compiles clean. Dylib rebuild in progress.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:17:38 -04:00
Natalie
b984143e60 feat(@projects/@magic-civilization): 🦌 p3-27 — living biosphere ticks in the headless turn
The ecology engine ticked only in the live game (GdFaunaEcology); the headless turn had no
living fauna. Now apply_end_turn drives mc_ecology::EcologyEngine every turn:
- process_ecology_phase (mc-player-api/ecology_phase.rs): build engine + species library from
  boot-supplied fauna JSONs, restore populations from the persisted continuation state (or
  seed_initial genesis on the first tick), process_step (mutates grid: emergence, population
  dynamics, flora succession), then re-serialize the continuation state. Deterministic from
  map_seed.
- Lives in mc-player-api, NOT mc-turn::step — mc-turn → mc-ecology → mc-mapgen → mc-turn is a
  dependency cycle; the orchestration layer (apply_end_turn, right after step) avoids it.
- GameState += ecology_species_json (#[serde(skip)] boot-loaded fauna JSONs) + worldsim_state_json
  (#[serde(default)] opaque continuation state — persists across the fresh-per-turn processor
  AND save/load, matching the live worldsim_state save payload) + load_ecology_species_json.

Reuses the existing engine save/restore (continuation_state / restore_continuation_state) — no
new ecology logic, just headless wiring. No-op until the species library is boot-loaded (the
FFI + harness wiring is the next slice). mc-player-api 138/0 (+3 ecology tests), mc-state green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:15:12 -04:00
Natalie
b8d2a1f20e docs(@projects/@magic-civilization): 🦌 p3-27 — concrete biosphere-headless implementation design
Researched the real ecology/flora crate APIs and recorded a ready-to-build spec: reuse
EcologyEngine (process_step + continuation_state save/restore) + EcologyConfig::from_json;
persist the opaque continuation-JSON on GameState (worldsim_state_json) + boot-load the config
bundle (#[serde(skip)] + FFI), mirroring the climate/events pattern. Ecology is already ticked
in GdFaunaEcology for the live game; the headless TurnProcessor just needs the same wiring.
7-step plan + test strategy captured.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:03:54 -04:00
Natalie
f8eadc9119 feat(@projects/@magic-civilization): 😊 p3-26 B1 — surface computed happiness_pool in view_json
project_resources hardcoded happiness_pool: 0 (a stub from before the happiness phase
existed). Now that process_happiness_phase computes player.happiness each turn, surface it
in the PlayerView so the headless view exposes it end-to-end (compute → projection → view).
mc-player-api 135/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:50:30 -04:00
Natalie
0752a51fb6 docs(@projects/@magic-civilization): p3-26 — B1+B2 done, B8 events 6/12 (parallel batch integrated)
Parallel migration batch (2 worktree agents) fully integrated + test-green:
- B1 happiness/golden-age + B2 healing → mc-turn (d5729d67c, 271/0).
- B8 events 6/12 → seismic/impact/tsunami added (afa7613fd, mc-climate 61/0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:47:21 -04:00
Natalie
d5729d67ce feat(@projects/@magic-civilization): 😊 p3-26 B1+B2 — happiness/golden-age + healing in the headless turn
Migrates two live-game-only per-turn subsystems into mc-turn (parallel agent port,
integrated by file-extraction onto current main — worktree forked from a stale base, so a
merge was unsafe; new files extracted clean, shared-file edits re-applied manually, API
drift fixed):

B1 happiness + golden age (happiness_phase.rs):
- process_happiness_phase(state) — per player, assembles HappinessInput (city count,
  citizens, luxuries, building bonus, growth tier) → mc-happiness → writes happiness pool +
  status, advances the golden-age meter (progress/active/turns/count). Mirrors the live
  _process_golden_age. Wired into step() after the per-player economy loop.
- Drift fix: p3-24 added building_happiness_effects/happiness_per_city_effects (Vec<i32>) to
  HappinessInput; bench leaves them empty (scalar building_happiness carries the bridge sum).

B2 unit + city healing (healing.rs):
- process_healing_phase(state) — units regen by territory class (garrison 20 / fortified-
  friendly 15 / neutral 10), cities heal toward max_hp. Mirrors _process_healing +
  _process_city_healing. Wired into step() after climate.

State: 9 new #[serde(default)] PlayerState fields (happiness, happiness_status, growth_tier,
owned_luxuries, golden_age_active/turns/progress/count, building_happiness) — all
backward-compat. mc-happiness promoted dev-dep → dep.

mc-turn 271/0 (incl. 16 new tests); mc-state green. Documented bench limitations: war-
weariness + enemy-territory healing + city siege-suppress need a tile-owner index (live
bridge writes them).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:46:42 -04:00
Natalie
afa7613fd8 feat(@projects/@magic-civilization): 🌍 p3-26 B8 — seismic + impact + tsunami event categories
Three geological natural-event categories (parallel agent port, extracted file-only onto
current main — the agent worktree forked from a stale base, so a branch merge was unsafe;
events.rs is a clean superset verified to compile + test against current HEAD):
- seismic — shifts tile elevation over a radius (with falloff).
- impact (asteroid) — T1-4 crater (biome/elevation/moisture/quality + local heat + aerosol);
  T5 extinction (global aerosol injection). Magic/anchor/resource-spawn deferred (Game-3).
- tsunami — floods coastal land (moisture/quality/reef_health), skips open water.

Each: apply_X + dispatch_X + match arm in process_events + tests. Configs already present
(seismic/impact/tsunami.json) + auto-loaded via the headless harness, so they dispatch in
process_climate_phase with no extra wiring. mc-climate 61/0; mc-turn builds.
6/12 categories live (wildfire/drought/volcanic/seismic/impact/tsunami).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:31:19 -04:00
Natalie
2a360af22a docs(@projects/@magic-civilization): 🗂️ p3-26 full migration backlog + p3-27 biosphere-headless objective
Recorded the verified live-turn-vs-headless migration sweep:
- p3-26: full backlog B1-B8 (happiness/golden-age, healing, improvements-tick, government,
  loot-decay, equipment, per-building-queues, remaining 9 event categories) — each grepped
  to 0 hits in mc-turn.
- p3-27 (new): biosphere headless port — ecology population tick + flora succession +
  marine ecology (the bio simulators exist as crates + tick in the live game but not the
  headless turn); underlies the plague/pandemic/marine events.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:20:17 -04:00
Natalie
e4a3808a19 docs(@projects/@magic-civilization): 📊 p3-26 gap 2 — honest remaining-category breakdown
3 event categories live (wildfire/drought/volcanic) on existing grid fields. Recorded the
real shape of the rest (not trivial pattern-repeats):
- seismic/impact/tsunami: geological terrain ops (elevation/crater/coastal) — partially
  portable with the current grid.
- solar/glacial: need the Rust climate physics to consume new solar_forcing/glacial_forcing
  fields (injecting a field without physics reading it is inert).
- plague/pandemic/marine: need fauna/marine subsystem integration (fauna population death,
  fish_stock/reef) — overlaps the marine_harvest port (gap-1 tail).
- magical: Game-3 deferred.

So gap 2 = a complete, verified framework + 3 categories; finishing the rest is sequenced
real work, not boilerplate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:57:51 -04:00
Natalie
8fd906241c feat(@projects/@magic-civilization): 🌋 p3-26 gap 2 — volcanic eruption category
Third (and flagship) natural-event category, ported from GDScript process_volcanic:
apply_volcanic — center → volcano (quality 1), scorch disk turns non-water tiles to
scorched_terrain (desert, drier, quality 1), sulfate_aerosol injected in aerosol_radius
(climate physics converts to cooling next tick). dispatch_volcanic resolves the tier
config + picks a non-water center. Added to process_events dispatch. (Anchor/resource
spawns are magic → Game-3 deferred.)

Test: apply_volcanic_erupts_scorches_and_injects_aerosol (volcano center, desert scorch,
water spared, aerosol injected). mc-climate events 9/9. 3/12 categories live
(wildfire, drought, volcanic).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:55:18 -04:00
Natalie
750824fbbb docs(@projects/@magic-civilization): p3-26 gap 2 — wildfire+drought events LIVE in the headless sim (verified)
Verification milestone for the natural-events port:
- Dylib rebuilt — set_events_config_json FFI confirmed present (strings); boot GUT 750/0
  (dylib loads, GdPlayerApi works, no regression).
- Full path proven: harness loads event configs (DataLoader.get_ecological_events →
  set_events_config_json) → mc-turn climate phase runs process_events → wildfire/drought
  fire + reshape terrain (climate_phase_fires_natural_events deterministic).

Gap 2 now: deterministic core + config loader + dispatch + 2 categories (wildfire,
drought) live + wired end-to-end. Remaining: the other 10 categories + surfacing fired
events in the turn result/view + era-based severity cap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 11:09:24 -04:00
Natalie
60e941caa5 feat(@projects/@magic-civilization): 🏜️ p3-26 gap 2 — drought category + shared tile-picker
Second natural-event category, ported from GDScript process_drought:
- apply_drought: reduce moisture (min 0) in a hex disk, skipping water tiles
  (mc_core::grid::biome_registry::has_tag(.., IsWater)). Returns tiles affected.
- dispatch_drought: resolve tier (radius/moisture_loss) + pick a non-water center.
- Extracted pick_matching_tile (shared deterministic tile selection); refactored
  dispatch_wildfire to use it. process_events now dispatches wildfire + drought.

Test: apply_drought_dries_land_skips_water (land moisture drops, ocean untouched).
mc-climate events 8/8. Remaining categories (volcanic/seismic/tsunami/plague/marine/…)
follow the same pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 11:01:50 -04:00
Natalie
64ea08b7ce feat(@projects/@magic-civilization): 🔌 p3-26 gap 2 — load event configs in the headless harness (events now LIVE)
Activates natural events in real headless games:
- GdPlayerApi.set_events_config_json FFI → state.load_events_config_json.
- player_api_main.gd::_apply_events_config stamps DataLoader.get_ecological_events()
  (the merged {category: config} map) onto GdPlayerApi after load_state_json (same
  #[serde(skip)] re-stamp pattern as resource_categories / catalogs).

So a headless self-play game now loads the 12-category event configs → mc-turn's climate
phase fires wildfire (other handlers to follow). The firing mechanism is already proven
deterministically (climate_phase_fires_natural_events). FFI compiles; GDScript additions
gdlint-clean. Dylib rebuild needed for the live boot to call the new FFI (next).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:55:34 -04:00
Natalie
9bc2b2c34b feat(@projects/@magic-civilization): p3-26 gap 2 — wire natural events into the headless turn
Events now fire in headless self-play (when configs are loaded):
- mc-state: GameState.events_config (BTreeMap<String, serde_json::Value>, #[serde(skip)],
  raw category→config so mc-state needs no mc-climate dep) + load_events_config_json loader
  (no-clobber). Tested.
- mc-climate::events: EventCategoryConfig::from_raw + configs_from_raw (build typed configs
  from the in-memory state map); load_event_configs refactored to share from_raw.
- mc-turn::process_climate_phase: after the climate tick + before weather (live-game order),
  runs events::process_events on the grid when events_config is non-empty (max_tier 10;
  era-cap is a follow-up). No-op when configs absent.

Test: climate_phase_fires_natural_events — an always-fire wildfire config on a forest grid
transforms forest→grassland during the climate phase. mc-turn 338/0, mc-state 13/0,
workspace cargo check clean (new GameState field broke no literals).

Next (makes it LIVE in real games): GdPlayerApi.set_events_config_json FFI + player_api_main
+ bench wiring to load public/resources/events/*.json; then the remaining category handlers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:52:09 -04:00
Natalie
b07e1dd367 feat(@projects/@magic-civilization): 🌋 p3-26 gap 2 — natural-event dispatch (process_events) + wildfire end-to-end
The per-turn dispatch ties the events core together, matching GDScript
ecological_events.process_events:
- turn_seed = seed*1000 + turn; per category in CATEGORY_ORDER (channel = index*10+10),
  gate on category_fires(base_frequency), roll_severity (era-capped), apply the effect.
- Wildfire implemented end-to-end: dispatch_wildfire resolves the tier config (radius/
  moisture_loss/becomes from raw JSON), picks a deterministic forest center, calls
  apply_wildfire. Returns FiredEvent{category,tier,center,affected}. Other 11 categories
  are recognised (gate+severity) with effect handlers to follow.

Test: an always-fire wildfire config on a forest grid → fires once, burns forest →
grassland, deterministic for (turn,seed). mc-climate events 7/7.

Tile-pick is Rust-deterministic (internal determinism; the dispatch GATE matches GDScript
bit-for-bit). Next: wire process_events into the mc-turn climate phase (config carried on
GameState) so wildfires fire in headless self-play, then the remaining category handlers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:45:33 -04:00
Natalie
fa93e59425 feat(@projects/@magic-civilization): 🔥 p3-26 gap 2 — wildfire effect (apply_wildfire) ported
The first per-category event EFFECT, pure + testable. apply_wildfire burns forest tiles in
a hex disk around a center (offset coords via offset_to_axial/hex_spiral/axial_to_offset):
transforms matching terrain to `becomes`, drops quality (min 1) + moisture (min 0); returns
tiles burned. Mirrors GDScript process_wildfire's per-tile effect.

Test: a forest patch + apply_wildfire(radius 2, becomes grassland, quality_loss 2) → forest
→ grassland, moisture/quality dropped, non-forest tiles untouched. mc-climate events 6/6.

Next: the dispatch (process_events — category_fires gate + roll_severity + deterministic
center pick → apply_wildfire) + config-on-GameState + wire into the mc-turn climate phase.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:42:29 -04:00
Natalie
4e82b322cb feat(@projects/@magic-civilization): 📋 p3-26 gap 2 — event-config loader (mc-climate::events)
Next brick of the events port: load per-category configs from the canonical JSON
(Rail-2, nothing hardcoded).

- EventCategoryConfig { base_frequency, severity_weights, raw } — typed dispatch inputs +
  the full JSON kept in `raw` so each per-category handler reads its own fields (tiers,
  target_terrain, becomes, aerosol_strength) without modeling all 12 shapes up front.
- load_event_configs(dir) reads public/resources/events/<category>.json (category =
  filename stem; skips *.schema / cross_triggers / events). Test parses the real
  wildfire.json (base_frequency 0.04, severity_weights, target_terrain ∋ "forest").

mc-climate events 5/5. Next: dispatch (process_events using category_fires + roll_severity)
+ per-category handlers (wildfire first — burn forest in radius, transform biome) + wire
into the mc-turn climate phase. Tile-picking will use a Rust-deterministic RNG (the
headless sim needs internal determinism, not byte-match with the live game's Godot RNG;
the dispatch GATE already matches GDScript bit-for-bit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:39:04 -04:00
Natalie
9ccc7e10ff feat(@projects/@magic-civilization): 🎲 p3-26 gap 2 (start) — deterministic event core ported to mc-climate::events
First slice of the natural/"apocalyptic" events port (M3). The deterministic primitives
every category depends on, ported from GDScript ecological_event_utils:

- hash_noise(x,y,seed) = frac(sin(x*127.1+y*311.7+seed*74.3)*43758.5453), f64 — verified
  to match the LIVE GDScript game bit-for-bit (ran it: hash_noise(10,0,1000) =
  0.67791910066535). The headless sim must match the game, NOT the TS web guide (whose
  Math.sin diverges on these large arguments — a pre-existing game-vs-guide gap, not a
  port bug; the old comment's "0.1270 from TS" golden was misleading).
- roll_severity(weights, turn_seed, channel, max_tier) — weighted tier roll with era cap.
- category_fires(base_frequency, channel, turn_seed) — the per-category dispatch gate.

4 cargo tests (GDScript-golden determinism, channel separation, severity bounds + cap,
fire gate). Source corrected: .messy is gone — the port source is the live
ecological_events.gd + handlers_a/b + public/resources/events/*.json. Next: event-config
structs/loading + dispatch + per-category handlers (wildfire first) + turn wiring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:35:14 -04:00
Natalie
1bdad8e497 feat(@projects/@magic-civilization): 🌪️ p3-26 gap 1 (cont.) — weather + climate effects (unit HP) in the headless turn
Extends the headless climate phase from physics-only to the full per-turn chain mirroring
the live game's _process_climate (climate → weather → effects):

- process_climate_phase now: ClimatePhysics::process_step → weather::derive_events
  (storms/heat-waves/blizzards, default thresholds = live GdWeatherPhysics) →
  apply_climate_effects.
- apply_climate_effects (extracted, testable): runs climate_effects::apply (tile effects +
  per-unit hp_loss) then fans hp_loss onto MapUnit.hp as max(0, hp - hp_loss) — exactly
  climate_effects.gd. movement_penalty surfaced but not applied to units (matches live).

Tests: apply_climate_effects_fans_hp_loss_onto_units (deterministic — unit in heat-wave
radius loses HP, unit outside unharmed) + the determinism test; mc-turn 337/0, no
regression. Gap 1 remaining: marine_harvest (ocean_dead_fraction → climate).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:25:33 -04:00
Natalie
e9409f22cd feat(@projects/@magic-civilization): 🌡️ p3-26 gap 1 (start) — climate physics ticks in the headless turn
The headless mc-turn ran no climate (live-game GDScript only). Now it does:
- mc-turn deps mc-climate (no cycle; mc-climate is lower-level).
- TurnProcessor::process_climate_phase ticks mc_climate::physics::ClimatePhysics once per
  round on state.grid (process_step(grid, turn, map_seed, dt=1.0)). Default config
  ("{}"/"[]"/"{}") matches the ecology bench; the grid carries climate state across turns;
  fresh-processor-per-turn is safe (physics is the operator, grid is the state). No-op
  without a grid.
- Called in step() right after fauna (world-level end-of-round phase).

First slice of gap 1 — temperature/aerosol/precipitation now evolve on the live grid in
self-play. Still to come: the weather + climate_effects (unit HP) + marine_harvest chain.
Verified: climate_phase_ticks_grid_deterministically (determinism + no-grid no-op);
mc-turn 336/0 (no regression — climate phase runs in every step() with a grid).

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