authored_encounter_rates(), authored_lair_siege_config(), and
severable_infra_from_data() drop their bespoke include_str! (two using
CARGO_MANIFEST_DIR concat, one a ../../../../../ relative path) and pull raw
bytes from content::get(EncounterRates/LairCombatModes/Couriers). Embedded
fallbacks centralized in mc-core. mc-turn tests green (297 + suites).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both promotion_config() and build_registry() now pull raw bytes from
content::get(Promotions) instead of a crate-local include_str! const. The
embedded fallback moved to the central ContentRegistry, so the Rail-2 gate gains
a registry_owned flag: Check A verifies mc-core/src/content.rs embeds the JSON,
while the XP_THRESHOLDS/HEAL_ON_PROMOTE_FRACTION tombstones still guard
mc-combat. Gate passes; mc-combat/mc-turn/mc-player-api tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both phase3_cfg() and heartbeat_interval_for_tier() dropped their own fragile
../../../../../ include_str! and now pull raw bytes from content::get(Comms).
Embedded fallback unchanged (now centralized in mc-core). 11 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
One place for canonical-JSON content packs both sim paths read. Embedded
include_str! defaults (the WASM/headless fallback) live in a single EMBEDDED
table keyed by ContentKey; hosts (Godot res:// bytes, web fetch bytes) override
via register(). No filesystem access — WASM- and GDExtension-safe. Consumers
will call content::get(key) instead of per-crate fragile relative include_str!.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase-2b live swap (default OFF). When RUST_TURN=1, the proven
GdTurnProcessor.step advances the WHOLE round on live state in one call
(sync presentation->inner, step, sync inner->presentation), and the
per-player _process_* loop + round-end ecology/climate/wild/diplomacy
GDScript passes are gated off to avoid double-processing. step's events[]
are translated to EventBus signals (tech/culture/golden-age now; entity-
payload kinds deferred). Default path is byte-for-byte the existing turn.
Render-proof of the ON path (live game plays a turn through the Rust step)
remains the render-gated acceptance item.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The daily-use section listed up/sim/down/train but not the new artifact
verbs. Add the publish -> sync fetch flow + dist:models, pointing at
cloud-dx-do.md for the full table.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every specialist loads this preamble. Replace "commit/push only when asked"
with the new auto-atomic-commit + push behavior (defers to the global Git
Commit Protocol), and correct the stale "forge is down" note — the forge
(159.203.170.249) is now the live origin.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Build the linux .so/wasm once on a worker and let sim/test/AI runners fetch the
prebuilt artifact (keyed by git sha) instead of recompiling — N workers share
one build. Adds the magicciv-artifacts DO Space, rclone in the golden image, and:
- dist:publish build + upload builds/<sha>/{.so,wasm}
- dist:fetch download the prebuilt .so for HEAD's sha
- dist:sync git pull -> fetch prebuilt if published, else build
- dist:models share RL .onnx via the Space (push/pull/ls)
Complements sccache (compile cache) by caching final outputs. Creds via
RCLONE_S3_* env over ssh, never on worker disk/argv; degrades to build-on-worker
when creds/cache absent.
Also hardens the dispatch layer (pre-existing, affected test/build/render too):
- pass -i ~/.ssh/id_mc_fleet on dispatch ssh (don't rely on agent-loaded key)
- guard _dist_first_host against an empty / "fleet down" inventory
- drop ssh -n on heredoc-stdin verbs (it redirected stdin from /dev/null)
Proven end-to-end on DO: publish built a 43.9MB .so + wasm; dist:sync fetched it
in 2.8s (no rebuild).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rail-1 spine rewrite Phase 2 foundation. GdTurnProcessor::step mutates
GdGameState.inner only, but the live game holds authoritative cities/units
in the rich presentation_* slots. Add state_sync module + two #[func]s
(sync_presentation_to_inner / sync_inner_to_presentation) implementing
Option C batch sync around the step:
- Units: whole-vec clone both ways (presentation_units and
inner.players[].units are the identical mc_state::MapUnit type).
- Cities: rich City <-> lean CityState scalar projection (population,
food_stored<->food, production_progress<->production_stored, owned_tiles,
hp/max_hp). Down-sync updates lean in place, preserving lean-only fields
(queue/queue_cost/queue_tier/food_yield/prod_yield/worker_expertise);
up-sync merges only the bridged scalars back, leaving rich-only fields
(queues, buildings, building_yields, culture_*, focus, name) untouched.
city_positions/capital_position kept aligned for process_culture/siege.
- Player scalars (gold/science/culture_pool/tech/relations) are inner-only;
no parallel rich slot, so no sync needed.
Sync gap (documented, not fabricated): lean single queue vs rich per-building
queues map has no clean 1:1 mapping and is deliberately not bridged.
8 cargo tests incl. a real mc_turn::TurnProcessor::step driven through the
down/up loop (city grows, rich queues survive). Not yet wired into the live
turn (GDScript Phase-2b). 8/0 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GUT coverage for Rail-1 Phase-1 increment 1. Each test reads the authoritative
Rust slot directly (unit_dict / unit_index_by_id / unit_locate_by_id) and
asserts the proxy stays in lockstep:
- spawn → slot reflects position + hp; proxy reads the same
- move → slot position updates
- take_damage → slot hp drops
- fortify → slot is_fortified set
- death → remove_unit deletes the entry and clears rust_id
- index-shift safety → survivors stay addressable by stable id
- wild creatures land in the wilds row, never colliding with player 0
- transfer_to_owner moves the slot entry between rows
- save → load round-trips a unit through presentation_units
Tests early-return with a pending() note when the GDExtension dylib is absent
(headless CI that never built it) rather than asserting the local-mirror
fallback, which the existing test_unit_actions.gd no-extension suite covers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rail-1 Phase-1 increment 1 (wiring) — every site that brings a Unit into or out
of the world now flips the authoritative Rust slot alongside the player/layer
lists, so the proxy resolves a live MapUnit.
Spawn sites → `spawn_into_slot()`:
- world_map_units.register_unit (the central chokepoint: starting units via
spawn_starting_units, the prologue tribe via _on_prologue_tribe_converged, and
any future caller)
- turn_processor._spawn_unit (city-built unit)
- wild_creature_ai (lair spawns → wilds row; now constructs via the populating
ctor so stats come from JSON)
Death / consumption sites → `remove_from_slot()` (index-shift-safe; snapshots
final pos/hp into the local mirror first so unit_destroyed subscribers — loot,
chronicle — still read the unit as it died):
- world_map_units.remove_unit
- combat_utils.handle_unit_death + _destroy_high_archon
- economy upkeep disband
- ai_turn_bridge_dispatch settler-consumed-on-found
- prologue_driver tribe-consumed-into-capital
No live unit-CAPTURE path exists in Game 1 (units die, they are not captured);
`transfer_to_owner` is wired on the proxy for parity but no site converts to it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rail-1 Phase-1 increment 1 — the units analogue of City's proxy over
presentation_cities. Unit's authoritative gameplay surface (position, hp,
max_hp, attack, defense, movement_remaining, xp/experience, the posture flags,
promotions) now lives in the Rust `presentation_units` slot, reached via
`GameState.get_gd_state().unit_*(_pi, _resolve_ui())`. The slot is positional,
so the view keys on a stable u32 `rust_id` and re-resolves its row index on
every access — a death (remove_unit shifts indices) or capture (transfer_unit
changes both _pi and _ui) never leaves a getter reading the wrong unit.
- Slot-backed fields become property getter/setter pairs that route to the slot
when spawned, falling back to `_local_*` mirrors when unspawned / no dylib, so
bare `Unit.new(...)` (arena tests, early construction) keeps working.
- DUAL ID: `id: String` stays the renderer/debug key; `rust_id: int` is the
Rust-backing key. unit_slot::spawn trusts the JSON-borne id (unlike
City::found which derives it), so GameState owns id assignment via a new
monotonic `next_unit_id()` counter (serialized; restored above loaded ids).
- Wild creatures (owner -1) land in a dedicated wilds row at `players.size()`
(`wilds_pi`/`unit_slot_pi`) so they never collide with player 0.
- spawn_into_slot / remove_from_slot / transfer_to_owner are the slot lifecycle;
from_save_dict reattaches a restored unit to a fresh slot entry keyed on its
saved rust_id (rust_id 0 = never-slotted synthetic unit, left on its mirror so
the save dict round-trips byte-identically).
- deserialize / reset drain the unit slot (incl. the wilds row), mirroring the
city-slot drain, so units do not accumulate across loads.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
unit_slot.rs + GdGameState.presentation_units + GdUnit + SaveEnvelope v4 land the
Rust home for the units hold-out (mirrors city_slot/presentation_cities). Rust-side
foundation done + headless-proven (unit_slot 7/0, save_envelope 6/0); remaining is
the render-gated live wiring (route unit input through the delegators, reduce
UnitScript to a view). MapUnit was already rich — no model widening needed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror the proven `presentation_cities` city store for units: a parallel
`presentation_units: Vec<Vec<mc_state::MapUnit>>` slot on GdGameState, owned
by a new `unit_slot` ops module that is the exact unit-side analogue of
`city_slot`.
- `api-gdext/src/unit_slot.rs`: bounds-safe `at/at_mut`, stable-u32-id
`index_by_id/locate_by_id`, `spawn`, `move_unit`, `transfer_owner` (capture),
`remove` (index-shift-safe), `take_damage/heal/set_hp`, posture setters
(fortify/sentry/movement), `to_dict` projection, `to_json/load_from_json`
serde round-trip. Pure holding + projection; turn/action logic stays in
mc-turn / mc-player-api dispatch (no duplication of MapUnit mutation).
- GdGameState gains `presentation_units`, initialised parallel to
`presentation_cities` (empty rows grow on demand) and folded into the save
envelope at every site (init, serialize_full, load_from_json).
- SaveEnvelope gains `presentation_units` (#[serde(default)]); CURRENT_VERSION
bumped v3 → v4. save_envelope.rs literals + version-lock test updated.
- `#[func]` delegators on GdGameState mirror the city_* surface (spawn_unit,
move_unit_slot, transfer_unit, remove_unit, unit_take_damage/heal/set_hp,
posture setters, unit_dict, unit_index_by_id/locate_by_id, unit_to_json/
load_from_json).
- `GdUnit` per-instance wrapper for parity with GdCity (owned MapUnit,
to_dict/to_json/field reads).
Tests: 7 unit_slot ops tests (spawn→locate, move, remove-shift-id-stable,
damage/heal clamp, posture, transfer between rows, json round-trip) + the
updated save_envelope suite, all green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The forge will hold full main history (8.4G of old committed target/ blobs);
the build only needs the current tree (~209MB), so clone --depth 1 keeps cold
builds fast regardless of the bloat. Matches the existing shallow fetch path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Promotions were half-built headless (eligible + AI-picked but never consumed,
combat read no modifier). Now closed: MapUnit.promotions, mc-turn consume phase,
mc-combat effect registry + per-unit combat modifiers at both PvP sites. 9/18
effect types live (every combat-expressible one); 9 deferred on missing combat
substrate (auras/multi-attack/splash/movement-waivers/status-on-attack) — flagged
as separate combat-feature objectives for an owner scope call.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add consume_pending_promotions phase to the end-turn step (after combat XP):
validates each unit's pending_promotion against its applied promotions
(requires-chain, no-dupes, existence), records it, folds hp_bonus into max_hp,
applies heal_on_promote (clamped), clears the pick. Illegal picks are dropped.
Inject per-unit promotion combat modifiers at both PvP combat sites, mirroring
equip_combat_bonus: attacker offence keys off its tile biome + defender flags,
defender defence off its own tile + whether the attack is ranged. Percentages
fold into flat atk/def/ranged against base stats. Projection now gauges
promotion_available against the real level (promotions.len()).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add MapUnit.promotions (applied promo ids; len = promotion level) and a
mc-combat registry parsed from the embedded promotions.json trees. Replace
the stale PromotionEffect {stat,value} model with {type,value,condition}
matching the JSON, and add promotion_combat_modifiers(applied, ctx) — a pure
aggregator that evaluates effect conditions (open/rough terrain, vs_ranged,
vs_city, vs_fortified, in_city) against a combat context and sums atk/def/
ranged/range/hp/movement/wall/xp modifiers. validate_promotion_pick enforces
existence, no-dupes, and requires-chains.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the two new verbs to the table and rewrites the iteration section as a
cost-tiered ladder (dist:sync seconds / dist:image ~8min / --cold ~20min) so agents
reach for the incremental rebuild, not a cold packer build per change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mark CityView.culture_stored done (04763a387) — the last clean Phase-0 projection
gap. Note UnitView.experience already projects bench MapUnit XP; the deferred item
is the live-unit XP store (Phase 1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The live city_screen reads a per-city culture meter off the GDScript CityScript;
view_json had no equivalent, so the UI-pure-view migration couldn't render it from
getState(). project_cities now surfaces culture_stored from
PlayerState.culture_pool.city(c_idx) (the same accumulator mc-turn::process_culture
ticks for border expansion); 0.0 for a city with no pool entry.
Closes the last genuine Phase-0 projection gap (UnitView equipped/experience/movement/
posture + ResourceView golden_age were already projected — design-doc table was stale).
Test: projection_surfaces_city_culture_stored. mc-player-api lib 142/0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Incremental rebuilds accumulate snapshots (~$0.40/mo each). dist:prune keeps
the newest N (default 2: current + one rollback); dist:image reminds you to run it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Packer base image is now a var; ./run dist:image builds FROM the newest
mc-golden snapshot by default, so the idempotent provision.sh only redoes changed
work (~3-8 min vs ~20 cold). --cold rebuilds from stock Ubuntu to reset layer
cruft. Made the clone step idempotent (clone-or-fetch) so it works on a
pre-provisioned base. Directly addresses 'avoid unnecessary rebuilds'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Validate provision.sh on a live box first; packer -on-error=ask; batch fixes;
check size/forge prereqs before building; code via dist:sync (image rebuild only
for toolchain/accelerator changes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Four E0063 compile errors broke `cargo test --workspace --no-run`, blocking
`./run dist:test` on the DO fleet. Each is a stale struct literal in test/test-cfg
code that drifted from its current definition:
- mc-worldsim event_dispatch low_bio_thresholds: BiologicalThresholds missing
migration_drought_factor / migration_drought_max (p3-21 drought coupling) —
set to 0.0 / 1.0 to keep the helper's migration-suppression intent.
- mc-mod-host wasm_controller_{noop,limits}: TacticalState missing embark_level —
Default::default() (EmbarkLevel::None) to match the empty-state intent.
- api-gdext ai.rs tile_with + ai_controller test: TacticalTile missing explored /
TacticalState missing embark_level — explored:true (pre-field default = seen),
embark_level default.
Mirrors the sibling fix 04fabbc1c. `cargo test --workspace --no-run` now compiles
clean; full suite passes except 3 pre-existing GPU-parity tests (Metal fp drift,
unrelated to these changes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Worker-only ~/.cargo/config.toml (Linux, never plum's macOS): mold via
-fuse-ld=mold (fast cdylib linking) + sccache rustc-wrapper (compiled-crate cache,
warm in the baked image). mold installs via apt or GitHub static-binary fallback;
both gated on install success so a miss never breaks cargo. Verified mold=true
sccache=true in the build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The RangedAttack dispatch work (e8dd4a85b) added `is_ranged` to
`AttackRequest`, but the mc-golden-tests pvp_combat_determinism test still
constructed the struct without it, breaking `cargo test --workspace` (and
the cloud fleet) with E0063. Set `false` for this melee-combat test,
matching the mc-turn PvP tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Golden image now installs the software-render stack (weston, libgl1-mesa-dri
llvmpipe, mesa-vulkan-drivers, vulkan-tools) so any worker renders proof scenes
via gl_compatibility/opengl3 with no GPU. New ./run dist:render <scene> <out.png>
wraps tools/capture-proof.sh against a worker (replaces the apricot SCREENSHOT_HOST).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
rust-source-of-truth.md: add the "two-path divergence" rule to the canonical
content store section. Content reaches the sim two ways — in-game (GDScript
DataLoader reads JSON at runtime, projection.rs:41) and headless (Rust falls back
to a compile-time include_str!/hardcode copy, dispatch.rs:410). A balance constant
hardcoded in a crate is both a Rail-2 violation and a silent second copy that
drifts from the JSON — and headless is where the AI trains. Rule: grep
public/resources + public/games/**/data for a JSON home before adding a numeric
balance const; if it exists, LOAD it (OnceLock+include_str!, never std::fs in
shared sim code). References the p3-28 ContentRegistry endgame.
p3-28: add the matching "Rail-2 verify gate (enforcement)" acceptance bullet —
tools/check-no-rust-hardcoded-content.py + a verify step to catch the next
hardcode, best landed alongside the ContentRegistry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Proves the spawn → command → view contract the GdGameState bridge exposes for
the render-gated live flip, at the mc_player_api layer its shims call: a MapUnit
pushed onto inner (as spawn_unit_into_inner produces) appears in project_view;
a Fortify via apply_action is reflected in the next view; a command on a stale
unit id is a typed error, not a panic. Existing integration tests load pre-built
states — none exercised the spawn-then-act-then-view triple a freshly-spawned
live unit goes through. De-risks the foundation before the GDScript flip depends
on it. mc-player-api 3/3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gap 3 — Equipment/crafting: verified the full craft→equip→combat path runs
headless and Rust-authoritative (orig bullet was stale at [ ]):
- PlayerAction::CraftEquipment → craft_equipment dispatch (materials gate +
consume strategic_ledger + equip), 2 tests
- recipe_phase ("recipe_refine") in END_OF_TURN_PHASES — passive crafting
economy refines raw→quality-tiered product every self-play turn, 1 test
- equip_combat_bonus reads boot-loaded item_combat at every combat site, 2 tests
- boot path: set_item_combat_json FFI ← headless harness _apply_item_combat
- MCTS AI not electing to craft = deliberate 9-kind GPU-rollout constraint,
not a missing system
Verified green: mc-turn + mc-player-api 557/0.
Gap 4 — Per-building queues: recorded verified assessment. Bench single-slot +
per-turn AI reselection is functionally equivalent to a FIFO build queue for the
self-play SIMULATION outcome; the multi-item queue is a live-game UI affordance
belonging to the p3-25/p3-29 projection arc. Owner scope call pending: does p3-26
require simulating a multi-item queue, or reclassify Gap 4 out of the headless bar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Offload heavy compute from plum (M2 Air) to on-demand DO workers:
- dist:test — cargo test --workspace (nextest) on a worker (the main DX win)
- dist:build — cargo build + WASM on a worker; rsync the platform-independent
WASM back (native .so is linux-only, stays on the worker)
- dist:sync — git pull <ref> + rebuild gdext on live workers (no image rebuild)
- forge:down/up — snapshot+destroy / restore-from-snapshot (DO bills powered-off
droplets; only destroy stops it). ~$6/mo -> ~$0.30/mo idle; refreshes the
forge IP in ~/.vault/mc_forge_creds on restore.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The live unit store (GdGameState.apply_action_json → inner) handled melee but
RangedAttack was NotYetImplemented. Wire it by reusing the melee resolver:
split resolve_single_pvp_attack into resolve_single_pvp_attack_typed(.., is_ranged);
ranged sets CombatType::Ranged → sources ranged_attack/range from units_catalog
and the resolver's prevents_retaliation(combat_is_ranged=true) suppresses the
counter-attack. Did NOT reuse the crude pending_volley AoE (separate Volley
action); verified live parity is immediate-resolve (combat_resolver.gd:87-104),
so a direct resolve mirroring melee is correct.
- AttackRequest gains is_ranged (serde-default); process_pvp_combat threads it.
- dispatch apply_ranged_attack: owner + enemy + within-range gate, then resolve.
- tests: ranged_pvp_no_retaliation (resolver: damage, attacker untouched, 0
retaliation), ranged_attack_no_retaliation (dispatch: range gate + rejections).
Deferred (parity, cited): no movement-spend on attack — melee doesn't spend it
either; a "ranged is in-scope; verify gate mc-combat+mc-turn+mc-player-api 0 failed.
Dispatched combat-dev; verify gate green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gives the live GdGameState the same Rust-driven surface the headless GdPlayerApi
has, on its own inner GameState, so inner.players[].units (rich MapUnit) can
become the live unit store:
- apply_action_json(player, action_json) → mc_player_api::apply_action(&mut inner)
- inner_view_json(player) → mc_player_api::project_view(&inner)
- spawn_unit_into_inner(player, unit_type_id, col, row) → MapUnit::new + push,
monotonic next_unit_id (same idiom as the AI-faction spawn).
Thin shims over the SAME mc_player_api fns GdPlayerApi calls (no dispatch/
projection duplication; 4 envelope helpers made pub(crate) for reuse). No
GDScript touched; GdPlayerApi + bench path untouched.
Contract for the later (render-gated) live caller: stamp inner.units_catalog
(+ action configs) via the existing set_*_catalog_json setters before relying on
the view — documented inline (lib.rs:4297). cdylib links with all 3 #[func]s
registered (distinct symbols from GdPlayerApi); mc-player-api 0 failed.
Dispatched simulator-infra; verify gate green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ephemeral CPU Droplet fleet that horizontally scales the iteration loop:
- infra/terraform/test-fleet: cattle Droplets from a golden image (auto-discovered
by name via digitalocean_images), grouped under the mc:dev DO project, with a
mocked-provider test suite (no token/spend).
- infra/packer: golden-image builder reusing scripts/dev-setup/linux.sh.
- scripts/run/dist.sh: ./run dist:{check,up,sim,train,down} — shard sim/test
batches across workers via autoplay-batch AUTOPLAY_HOST+SEED_OFFSET.
GPU intentionally absent (workload is CPU-bound per docs/ai-production.md).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Units gained no XP in the headless/bench turn (only GDScript UnitScript tracked
it). The XP amounts were already Rust-authoritative (mc-combat: BASE_COMBAT_XP=5
× xp_from_combat strength scaling; resolver zeroes dead-defender XP / suppresses
capture XP). This wires the award into the bench turn so the unified game has
veterancy:
- MapUnit.experience: i32 (#[serde(default)]; all 110 literals use ..default()).
- resolve_single_pvp_attack accumulates attacker_xp/defender_xp onto survivors,
survival-gated exactly like combat_resolver.gd:215-223.
- project_units surfaces UnitView.experience + promotion_available from XP
threshold eligibility (mc_combat::check_promotion), replacing the 0 stub.
- new test pvp_combat_awards_xp_to_survivors (queued-attack path, no kills →
both survivors gain XP).
Deferred (cited, out of scope): the veteran_level/promotion stat-growth pick
subsystem (bench uses flat UnitStats, not the D20 path) and the pre-existing
Rust↔JSON promotion-threshold divergence (promotions.json [15,30,45,60] vs Rust
[10,30,60,100]) — a Rail-2 content/code gap tracked separately.
Dispatched combat-dev; verify gate: mc-combat+mc-turn+mc-player-api 0 failed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verified the live city-projection path: api-gdext/city_slot.rs is a full ops
module over presentation_cities (rich mc_city::City) + GdCity wraps it; CityScript
is a hybrid proxy already routing to the Rust slot. So cities are largely
Rust-authoritative — the GDScript residue is just the city-centre queue +
placed_buildings. UNITS are the real Phase-1 hold-out (UnitScript fully
GDScript-authoritative, no Rust slot). Rails: bench CityState/MapUnit and the live
game are sanctioned-separate contexts (code-layering #3), so the live view must
project the RICH city / a new live-unit store — NOT the bench types. Refines the
Phase-1 plan accordingly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
EquippedItemView (item_id, category, charges_remaining, triggers_in_combat —
the exact mc_items::EquippedItem fields, cited) + UnitView.equipped, projected
from MapUnit.equipped for OWN units only, omitted from the wire when empty.
Surfaces the unit_panel.gd:789 entity read via view_json.
happiness_breakdown DEFERRED (verified, not fabricated): the per-contributor
breakdown is a transient calculate_happiness return (mc-happiness/pool.rs:170),
not persisted PlayerState — only the scalar happiness pool is stored
(game_state.rs:1295), already surfaced as ResourceView.happiness_pool. A
Phase-1 SOT-flip widening, like XP/culture_stored.
Dispatched simulator-infra; verify gate: mc-player-api green incl. new equipped
round-trip/omit test. Additive (serde defaults).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-scope p3-25's view-completeness as Phase 0 of the owner's full UI-pure-view
migration (blueprint: designs/p3-rail1-ui-pure-view-migration-design.md). Log
the two landed increments (unit movement+posture 568e43084, golden age 0d501a3d7)
and the deferred bench-model gaps (XP, culture_stored, building_queues,
placed_buildings → Phase-1 SOT flip).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ResourceView gains golden_age_active + golden_age_turns, projected from the
PlayerState GA fields. These are the top_bar.gd:162/169 entity reads now
available via view_json (HUD badge driven by Rust, not the Player entity).
Omitted from the wire when inactive. Per-city culture_stored (city_screen.gd:287)
is DEFERRED: it has no bench CityState backing (culture is a player-level pool in
the reduced model) — a Phase-1 SOT-flip widening, not fabricated here.
Additive (serde defaults). mc-player-api 140/0 incl. a GA round-trip/omission test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First projection-completeness increment toward UI-driven-by-Rust. project_units
stubbed movement_left/max=0, sentry=false, promotion_available=false despite the
bench MapUnit carrying the real data — fix to read movement_remaining/base_moves/
is_sentrying/pending_promotion (same fields the move dispatch + legal-move gate
use). Add UnitPostureView (embarked/deployed/stealthed/ambushing/field_aura/
fire_arrows/pursuing/shield_wall/braced/rage/war_cry) + formation_id, projected
for OWN units only (no stealth/ambush leak); omitted from the wire when resting.
These are the unit_panel.gd entity reads (audit Group B) now available via
view_json. XP stays 0 (not yet on MapUnit — Phase-1 SOT-flip gap, noted).
Additive (serde defaults; no other UnitView constructors in the workspace).
mc-player-api 139/0 incl. 2 new posture round-trip/omission tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Owner directive: remove all sim logic from GDScript; UI becomes a pure view of
the Rust game server. Phased design grounded in a verified 3-surface audit +
view.rs/projection.rs/game_state.rs/turn_*.gd ground-truth. Key finding: the
live game holds TWO unsynced authoritative stores (GDScript entities = live SOT;
Rust GdGameState = parallel copy), so this is a spine rewrite — the live game
must converge onto the headless GdPlayerApi shape. Phases: 0 projection
completeness (headless, first), 1 SOT flip, 2 live turn = end_turn(), 3 delete
GDScript sim layer, 4 render-proof.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Owner ruled the GdWildAiController bridge over an in-step substrate. Mark
acceptance #2 [~] (Rust bridge done 8696a48aa; GDScript rewire render-gated)
and spell out the remaining live-game rewire steps + the render/live-run host
needed for the parity proof.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Owner chose the bridge over a headless wild-unit substrate. Adds the
JSON contract + a pure mc_ai::wild::decide_wild_actions_json(json, seed)
helper (parses a WildContextDto — wilds, player_units, lairs, cities, config,
passable set — runs the decision core, returns per-action JSON strings, the
GdAiController envelope), and a thin GdWildAiController GDExtension shim
(set_rng_seed + decide_actions → PackedStringArray) over it.
The live game keeps its roaming owner==-1 units; GDScript projects them into
the DTO and dispatches the returned move/attack Actions via the existing
AI-action path — so the wild DECISION logic is fully Rust (Rail-1), no
duplicated headless model. 16 wild tests (4 new JSON-bridge: chase/attack/
passable-roam/malformed), mc-ai lib 305/0; gdext cdylib links with the class
registered. Remaining (render-gated): GDScript rewire of _process_wild_creatures
+ wild_creature_ai.gd deletion + render-proof.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Close the flora-succession [~] bullet. Verified in engine code (not the
comment): process_step → run_tier_advancement advances tiers in-place
(par_iter_mut → tick_tiers_capped mutates slots) and returns FloraTransitions
only as a chronicle report. The headless ecology_phase applies succession via
process_step and buffers transitions for the p3-29 FloraSuccession event — no
separate mc-flora::FloraEngine pass needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
stub → partial. Acceptance #1 (decide_wild_actions) + #4 (determinism) ✓ with
cited evidence (95a2e580b, mc-ai 301/0). Record the verified premise that the
headless GameState has no roaming wild-unit substrate (units implicit-owned,
wilds = lairs + encounters), making the in-step path a substrate-build that
duplicates encounters vs. the allowed GdWildAiController bridge. Recommend the
bridge; integration + .gd deletion stay render/decision-gated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>