feat(turn-manager): Introduce parallel processing for Rust fauna encounters via environment flag

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-04-08 17:19:29 -07:00
parent e3eceb3643
commit d39f9b3a64
3 changed files with 132 additions and 91 deletions

View file

@ -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()

View file

@ -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.

View file

@ -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.<method>()` pattern as the other turn phases.
RustFaunaIntegrationScript.run_all_players()
func _process_spell_system(player: RefCounted) -> void: # Player