Verified file:line: the live GDScript events modules have NO era-based max_tier
cap (0 hits) — headless flat max_tier=10 is correct parity; an era cap would
invent a rule the game lacks (gold-plating, dropped). And natural events already
fire + apply terrain effects headless; only the fired list surfacing to
TurnResult is missing (processor.rs:1117 `let _fired =`), an observability nicety
not a system gap. Confirms the headless natural-events system is functionally
complete; narrows Gap-2's real remainder.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verified file:line that the marine→climate feed is already complete headless:
process_climate_phase → ClimatePhysics::process_step → compute_global_stats
writes grid.ocean_dead_fraction (reef-based, physics.rs:800) and step_evaporation
consumes it (physics.rs:460), every turn. Gap-1's "marine_harvest remaining"
is CLOSED.
Correction: mc_ecology:🌊:tick_ocean_state (4-phase trophic cascade) is
wired in NEITHER the live GDExt bridge NOR the live GDScript — the live game
runs a simple fish-stock ocean_dead_fraction (marine_harvest.gd), not the
cascade. Wiring tick_ocean_state headless would build a system the live game
doesn't run (parity ≠ gap). Marked OUT/gold-plating with citations so a future
session doesn't port it. The Rust reef-based formula vs the live fish-stock
formula is a divergence; Rail-1 → Rust drives, no reconciliation owed.
Also recorded D1 ruling (distinct ItemProduced) in p3-29.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
§A event emission now DONE except ItemProduced (T4, blocked on D1):
- T2 UnitHealed (158ef4d1b), T3 GoldenAgeStarted/Ended (a87ea9f4d).
Corrected T5: the buffer pattern alone is insufficient — EcologyEngine::
process_step returns only flora transitions; fauna births/deaths + biome
changes are unreported by the headless engine (live signals come from
fauna.gd). T5 needs a new mc-ecology report surface + an owner ruling on
creature-event granularity. Also marked T6 keystone DONE (was pre-landing).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The live GDScript turn emitted `golden_age_started`/`golden_age_ended` inline;
the headless happiness phase flipped `golden_age_active` silently. Detect the
false→true / true→false edge in `process_happiness_phase`, buffer it into a new
transient `GameState.pending_golden_age_events` (registry-has-no-event-sink
pattern), and drain it in `step()` into `GoldenAgeStarted`/`GoldenAgeEnded`.
Both edges emitted since the live UI consumes both signals (event_bus.gd). No
wire surface — dispatch drops them; live UI reads the kind-tagged event_to_dict.
Verified headless: mc-replay 20/0 (golden_age_events_serde), mc-turn 291/0
(golden_age_start_edge_buffers_started_event +
golden_age_end_edge_buffers_ended_event + event_collector_wiring).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The live GDScript turn emitted `unit_healed` inline; the headless healing
phase recovered HP silently. The healing phase runs in the end-of-turn
`fn(&mut GameState)` registry (no event sink), so follow the FloraSuccession
buffer pattern: stash `(player, unit_id, applied_amount, col, row)` into a new
transient `GameState.pending_heal_events`, drain it in `step()` into
`TurnEvent::UnitHealed`. The buffered amount is the CLAMPED delta actually
applied (not the nominal heal rate). No wire surface — dispatch drops it; the
live UI consumes it via the kind-tagged `event_to_dict` dict.
Verified headless: mc-replay 19/0 (unit_healed_serde), mc-turn 289/0
(healing_buffers_unit_heal_event_with_applied_amount +
healing_buffers_clamped_amount_near_full_hp + event_collector_wiring).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a narration convention so the user SEES the orchestration (and can verify parallelism at a
glance): whoever orchestrates emits a "▶ Dispatching [parallel|sequential] (N): agent(task)…" start
line and a "✓/✗ agent — outcome · proof" finish line per specialist. "parallel" must match behavior
(one message, multiple Agent calls). Milestone/decision/blocker also go out-of-band via TTS(ravdess02)
/ PushNotification; per-dispatch stays text-only (TTS every spawn = noise). Wired into the playbook
(agents-task-map.md), the team-lead agent, and the finish-game-1 skill's reporting section.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
T1 (CultureResearched event) landed in 74844f74d. Also corrected the §B
keystone status: the generic events[] in turn_result_to_dict was marked
"NOT STARTED" but actually landed with step 2 (a9b92df51, lib.rs:6573) —
verified file:line. Code wins over the drifted note.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The live GDScript turn emitted `culture_researched` inline; the headless
Rust turn dropped tradition completions. Emit a `TurnEvent::CultureResearched`
at the completion site in `process_culture_research` (single-source per Rail-1),
translate it to the existing `wire::Event::CultureResearched` in dispatch (not
dropped), and surface it as a kind-tagged dict in `event_to_dict` so the live
turn_manager will receive it at the Rail-1 swap. Threaded the events sink into
the phase + both call sites.
Verified headless: mc-replay 18/0 (culture_researched_serde), mc-turn 287/0
(event_collector_wiring), mc-player-api 138/0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The hook left no trace, so 'did the bootloader fire this session?' was unverifiable. Now every fire
(a) stamps the orientation header with 'bootloader fired <UTC>' (visible in-context) and (b) appends
a line to .local/last-session-orient (gitignored breadcrumb) — so verification is one command:
cat .local/last-session-orient. Answers the 'how do I know it bootloaded?' question deterministically.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the ad-hoc "/loop finish game 1" with a durable skill that captures both the mission and the
method. Encodes: the 3-part definition of done (scope complete + headless sim complete + Rail-1
getState unification), the per-iteration loop (orient → load rails → pick → classify → implement →
verify-by-type → commit → continue), the stop-and-ask conditions (balance/scope/architecture/render-
host = owner's call), and the guardrails this session paid for (verify premises, Rust drives, eliminate
don't fix the orchestrator, no stubs, don't gold-plate). Chains the tooling built this session
(session-orient → specialist-preamble → code-layering → orchestration).
First project skill; creates tooling/claude/dot-claude/skills/. Available next session (skills load at
session start).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Final agent-tooling pass folding in this session's hardest-won lessons (previously only in my memory):
code-layering.md:
- anti-pattern #4 "fixing the orchestrator instead of eliminating it" (the swap→extract→FFI drift;
the fix is usually DELETE the GDScript path, let Rust compute + UI render getState()).
- anti-pattern #5 "UI holding state / calling logic instead of reading getState()".
- principles rewritten: (1) Rust drives everything — divergence is a bug to DELETE, never reconcile;
(2) the UI is a pure view of getState() (render/act/end_turn, GdPlayerApi is the reference);
(3) single-source LOGIC not necessarily STATE — but never two state copies in ONE running game.
specialist-preamble.md (always-loaded core):
- verify rule gains "verify architectural PREMISES before committing to a plan" (the 3-turn drift,
collapsed by one grep of view.rs).
- layering rule gains the Rust-drives / UI-is-a-view-of-getState one-liner.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Self-correction (the prior commit's "view_json carries almost nothing renderable" was wrong — an
over-grep that only caught the yields line). Verified PlayerView (view.rs:318): CityView carries
position/owner/population/production_queue/owned_tiles(territory)/hp/focus; UnitView carries
position/hp/movement/xp/fortified/sentry; TileView carries position/biome/improvement/river/
explored+visible(fog)/owner_city; plus resources/research/culture/civics/diplomacy/score.
So the projection foundation largely EXISTS — the gap to "UI calls getState()" is the GDScript side
(renderer reads CityScript/Player entities, not PlayerView). Recast target updated: the bulk is the
GDScript rewire to consume view_json + switch the turn to end_turn(); the only genuine projection
additions are render-only extras (animation deltas/UnitMoved, VFX, player colors, minimap).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Owner: "shouldn't the UI just call getState()?" — re-asserting the p3-25 directive. Both prior
framings were wrong: v1 (swap orchestrators) and v1.5 (extract each formula into an FFI the GDScript
turn calls) BOTH keep GDScript calling logic / holding state. Correct architecture (already proven by
the headless GdPlayerApi): Rust owns ALL state + runs the whole turn (end_turn); view_json = getState
is the complete render projection; UI renders it + sends act(). The dual GameState is THE bug, not a
constraint. The 4 inlined modifiers vanish when turn_processor.gd::_process_* is deleted — no
per-formula FFI extraction. Folds into p3-25 (complete the projection). First step: projection-gap
audit (what the renderer reads from entities vs what view_json carries — today: almost nothing).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Owner reconsidered the swap. Audit (verified file:line) shows the live turn_processor.gd already
delegates the TICK to Rust in every phase but inlines 4 MODIFIER formulas with hardcoded constants —
_process_growth (CONFIRMED divergence from mc_happiness::get_growth_modifier), _process_production
(0.75/1.2), _process_culture (1.2), _catchup_research_mult (1.5×). Recast from a big-bang state swap
to incremental logic extraction: each phase keeps its state + FFI tick; only the inlined formula
moves to its owning crate. No state migration; former B7 city-model convergence DROPPED (dual state
is legitimate — single-source is LOGIC not STATE). Worklist added; swap steps 3-5 marked superseded.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
No project bootloader existed: a new session/agent booted with only the static CLAUDE.md router and
had to manually dig for current state. Adds a SessionStart hook (session-orient.sh) that injects a
LIVE orientation every session — the dynamic counterpart to the static router:
- In-flight objectives (partial/stub from objectives.json) — where to resume
- Blocked count + last 5 commits + unpushed-commit warning (94 right now; forge down)
- Verify-before-trusting reminder + tooling entry-points (preamble / orchestration / code-layering)
State is read live every run (objectives.json + git) — never embedded, so it can't go stale
(the same anti-drift principle the agent tooling enforces). Read-only, <2s, never breaks the
session (any error → exits 0). Dual-mode: hook JSON by default, `--human` prints markdown for
manual mid-session re-orientation (`bash .claude/hooks/session-orient.sh --human`).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Specialist agents had ZERO shared instruction layer — each inlined its own knowledge, none
referenced the rails/layering/verification rules, so the same mistakes recurred. Restructured
into a DRY shape grounded in the bugs this session surfaced:
NEW specialist-preamble.md — the shared core every specialist loads first. Six commandments,
each earned by a real violation: (1) verify-don't-infer incl. docs/memory drift (the rust-source
false claims), (2) code-layering (formula vs orchestration vs presentation vs content vs type),
(3) prove-it by output type (cargo test / headless loop / golden-repin / render-proof), (4) stay
in scope (Game 1, no gold-plating disabled systems), (5) objective state = pointer+protocol not
embedded, (6) safety/workflow (build-output, atomic commits, worktree-fork→file-extraction).
agents-task-map.md rewritten as the orchestration PLAYBOOK (not just a lookup): dispatch-vs-inline,
parallel-by-default, the mandatory verify gate per output type, the worktree-fork integration rule,
"specialists return data not prose". Fixed the phantom mc-magic row (Game 2/3, no crate).
All 13 agents converted to thin shared-core pointer + domain delta (fault line + domain docs +
how-to-verify), each fault line drawn from a real drift risk for that specialist. Router + README +
CLAUDE.md agents section updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Auto-regen of objectives.json + DASHBOARD_* reflecting this session's status edits (p3-26 B-series,
p3-27 biosphere, p3-29 steps 1-2). Bookkeeping only; split out of the agent-tooling commit to keep
both atomic.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Better-guide-agents pass, grounded in this session's architecture review (GDScript turn-logic
divergence, rules leaked into mc-core). Two-pronged:
NEW `code-layering.md` — the decision PROCEDURE that was missing (the Rails state the rule; agents
still drifted because there was no "where does this code go?" procedure). Classifies every change as
formula/orchestration/presentation/content/shared-type, gives each one home, mandates grep-before-
reimplement, uses biomes as the template, records the real anti-patterns hit. Wired into the
always-loaded router + README index.
FIX `rust-source-of-truth.md` drift (it was actively misleading agents):
- "AI exception (one of one)" CONTRADICTED Rail-1 → recast as tech-debt; GdAiController DOES exist
(api-gdext/src/ai.rs:333), dispatch via mc_player_api::controllers.
- crate table listed phantom `mc-magic` + claimed BiomeRegistry in mc-core (false) → accurate
pure-logic-vs-orchestrator table; mc-core = shared TYPES, no rules.
game-systems + godot-engine agents point at code-layering at the fault lines they own.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
turn_result_to_dict now includes an "events" array (each TurnEvent mapped via the reused
replay::event_to_dict, now pub(crate)) — CityGrew, CityBordersExpanded, FloraSuccession,
CityBuildingCompleted, UnitCreated, CityCaptured, etc. So when turn_manager adopts
GdTurnProcessor.step (the Rail-1 swap), it can translate result["events"] → EventBus signals
and the GDScript turn orchestration can be deleted. gdext compiles.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Regression fix: the B2 healing phase ran end-of-turn BEFORE siege and healed cities
unconditionally — so a besieged city healed (e.g. 30→50 hp) before the same turn's siege
resolved, defeating captures (last_survivor_via_capture got empty events: a 30-hp city +
3 attackers @15 = 45 dmg no longer captured after healing to 50). Bisected to this session
(passed at e926345ad; the healing phase introduced the bug).
Fix: process_healing_phase now snapshots all units' tiles and SKIPS healing any city with an
enemy unit on its tile (under siege) — real-time siege-suppress matching the live game's intent
(its `last_attacked_turn` window). Un-besieged cities still heal. Tests:
besieged_city_does_not_heal (new) + last_survivor_via_capture (restored) + healing 11/0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All three granular UI events the GDScript turn emitted inline are now emitted by the Rust turn
(replay value now; UI-parity ready for the swap). Remaining: step 2 (dict surface) + steps 3-5
(turn_manager → GdTurnProcessor.step + delete GDScript orchestration + render proof).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Third + final p3-29 step-1 event. The ecology phase (uniform fn(&mut GameState) registry
signature, no event sink) buffers its flora-succession transitions into a transient
GameState.pending_flora_events; step() drains them into the TurnResult as
TurnEvent::FloraSuccession — single-source replacement for the GDScript turn's flora_succession
signal, avoiding a 40-call-site registry-signature cascade. Surfaced through all four TurnEvent
consumers + tested (step_drains_flora_buffer_into_flora_succession_events).
p3-29 step 1 DONE: the Rust turn now emits CityGrew + CityBordersExpanded + FloraSuccession —
the granular UI events the live game's GDScript turn emitted inline. Replay value now;
UI-parity ready for the swap (steps 3-5). Events-only → golden/combat unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 2 high-value growth/border events are surfaced from the Rust turn (replay value now,
UI-parity at the swap). FloraSuccession deferred: the ecology phase's registry signature
(fn(&mut GameState)) has no event sink, and the registry-events refactor belongs with the
swap (steps 3-5), so flora rides with it rather than triggering a 40-call-site cascade now.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second p3-29 step-1 event: the Rust turn emits border expansion (process_culture now takes
&mut events, collects claimed tiles + flushes TurnEvent::CityBordersExpanded with clan/city/hex)
— single-source replacement for the GDScript turn's inline `city_border_expanded` signal.
Surfaced through all four TurnEvent consumers + tested (culture_expansion_claims_frontier_tiles
now asserts the event). Events-only → no state change → golden/combat unaffected. 2/3 step-1
events done (CityGrew, CityBordersExpanded); FloraSuccession next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First step of the Rail-1 turn-unification: the Rust turn now EMITS city growth as
`TurnEvent::CityGrew` (process_city_production collects grown cities + flushes the event with
clan/city/population/hex), instead of growth being a silent state mutation only the GDScript
turn announced (`city_grew` signal). Surfaced through all four TurnEvent consumers (replay
turn(), mc-player-api wire drop, api-gdext replay dict, test labels).
Value now: richer replay. Value next: when turn_manager switches to GdTurnProcessor.step, it
translates this event to the EventBus.city_grew signal — letting us delete the GDScript growth
logic (Rail-1 DRY). Doesn't change turn state (events only) → golden + combat tests unaffected.
mc-turn city_growth_emits_city_grew_event green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
- 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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>