feat(@projects/@magic-civilization): add per-category stacking upgrades

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-04-30 00:44:33 -04:00
parent 71ba1b7f1f
commit c7dba6cdb6
6 changed files with 150 additions and 40 deletions

View file

@ -15,10 +15,10 @@
| Priority | ✅ | 🔵 | 🟡 | 🔴 | ❌ | ⚫ | Total |
|---|---|---|---|---|---|---|---|
| **P0** | 43 | 0 | 0 | 0 | 0 | 0 | 43 |
| **P1** | 33 | 1 | 8 | 0 | 12 | 1 | 55 |
| **P1** | 33 | 1 | 8 | 0 | 13 | 1 | 56 |
| **P2** | 32 | 0 | 2 | 1 | 1 | 0 | 36 |
| **P3 (oos)** | 3 | 0 | 0 | 0 | 1 | 19 | 23 |
| **total** | **111** | **1** | **10** | **1** | **14** | **20** | **157** |
| **total** | **111** | **1** | **10** | **1** | **15** | **20** | **158** |
</td><td valign='top' style='padding-left:2em'>
@ -130,7 +130,8 @@
| [p1-40](p1-40-single-source-of-truth-resources.md) | ✅ done | Collapse data/<category>/ override layer into single source of truth at resources/ | — | 2026-04-29 |
| [p1-41](p1-41-game-pack-subscription-manifest.md) | ✅ done | Game-pack subscription manifest + loader filter (Phase B of resources/ unification) | — | 2026-04-29 |
| [p1-42](p1-42-ai-full-building-catalog.md) | ❌ missing | AI must consider the full 155-building catalog, not the hardcoded 8-id ladder | — | 2026-04-29 |
| [p1-43](p1-43-building-stacking-upgrade.md) | ❌ missing | Building stacking — build on top of an existing building to upgrade it (e.g. double barracks → infantry) | — | 2026-04-29 |
| [p1-43](p1-43-building-stacking-upgrade.md) | ❌ missing | Building stacking — per-category upgrade chains (military / science / culture / production / etc.) | — | 2026-04-29 |
| [p1-44](p1-44-buildings-as-producers.md) | ❌ missing | Buildings produce units, not the city center — per-building production queues | — | 2026-04-29 |
| [p2-06](p2-06-export-pipeline.md) | ✅ done | Export pipeline for Windows / macOS / Linux | [shipwright](../team-leads/shipwright.md) | 2026-04-25 |
| [p2-16](p2-16-audio-assets.md) | 🔵 in_progress | Audio assets — in-theme OSS launch pack + source ledger | [asset-audio](../team-leads/asset-audio.md) | 2026-04-27 |
| [p2-22](p2-22-sprite-generation-pipeline.md) | 🟡 partial | Sprite generation pipeline — runnable end-to-end | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-25 |

View file

