From d39f9b3a64cfdb1235bb825b5d441e47b62057f6 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 8 Apr 2026 17:19:29 -0700 Subject: [PATCH] =?UTF-8?q?feat(turn-manager):=20=E2=9C=A8=20Introduce=20p?= =?UTF-8?q?arallel=20processing=20for=20Rust=20fauna=20encounters=20via=20?= =?UTF-8?q?environment=20flag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- src/game/engine/src/autoloads/turn_manager.gd | 3 + .../management/rust_fauna_integration.gd | 121 ++++++++++++++++++ .../src/modules/management/turn_processor.gd | 99 ++------------ 3 files changed, 132 insertions(+), 91 deletions(-) create mode 100644 src/game/engine/src/modules/management/rust_fauna_integration.gd diff --git a/src/game/engine/src/autoloads/turn_manager.gd b/src/game/engine/src/autoloads/turn_manager.gd index d6a9c885..a406ee7d 100644 --- a/src/game/engine/src/autoloads/turn_manager.gd +++ b/src/game/engine/src/autoloads/turn_manager.gd @@ -226,6 +226,9 @@ func next_player() -> void: if next_index >= GameState.players.size(): # All players have taken their turn — run wild creatures, then advance var proc := _processor as TurnProcessorScript + # Iter 7k: optional parallel Rust fauna encounter pass. No-op unless + # RUST_FAUNA_ENCOUNTERS env flag is set (off by default). + proc._process_rust_fauna_encounters() proc._process_wild_creatures() # Diplomacy tick: decay timed modifiers and agreements once per full turn. (diplomacy as DiplomacyScript).process_turn() diff --git a/src/game/engine/src/modules/management/rust_fauna_integration.gd b/src/game/engine/src/modules/management/rust_fauna_integration.gd new file mode 100644 index 00000000..a9477175 --- /dev/null +++ b/src/game/engine/src/modules/management/rust_fauna_integration.gd @@ -0,0 +1,121 @@ +class_name RustFaunaIntegration +extends RefCounted +## Iter 7k — gated parallel Rust fauna encounter pass. +## +## When `RUST_FAUNA_ENCOUNTERS` is enabled in EnvConfig (typically via +## `.env.development` for testing), the turn processor runs an additional +## rust-resolved fauna encounter pass adjacent to `_process_wild_creatures`. +## The two flows COEXIST — the existing GDScript `wild_creature_ai` keeps +## moving wild creatures and resolving combat, and the rust pass runs only +## the encounter rolls (via `GdTurnProcessor::step_encounters_only`) for +## the units in each player's roster against the lair NPC buildings. +## +## Off by default. iter 7l will decide whether to make this the canonical +## fauna pipeline (and retire wild_creature_ai's combat layer) or keep +## both coexisting permanently. iter 7k just lands the integration. +## +## Extracted to its own file from `turn_processor.gd` to keep that file +## under the project's 500-line GDScript cap (same extraction pattern as +## `city_buildable_helper.gd` from iter 7c). + +const RustFaunaBridgeScript: GDScript = preload( + "res://engine/src/modules/management/rust_fauna_bridge.gd" +) + +const ENV_KEY: String = "RUST_FAUNA_ENCOUNTERS" +const DEFAULT_LAIR_TIER: int = 5 + + +## Run the Rust fauna encounter resolver against every player's units. +## Gated by `EnvConfig.get_bool("RUST_FAUNA_ENCOUNTERS")` — returns +## immediately if the flag isn't set, so this call is a no-op in the +## default game configuration. +static func run_all_players() -> void: + if not EnvConfig.get_bool(ENV_KEY, false): + return + + var game_map: RefCounted = GameState.get_game_map() + if game_map == null: + return + + var processor: RefCounted = ClassDB.instantiate("GdTurnProcessor") as RefCounted + if processor == null: + push_warning( + "RustFaunaIntegration: GdTurnProcessor not registered " + + "(GDExtension missing or out-of-date)" + ) + return + processor.call("set_victory_city_count", 255) + + var lairs: Array = _build_lair_list() + if lairs.is_empty(): + return + + var grid_size: Vector2i = Vector2i( + int(game_map.width), int(game_map.height) + ) + + for player: Variant in GameState.players: + _run_for_player(processor, player, lairs, grid_size) + + +## Extract the list of lair-stamping tuples from `GameState.npc_buildings`. +## Each tuple is `[col, row, tier, species_id]` — the shape +## `RustFaunaBridge.stamp_lairs` accepts. +## +## Default tier baseline is used because the `Building` entity doesn't +## carry a tier field directly. iter 7l can refine via wilds.json lookup +## if the bench targets need true per-lair tiers. +static func _build_lair_list() -> Array: + var lairs: Array = [] + var species_seed: int = 1 + for b: Variant in GameState.npc_buildings: + var type_id: String = b.type_id + if type_id == "village" or type_id == "ruin": + continue + var pos: Vector2i = b.position + lairs.append([pos.x, pos.y, DEFAULT_LAIR_TIER, species_seed]) + species_seed += 1 + return lairs + + +## Run the bridge for one player's units against the shared lair list. +## Invoked once per player per turn by `run_all_players`. +static func _run_for_player( + processor: RefCounted, + player: Variant, + lairs: Array, + grid_size: Vector2i, +) -> void: + var live_units: Array[Unit] = [] + for u: Variant in player.units: + if u is Unit and u.is_alive(): + live_units.append(u) + if live_units.is_empty(): + return + + var city_positions: Array[Vector2i] = [] + for c: Variant in player.cities: + if c != null: + city_positions.append(c.position) + + var axes: Dictionary = { + "expansion": 3, + "production": 3, + "wealth": 3, + "culture": 2, + } + + var state: RefCounted = RustFaunaBridgeScript.build_state( + axes, live_units, city_positions, grid_size, null + ) + if state == null: + return + RustFaunaBridgeScript.stamp_lairs(state, lairs) + RustFaunaBridgeScript.resolve_fauna_encounters(processor, state, live_units) + # resolve_fauna_encounters has already: + # - set hp=0 on dead units + # - emitted EventBus.unit_destroyed for each + # The caller is responsible for sweeping dead units from collections; + # we leave the references in place so other systems can react to the + # death signal first. diff --git a/src/game/engine/src/modules/management/turn_processor.gd b/src/game/engine/src/modules/management/turn_processor.gd index b6206408..a333dbc3 100644 --- a/src/game/engine/src/modules/management/turn_processor.gd +++ b/src/game/engine/src/modules/management/turn_processor.gd @@ -33,8 +33,8 @@ const WeatherScript: GDScript = preload("res://engine/src/modules/climate/weathe const EcosystemScript: GDScript = preload( "res://engine/src/modules/ecology/ecosystem.gd" ) -const RustFaunaBridgeScript: GDScript = preload( - "res://engine/src/modules/management/rust_fauna_bridge.gd" +const RustFaunaIntegrationScript: GDScript = preload( + "res://engine/src/modules/management/rust_fauna_integration.gd" ) var unit_manager: RefCounted # UnitManager — set by TurnManager._ready() @@ -283,96 +283,13 @@ func _process_wild_creatures() -> void: wild_ai.process_wild_turn(game_map) -# ── Iter 7k: gated parallel Rust fauna encounter pass ────────────────── -# -# When `RUST_FAUNA_ENCOUNTERS` is enabled in EnvConfig (typically via -# `.env.development` for testing), the turn processor runs an additional -# rust-resolved fauna encounter pass adjacent to `_process_wild_creatures`. -# The two flows COEXIST — the existing GDScript wild_creature_ai keeps -# moving wild creatures and resolving combat, and the rust pass runs only -# the encounter rolls (via GdTurnProcessor::step_encounters_only) for the -# units in each player's roster against the lair NPC buildings. -# -# Off by default. iter 7l will decide whether to make this the canonical -# fauna pipeline (and retire wild_creature_ai's combat layer) or keep both -# coexisting permanently. iter 7k just lands the integration. - -const RUST_FAUNA_ENV_KEY: String = "RUST_FAUNA_ENCOUNTERS" -const RUST_FAUNA_DEFAULT_LAIR_TIER: int = 5 - - func _process_rust_fauna_encounters() -> void: - ## Run the Rust fauna encounter resolver against every player's units. - ## Gated by `EnvConfig.is_enabled("RUST_FAUNA_ENCOUNTERS")` — the method - ## returns immediately if the flag isn't set, so this call is a no-op - ## in the default game configuration. - if not EnvConfig.get_bool(RUST_FAUNA_ENV_KEY, false): - return - - var game_map: RefCounted = GameState.get_game_map() - if game_map == null: - return - - var processor: RefCounted = ClassDB.instantiate("GdTurnProcessor") as RefCounted - if processor == null: - push_warning( - "_process_rust_fauna_encounters: GdTurnProcessor not registered " - + "(GDExtension missing or out-of-date)" - ) - return - processor.call("set_victory_city_count", 255) - - # Build the lair list once. Default tier baseline used because the - # building entity doesn't carry a tier field directly — iter 7l can - # refine via wilds.json lookup if the bench targets need it. - var lairs: Array = [] - var species_seed: int = 1 - for b: Variant in GameState.npc_buildings: - var type_id: String = b.type_id - if type_id == "village" or type_id == "ruin": - continue - var pos: Vector2i = b.position - lairs.append([pos.x, pos.y, RUST_FAUNA_DEFAULT_LAIR_TIER, species_seed]) - species_seed += 1 - - if lairs.is_empty(): - return - - var grid_size: Vector2i = Vector2i(int(game_map.width), int(game_map.height)) - - for player: Variant in GameState.players: - var live_units: Array[Unit] = [] - for u: Variant in player.units: - if u is Unit and u.is_alive(): - live_units.append(u) - if live_units.is_empty(): - continue - - var city_positions: Array[Vector2i] = [] - for c: Variant in player.cities: - if c != null: - city_positions.append(c.position) - - var axes: Dictionary = { - "expansion": 3, - "production": 3, - "wealth": 3, - "culture": 2, - } - - var state: RefCounted = RustFaunaBridgeScript.build_state( - axes, live_units, city_positions, grid_size, null - ) - if state == null: - continue - RustFaunaBridgeScript.stamp_lairs(state, lairs) - RustFaunaBridgeScript.resolve_fauna_encounters(processor, state, live_units) - # resolve_fauna_encounters has already: - # - set hp=0 on dead units - # - emitted EventBus.unit_destroyed for each - # The caller (next_player in turn_manager.gd) is responsible for - # sweeping dead units from collections; we leave the references - # in place so other systems can react to the death signal first. + ## Iter 7k: gated parallel Rust fauna encounter pass. + ## Delegates to `RustFaunaIntegration.run_all_players()`, which handles + ## the env-flag check, lair enumeration, and per-player bridge calls. + ## Kept as a method on `TurnProcessor` so `turn_manager.gd` can call it + ## through the same `proc.()` pattern as the other turn phases. + RustFaunaIntegrationScript.run_all_players() func _process_spell_system(player: RefCounted) -> void: # Player