diff --git a/.project/objectives/README.md b/.project/objectives/README.md index 3bb3a107..1b8bb4fd 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -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** | @@ -130,7 +130,8 @@ | [p1-40](p1-40-single-source-of-truth-resources.md) | βœ… done | Collapse data// 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 | diff --git a/.project/objectives/p1-43-building-stacking-upgrade.md b/.project/objectives/p1-43-building-stacking-upgrade.md index 6ea81606..3c615662 100644 --- a/.project/objectives/p1-43-building-stacking-upgrade.md +++ b/.project/objectives/p1-43-building-stacking-upgrade.md @@ -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: `, `consumes_existing: `. 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/.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 ` β†’ β†’ `): + - **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 diff --git a/.project/objectives/p1-44-buildings-as-producers.md b/.project/objectives/p1-44-buildings-as-producers.md new file mode 100644 index 00000000..a5daac6b --- /dev/null +++ b/.project/objectives/p1-44-buildings-as-producers.md @@ -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` β€” `{ : Array }` for each producer building this city owns. +- βœ— `Building` schema gains `produces: Array` 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[]` 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. diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json index 75e1102f..a7bcf1a4 100644 --- a/public/games/age-of-dwarves/data/objectives.json +++ b/public/games/age-of-dwarves/data/objectives.json @@ -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", diff --git a/src/game/engine/scenes/tests/auto_play.gd b/src/game/engine/scenes/tests/auto_play.gd index dfc64034..64c1701a 100644 --- a/src/game/engine/scenes/tests/auto_play.gd +++ b/src/game/engine/scenes/tests/auto_play.gd @@ -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 diff --git a/src/game/engine/src/modules/management/turn_processor.gd b/src/game/engine/src/modules/management/turn_processor.gd index 0bd810a7..1d8175d5 100644 --- a/src/game/engine/src/modules/management/turn_processor.gd +++ b/src/game/engine/src/modules/management/turn_processor.gd @@ -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