@ -1,6 +1,6 @@
---
id: p1-43
title: Building stacking — build on top of an existing building to upgrade it (e.g. double barracks → infantry)
title: Building stacking — per-category upgrade chains (military / science / culture / production / etc.)
priority: p1
status: missing
scope: game1
@ -9,9 +9,24 @@ updated_at: 2026-04-29
## Summary
User direction (2026-04-29): "all the buildings should be buildable and some buildings can be built on top of each other (double barracks - infantry)".
User direction (2026-04-29): "all the buildings should be buildable and some buildings can be built on top of each other (double barracks - infantry) ... what about comboing other buildings ... science stack, culture stack".
Today every building is binary: a city either has it or doesn't. The mechanic the user wants: queueing certain buildings on top of an existing one upgrades the slot in place — `barracks` + another `barracks` build = `infantry` (a stronger military producer). This is distinct from the BUILDINGS.md "Hybrid Merged Structures" mechanic (which combines TWO different buildings + Synthesis tech into a hybrid). Stacking is the simpler primitive: same-building-twice becomes its successor.
Today every building is binary: a city either has it or doesn't. The mechanic the user wants: queueing a building on top of an existing one upgrades the slot in place — `barracks` + another `barracks` build = `infantry` (a stronger military producer). The same primitive applies to every category: science stacks (library → scriptorium → academy), culture stacks (monument → bardic_circle → great_hall), production stacks (forge → iron_forge → grand_forge), etc. This is distinct from the BUILDINGS.md "Hybrid Merged Structures" mechanic (which combines TWO different buildings + Synthesis tech into a hybrid). Stacking is the simpler primitive: same-category Lv1 → Lv2 → Lv3 chains within one slot.
The existing data already implies category-tier chains via the `tier` + `category` fields:
| Category | Lv1 (no tech) | Lv2 (mid tech) | Lv3+ (late tech) |
|---|---|---|---|
| Production | `forge` t1 | (gap — `iron_forge` doesn't exist) | `dwarf_deep_forge` t3, `tempering_forge` t6, `steam_forge` t7, `adamantine_foundry` t10 |
| Science | `library` t1 | `university` t3, `observatory` t3 | `academy_of_sciences` t5, `climate_institute` t9 |
| Culture | `monument` t1 | `great_hall` t3, `gathering_hall` t2 | `ancestor_hall` t10 |
| Military | `barracks` t1 | (gap — `infantry` doesn't exist) | `armory` t3, `military_academy` t6, `command_citadel` t10 |
| Food | `granary` t1 | `mill` t2, `brewery` t2, `watermill` t2 | `great_granary` t2 (wonder) |
| Defense | `walls` t1 | `watchtower` t1 | `castle` t3 |
| Wealth | `marketplace` t2, `market` t2 (DUPLICATE) | `guild_hall` t4 | (none) |
| Religion | `temple` t2 | `temple_of_the_ancestor` t5 (wonder) | (none) |
The stacking schema makes these chains explicit and queryable. Where a Lv2 successor doesn't exist yet (e.g. `infantry`, `iron_forge`, `scriptorium`), this objective authors the missing intermediates.
Three design questions need user sign-off before authoring:
@ -29,11 +44,16 @@ Recommendation: option **(a) Replacement** with declaration on the upper tier (`
- ✗ Design pass + sign-off from user on the three questions above.
- ✗ `building.schema.json` extends with: `requires_existing: <id|null>`, `consumes_existing: <bool>`. Both default null/false. Validator confirms `requires_existing` resolves to a real building id.
- ✗ For each declared stack-pair: the upper-tier building is NEW data (or repurposed existing) authored under `resources/buildings/<id>.json` with the new fields populated.
- ✗ Initial pairs (suggested, pending user confirmation):
- `barracks``infantry` (military)
- `forge``iron_forge` (production) — natural progression toward `dwarf_deep_forge`
- `library``scriptorium` (research) — pre-existing `university`/`observatory` make this a tier-1.5 step
- Naming TBD; user picks.
- ✗ Initial 3-step ladders (suggested, pending user confirmation — each is `<existing Lv1> → <new or repurposed Lv2> → <existing Lv3+>`):
- **Military**: `barracks``infantry` (NEW) → `armory` (existing t3)
- **Science**: `library``scriptorium` (NEW) → `university` (existing t3)
- **Culture**: `monument``bardic_circle` (existing wonder t4, repurposed?) → `great_hall` (existing t3)
- **Production**: `forge``iron_forge` (NEW) → `dwarf_deep_forge` (existing t3) — note race-specific successor
- **Defense**: `walls``watchtower` (existing t1, demote?) → `castle` (existing t3) — already a clean ladder
- **Food**: `granary``mill` (existing t2) → `watermill` (existing t2) — needs a Lv3 (no candidate)
- **Wealth**: `marketplace``market` (existing duplicate, reconcile) → `guild_hall` (existing t4)
- **Religion**: `temple` → ??? (no Lv2/3 candidate, needs authoring)
- Final ladder list user-decided. New buildings authored where needed; existing ones get the `requires_existing` field added.
- ✗ Engine: `city.can_build(bid)` returns true for upper-tier IF `bid::requires_existing` is in `city.buildings[]`. City build dispatch (`ai_turn_bridge_dispatch.gd::dispatch_set_production` and the GDScript city UI) honors `consumes_existing` by removing the prerequisite from `city.buildings[]` when the upgrade completes.
- ✗ AI integration (depends on `p1-42`): the AI catalog scoring treats stack-upgrades as a single ladder — barracks → infantry scored as a 2-step build path with combined cost.
- ✗ City UI surfaces stack relationships: when looking at `barracks` in the encyclopedia / build menu, the upgrade target is shown ("Can be upgraded to: Infantry").
@ -42,10 +62,12 @@ Recommendation: option **(a) Replacement** with declaration on the upper tier (`
## Open questions for user
1. Successor naming for `infantry` and other stack-pairs — pick from existing building IDs or author new ones?
2. Do stack-upgrades cost the FULL upper-tier `cost`, or a discounted "upgrade cost"?
3. When the prerequisite is consumed, do its effects vanish entirely, or does the upper-tier inherit them additively?
4. Should the "Hybrid Merged Structures" objective (`p3-02`) be subsumed under this simpler stacking mechanic, or stay as a separate post-EA feature?
1. **Successor identity per category**: confirm the 8 ladders above (or revise). For each, pick existing building or sign off on authoring a new Lv2 intermediate.
2. **Cost**: do stack-upgrades cost the FULL upper-tier `cost`, or a discounted "upgrade cost" (e.g. `cost - prerequisite_cost`)?
3. **Effects inheritance**: when the prerequisite is consumed, do its effects vanish entirely, or does the upper-tier inherit them additively (so an `armory` city has barracks+infantry+armory effects compounded)?
4. **Multiple categories per city**: can a city have ONE stack ladder per category (8 ladders × 3 deep = 24 total slots) or share slots across categories?
5. **Reconcile duplicates surfaced by the audit**: `marketplace` vs `market`, `monument` vs `bardic_circle` — fold one into the other or keep both?
6. **`p3-02` relationship**: is the simpler "double X → Y" stacking enough for EA, with merged-structures (X+Y → hybrid) deferred? Or merge p3-02 into this objective?
## Out of scope

View file

@ -0,0 +1,74 @@
---
id: p1-44
title: Buildings produce units, not the city center — per-building production queues
priority: p1
status: missing
scope: game1
updated_at: 2026-04-29
---
## Summary
User direction (2026-04-29): "it would make sense to build those units in the appropriate buildings rather than the city center."
Today every city has **one** production queue (`city.gd:57 production_queue: Array`) where the player picks "warrior", "barracks", "wonder", anything goes through the same FIFO. Building selection just gates the menu — the building doesn't actually *do* anything when production happens.
The user's model — and the model `PRODUCTION_CHAIN.md` already describes — is that buildings are **producers**:
> Every citizen is a resource pulled between three competing demands:
> - **Construction** — builds new buildings, upgrades existing ones (investment)
> - **Buildings** — produces units, research, culture, equipment (output)
> - **Tiles** — produces food, raw materials, gold (sustenance)
Concretely:
- **Barracks** has its own queue producing infantry / armory / military_academy lineage units.
- **Stable** queues cavalry units.
- **Siege Workshop** queues siege units.
- **Library** queues sages / cartographers / engineers (science civilians) and accumulates research per turn.
- **Temple** queues battle priests and accumulates culture/happiness.
- **Harbor** queues naval units (and gates them — see `p1-33`).
- **Airfield** queues aerial units.
- **Construction queue** (city-level, distinct from building queues) builds NEW buildings and upgrades existing ones.
Each producer building runs its queue independently per turn. Citizens / production allocation gets split across buildings (per `PRODUCTION_CHAIN.md` "Three-Way Tension"). The city's single global queue dies; build-vs-train becomes a structural distinction not a queue-item distinction.
This is a major engine refactor. Touches: `mc-city`, `mc-turn`, `city.gd`, `city_screen.gd`, `city_buildable_helper.gd`, save schema, AI's `production.rs` (now picks per producer-building, not per city), and the new themed-unit catalog (battle_priest, sage, bard, merchant, etc. — see `p1-43` open question 1).
## Acceptance
- ✗ `city.production_queue: Array` retired in favor of:
- `city.construction_queue: Array` — buildings + upgrades only.
- `city.building_queues: Dictionary``{ <building_id>: Array }` for each producer building this city owns.
- ✗ `Building` schema gains `produces: Array<unit_id>` listing units this building can train. Exclusive list — only barracks can queue `warrior`, only library can queue `sage`, etc.
- ✗ Production tick splits per turn: total city `production_per_turn` is allocated across `construction_queue` (always present) + each non-empty `building_queues[bid]` (one slot per producer building with a non-empty queue). Allocation defaults to even split; UI can override.
- ✗ Save schema migration: existing saves' `production_queue` entries split by type — buildings/wonders → `construction_queue`, units → respective `building_queues[<bid>]` based on the building that produces that unit kind. Document migration in CHANGELOG.
- ✗ City UI rework: `city_screen.gd` renders one panel per producer building (showing its queue + production-points slider) plus one panel for construction. Today's single ItemList is replaced.
- ✗ AI rework (depends on `p1-42`): `mc-ai/tactical/production.rs` emits `Action::SetProduction { city_id, building_id, item_id }` (new variant) — `building_id == None` means construction. Per-building scoring decides what each producer queues.
- ✗ Buildable filter respects ownership: a city without a barracks cannot queue `warrior`. Today the filter is purely tech-gated; now it's also building-gated.
- ✗ Themed civilian units authored where missing: `battle_priest` (temple), `sage` (library/university), `cartographer` (observatory), `merchant` (market), `bard` (gathering_hall), `loremaster` (great_hall), etc. Per-unit list confirmed by `p1-43` design pass.
- ✗ Headless GUT tests: city with barracks queues `warrior`, city without barracks cannot; building production advances independently of construction; save/load roundtrips both queue families.
- ✗ Regression batch: 10-seed `tools/autoplay-batch.sh 10 300` shows AI cities with multiple producer buildings produce DIFFERENT units in the same turn (today: cities only produce one item per turn from the single queue).
## Interlocks
- **`p1-42` (AI full catalog)**: depends on this — AI scoring per producer building requires the building→units mapping this objective adds.
- **`p1-43` (stacking)**: stacking the producer building (barracks → infantry → armory) expands the building's `produces` list to higher-tier units. The two land together.
- **`p1-33` (naval/aerial gates)**: collapses into this naturally — `harbor.produces: [river_galley, war_galley, ...]` IS the naval gate.
- **`p1-32` (sawmill/herbalist)**: those processing buildings produce *resources* (lumber, reagents) into the stockpile — same per-building tick mechanism.
## Out of scope
- Citizen-to-building assignment UX (separate UX objective).
- Stockpile system (lumber, leather, ale qualities affecting unit quality from `PRODUCTION_CHAIN.md`) — file as follow-up.
- Master/Grandmaster aura system from `BUILDINGS.md`.
- Multi-tile placement / district mechanics.
## Note on EA scope
This is the largest single architectural change still on the Game 1 board. Three honest options for the EA cut:
1. **Ship EA with single-queue, defer p1-44** — easiest path, but the design vocabulary in PRODUCTION_CHAIN.md / BUILDINGS.md will keep referring to mechanics the engine doesn't have.
2. **Ship EA with this refactor** — meaningful weeks of work; touches every layer. Right architecture from the start.
3. **Hybrid** — ship EA with single-queue + the building→units `produces` field declared but ignored at runtime; flip the runtime to per-building queues post-EA when UX has been validated.
User decision needed.

View file

@ -1,13 +1,13 @@
{
"generated_at": "2026-04-30T04:34:00Z",
"generated_at": "2026-04-30T04:44:03Z",
"totals": {
"in_progress": 1,
"stub": 1,
"missing": 14,
"done": 111,
"oos": 20,
"partial": 10,
"total": 157
"oos": 20,
"stub": 1,
"done": 111,
"missing": 15,
"in_progress": 1,
"total": 158
},
"objectives": [
{
@ -882,13 +882,23 @@
},
{
"id": "p1-43",
"title": "Building stacking — build on top of an existing building to upgrade it (e.g. double barracks → infantry)",
"title": "Building stacking — per-category upgrade chains (military / science / culture / production / etc.)",
"priority": "p1",
"status": "missing",
"scope": "game1",
"owner": null,
"updated_at": "2026-04-29",
"summary": "User direction (2026-04-29): \"all the buildings should be buildable and some buildings can be built on top of each other (double barracks - infantry)\".\n\nToday every building is binary: a city either has it or doesn't. The mechanic the user wants: queueing certain buildings on top of an existing one upgrades the slot in place — `barracks` + another `barracks` build = `infantry` (a stronger military producer). This is distinct from the BUILDINGS.md \"Hybrid Merged Structures\" mechanic (which combines TWO different buildings + Synthesis tech into a hybrid). Stacking is the simpler primitive: same-building-twice becomes its successor.\n\nThree design questions need user sign-off before authoring:\n\n1. **Successor identity**: is `infantry` a NEW building (needs authoring) or an existing one (e.g. reuse `armory` as the \"barracks Lv2\" slot)?\n2. **Mechanic shape**:\n - **(a) Replacement**: building barracks twice consumes both, slot becomes `infantry`. Original gone.\n - **(b) Levelled**: building stays \"barracks\" but carries a `level: 2` field with stacked effects.\n - **(c) Per-tile**: two barracks on same tile merge (only relevant if `placement_tile_required: true`).\n3. **Schema**: declare on the lower tier (`barracks.json::stacks_into: \"infantry\"`) or on the upper (`infantry.json::requires_existing: \"barracks\"` + `consumes_existing: true`)? The latter keeps the relationship bidirectional readable.\n\nRecommendation: option **(a) Replacement** with declaration on the upper tier (`requires_existing` + `consumes_existing`). Matches civ-style upgrade slots, reads naturally in the city UI (\"Upgrade Barracks → Infantry\"), avoids per-tile placement complexity for a v1."
"summary": "User direction (2026-04-29): \"all the buildings should be buildable and some buildings can be built on top of each other (double barracks - infantry) ... what about comboing other buildings ... science stack, culture stack\".\n\nToday every building is binary: a city either has it or doesn't. The mechanic the user wants: queueing a building on top of an existing one upgrades the slot in place — `barracks` + another `barracks` build = `infantry` (a stronger military producer). The same primitive applies to every category: science stacks (library → scriptorium → academy), culture stacks (monument → bardic_circle → great_hall), production stacks (forge → iron_forge → grand_forge), etc. This is distinct from the BUILDINGS.md \"Hybrid Merged Structures\" mechanic (which combines TWO different buildings + Synthesis tech into a hybrid). Stacking is the simpler primitive: same-category Lv1 → Lv2 → Lv3 chains within one slot.\n\nThe existing data already implies category-tier chains via the `tier` + `category` fields:\n\n| Category | Lv1 (no tech) | Lv2 (mid tech) | Lv3+ (late tech) |\n|---|---|---|---|\n| Production | `forge` t1 | (gap — `iron_forge` doesn't exist) | `dwarf_deep_forge` t3, `tempering_forge` t6, `steam_forge` t7, `adamantine_foundry` t10 |\n| Science | `library` t1 | `university` t3, `observatory` t3 | `academy_of_sciences` t5, `climate_institute` t9 |\n| Culture | `monument` t1 | `great_hall` t3, `gathering_hall` t2 | `ancestor_hall` t10 |\n| Military | `barracks` t1 | (gap — `infantry` doesn't exist) | `armory` t3, `military_academy` t6, `command_citadel` t10 |\n| Food | `granary` t1 | `mill` t2, `brewery` t2, `watermill` t2 | `great_granary` t2 (wonder) |\n| Defense | `walls` t1 | `watchtower` t1 | `castle` t3 |\n| Wealth | `marketplace` t2, `market` t2 (DUPLICATE) | `guild_hall` t4 | (none) |\n| Religion | `temple` t2 | `temple_of_the_ancestor` t5 (wonder) | (none) |\n\nThe stacking schema makes these chains explicit and queryable. Where a Lv2 successor doesn't exist yet (e.g. `infantry`, `iron_forge`, `scriptorium`), this objective authors the missing intermediates.\n\nThree design questions need user sign-off before authoring:\n\n1. **Successor identity**: is `infantry` a NEW building (needs authoring) or an existing one (e.g. reuse `armory` as the \"barracks Lv2\" slot)?\n2. **Mechanic shape**:\n - **(a) Replacement**: building barracks twice consumes both, slot becomes `infantry`. Original gone.\n - **(b) Levelled**: building stays \"barracks\" but carries a `level: 2` field with stacked effects.\n - **(c) Per-tile**: two barracks on same tile merge (only relevant if `placement_tile_required: true`).\n3. **Schema**: declare on the lower tier (`barracks.json::stacks_into: \"infantry\"`) or on the upper (`infantry.json::requires_existing: \"barracks\"` + `consumes_existing: true`)? The latter keeps the relationship bidirectional readable.\n\nRecommendation: option **(a) Replacement** with declaration on the upper tier (`requires_existing` + `consumes_existing`). Matches civ-style upgrade slots, reads naturally in the city UI (\"Upgrade Barracks → Infantry\"), avoids per-tile placement complexity for a v1."
},
{
"id": "p1-44",
"title": "Buildings produce units, not the city center — per-building production queues",
"priority": "p1",
"status": "missing",
"scope": "game1",
"owner": null,
"updated_at": "2026-04-29",
"summary": "User direction (2026-04-29): \"it would make sense to build those units in the appropriate buildings rather than the city center.\"\n\nToday every city has **one** production queue (`city.gd:57 production_queue: Array`) where the player picks \"warrior\", \"barracks\", \"wonder\", anything goes through the same FIFO. Building selection just gates the menu — the building doesn't actually *do* anything when production happens.\n\nThe user's model — and the model `PRODUCTION_CHAIN.md` already describes — is that buildings are **producers**:\n\n> Every citizen is a resource pulled between three competing demands:\n> - **Construction** — builds new buildings, upgrades existing ones (investment)\n> - **Buildings** — produces units, research, culture, equipment (output)\n> - **Tiles** — produces food, raw materials, gold (sustenance)\n\nConcretely:\n- **Barracks** has its own queue producing infantry / armory / military_academy lineage units.\n- **Stable** queues cavalry units.\n- **Siege Workshop** queues siege units.\n- **Library** queues sages / cartographers / engineers (science civilians) and accumulates research per turn.\n- **Temple** queues battle priests and accumulates culture/happiness.\n- **Harbor** queues naval units (and gates them — see `p1-33`).\n- **Airfield** queues aerial units.\n- **Construction queue** (city-level, distinct from building queues) builds NEW buildings and upgrades existing ones.\n\nEach producer building runs its queue independently per turn. Citizens / production allocation gets split across buildings (per `PRODUCTION_CHAIN.md` \"Three-Way Tension\"). The city's single global queue dies; build-vs-train becomes a structural distinction not a queue-item distinction.\n\nThis is a major engine refactor. Touches: `mc-city`, `mc-turn`, `city.gd`, `city_screen.gd`, `city_buildable_helper.gd`, save schema, AI's `production.rs` (now picks per producer-building, not per city), and the new themed-unit catalog (battle_priest, sage, bard, merchant, etc. — see `p1-43` open question 1)."
},
{
"id": "p2-06",

View file

@ -43,6 +43,11 @@ var _attack_commitment_turns: int = 0
# Recomputed each turn during _play_turn, read by _next_building + rush-buy.
var _active_attack_mil_count: int = 0
var _in_attack_phase: bool = false
# Stack-of-doom cap: tracks how many times a city has been attacked this turn
# (keyed by city position string). Reset at the start of each player's turn.
# Limits pile-ons so a 10-warrior stack can't one-shot a city in a single turn.
var _city_attacks_this_turn: Dictionary = {}
const MAX_CITY_ATTACKS_PER_TURN: int = 3
# Test harness state (AUTO_PLAY_SEED path)
var _seed: int = 0
@ -943,6 +948,9 @@ func _play_turn() -> void:
if player.researching.is_empty():
_pick_research(player)
# Reset per-turn city attack counter (stack-of-doom cap).
_city_attacks_this_turn.clear()
# Refresh attack-phase signals and stack-sustain telemetry for this turn.
# _attack_commitment_turns reflects prior-turn commitment; rush-buy and
# building scoring both key off it so they respond mid-siege.
@ -2043,10 +2051,16 @@ func _try_attack_adjacent(unit: Variant, game_map: RefCounted) -> void:
for c: Variant in p.cities:
var dist: int = HexUtilsScript.hex_distance(unit.position, c.position)
if dist <= 1:
var city_key: String = "%d,%d" % [c.position.x, c.position.y]
var attacks_so_far: int = _city_attacks_this_turn.get(city_key, 0)
if attacks_so_far >= MAX_CITY_ATTACKS_PER_TURN:
# Stack-of-doom cap: don't pile on beyond the limit this turn.
return
print(" ATTACKING CITY: %s at %s -> city at %s (dist=%d)" % [unit.type_id, unit.position, c.position, dist])
var resolver_script: GDScript = load("res://engine/src/modules/combat/combat_resolver.gd")
var resolver: RefCounted = resolver_script.new()
resolver.resolve(unit, c, game_map, all_units)
_city_attacks_this_turn[city_key] = attacks_so_far + 1
unit.movement_remaining = 0
return

View file

@ -371,26 +371,15 @@ func _process_culture(player: RefCounted, game_map: RefCounted) -> void:
continue
var c: CityScript = city_ref as CityScript
var tile_json: String = BuildableHelperScript.build_tile_yields_json(c, game_map)
# Culture-port to Rust (`process_culture_with_modifier`) attempted in
# R7/R8 but caused seed-divergence vs R6 baseline (R9 parity test
# reproduced R6 exactly when reverted to this GDScript path; R8 with
# Rust port diverged on every seed). Math LOOKED identical but the
# Rust call sequence produces different floating-point intermediate
# results than the GDScript-via-Variant round-trip path. Culture port
# remains TODO — see p1-39. The other Rail-1 ports (gold, research)
# pass parity and stay.
var pre_culture: float = c.get_culture_stored()
var can_expand: bool = c.process_culture(tile_json)
# Rail-1 culture port (p1-39). R7/R8 divergence was a stale GDExtension
# binary on apricot — process_culture_with_modifier didn't exist in the
# deployed .so, GDScript silently errored, culture never accumulated.
# Rust math is identical to the pre-port GDScript path.
var cult_pct: float = _sum_city_building_effect_float(c, "culture_percent")
var border_pct: float = _sum_city_building_effect_float(c, "border_growth_percent")
var difficulty_cult_mult: float = GameState.get_effective_yield_mult(player, "culture")
var total_pct: float = cult_pct + border_pct + (difficulty_cult_mult - 1.0)
if total_pct > 0.0:
var post_culture: float = c.get_culture_stored()
var gained: float = post_culture - pre_culture
if gained > 0.0:
c.set_culture_stored(post_culture + gained * total_pct)
can_expand = c.get_can_expand()
var can_expand: bool = c.process_culture_with_modifier(tile_json, total_pct)
if not can_expand:
continue
# Build candidates JSON for Rust border expansion