Merge remote-tracking branch 'origin/main'

This commit is contained in:
Natalie 2026-04-30 00:49:04 -04:00
commit 868a4b6bd2
3 changed files with 3406 additions and 0 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,164 @@
class_name CombatUtils
extends RefCounted
## Static utility functions for combat: unit/city lookup, death handling, siege capture.
## No damage computation lives here — all combat math is in Rust (GdCombatResolver).
const UnitScript = preload("res://engine/src/entities/unit.gd")
const CityScript = preload("res://engine/src/entities/city.gd")
const HexUtilsScript = preload("res://engine/src/map/hex_utils.gd")
const ItemSystemScript = preload("res://engine/src/modules/management/item_system.gd")
## Get garrison combat unit at a city tile.
static func get_garrison(pos: Vector2i, all_units: Array) -> RefCounted:
for unit: RefCounted in all_units:
if unit is UnitScript and unit.position == pos and unit.is_military():
return unit
return null
## Get all units at a position.
static func get_units_at(pos: Vector2i, all_units: Array) -> Array:
var result: Array = []
for unit: RefCounted in all_units:
if unit is UnitScript and unit.position == pos:
result.append(unit)
return result
## Get all specialist units at a position owned by non-attacker.
static func get_specialists_at(pos: Vector2i, all_units: Array, attacker_owner: int) -> Array:
var result: Array = []
for unit: RefCounted in all_units:
if unit is UnitScript and unit.position == pos and unit.owner != attacker_owner:
if unit.is_specialist():
result.append(unit)
return result
## Get all units adjacent to a position.
static func get_adjacent_units(pos: Vector2i, all_units: Array) -> Array:
var neighbors: Array[Vector2i] = HexUtilsScript.get_neighbors(pos)
var result: Array = []
for neighbor_pos: Vector2i in neighbors:
for unit: RefCounted in all_units:
if unit is UnitScript and unit.position == neighbor_pos:
result.append(unit)
return result
## Get the city at a position (if any) from GameState.
static func get_city_at(pos: Vector2i) -> RefCounted:
for player: RefCounted in GameState.players:
for city: RefCounted in player.cities:
if city is CityScript and city.position == pos:
return city
return null
## Whether this unit is a siege unit (gets bonus vs cities).
static func is_siege_unit(unit: RefCounted) -> bool:
if unit is UnitScript:
return unit.get_combat_type() == "siege"
return false
## Handle a unit's death: check soul gem, drop loot, remove from game state, emit signal.
static func handle_unit_death(unit: RefCounted, killer: RefCounted, all_units: Array) -> void:
if not unit is UnitScript:
return
# Soul Gem check: resurrect at 30% HP instead of dying.
if ItemSystemScript.has_active_item(unit, "soul_gem"):
ItemSystemScript.consume_charge(unit, "soul_gem")
unit.hp = maxi(int(float(unit.get_max_hp()) * 0.3), 1)
return
# Drop equipped items as ground loot before removing the unit.
# ItemSystem.drop_all_loot wants the dying unit's tile, not the whole
# map — passing the map used to Array-type-error the FFI and abort the
# death handler before it could emit unit_destroyed.
var primary_layer: Dictionary = GameState.get_primary_layer()
if not primary_layer.is_empty():
var game_map: RefCounted = primary_layer.get("map")
if game_map != null:
var tile: Resource = game_map.get_tile(unit.position) as Resource
if tile != null:
ItemSystemScript.drop_all_loot(unit, tile)
if unit.owner == -1 and killer != null and killer is UnitScript and killer.owner >= 0:
_roll_wild_creature_loot(unit, killer)
all_units.erase(unit)
if unit.owner >= 0 and unit.owner < GameState.players.size():
var player: RefCounted = GameState.players[unit.owner]
player.units.erase(unit)
if not primary_layer.is_empty():
primary_layer.get("units", []).erase(unit)
EventBus.unit_destroyed.emit(unit, killer)
## Handle city capture: transfer ownership, emit signals, destroy High Archon if capital.
static func capture_city(
city: RefCounted,
attacker: RefCounted,
old_owner: int,
all_units: Array,
) -> void:
var old_player: RefCounted = GameState.get_player(old_owner)
var new_player: RefCounted = GameState.get_player(attacker.owner)
if old_player != null:
old_player.cities.erase(city)
if new_player != null and city not in new_player.cities:
new_player.cities.append(city)
city.owner = attacker.owner
city.is_capital = false
city.captured_turn = GameState.turn_number
for tile_pos: Vector2i in city.owned_tiles:
var layer: Dictionary = GameState.get_primary_layer()
if layer.is_empty():
continue
var map_ref: RefCounted = layer.get("map")
if map_ref == null:
continue
var t: RefCounted = map_ref.get_tile(tile_pos)
if t != null:
t.owner = attacker.owner
if old_player != null:
_destroy_high_archon(old_player, all_units)
EventBus.city_captured.emit(city, old_owner, attacker.owner)
if old_player != null and old_player.cities.is_empty():
EventBus.player_eliminated.emit(old_owner)
static func _roll_wild_creature_loot(victim: RefCounted, killer: RefCounted) -> void:
if killer.owner < 0 or killer.owner >= GameState.players.size():
return
var killer_player: RefCounted = GameState.players[killer.owner]
var creature_type: String = victim.type_id if victim.type_id != "" else victim.unit_id
var turn_seed: int = GameState.game_rng.seed ^ GameState.turn_number
var killer_id: int = hash(killer.id)
var victim_id: int = hash(victim.id)
ItemSystemScript.roll_fauna_drops(creature_type, killer_player, turn_seed, killer_id, victim_id)
## Destroy the High Archon of a player (capital capture penalty).
static func _destroy_high_archon(player: RefCounted, all_units: Array) -> void:
for unit: RefCounted in player.units:
if unit is UnitScript and unit.type_id == "high_archon":
unit.hp = 0
all_units.erase(unit)
player.units.erase(unit)
var primary_layer: Dictionary = GameState.get_primary_layer()
if not primary_layer.is_empty():
primary_layer.get("units", []).erase(unit)
EventBus.unit_destroyed.emit(unit, null)
break

View file

@ -0,0 +1,514 @@
# gdlint: disable=no-elif-return,no-else-return,max-returns,class-definitions-order
extends RefCounted
## End-of-turn processing. Per-player and global _process_* logic.
## DISABLED stubs: _process_culture, _process_golden_age, _process_loot_decay,
## _process_spell_system, _process_government — blocked on empty module stubs.
## `_process_climate` runs the full marine_harvest→climate→weather→effects chain.
## Grep for `DISABLED:` to find every remaining guarded call site.
const PlayerScript: GDScript = preload("res://engine/src/entities/player.gd")
const UnitScript: GDScript = preload("res://engine/src/entities/unit.gd")
const CityScript: GDScript = preload("res://engine/src/entities/city.gd")
const CultureScript: GDScript = preload("res://engine/src/modules/empire/culture.gd")
const EconomyScript: GDScript = preload("res://engine/src/modules/empire/economy.gd")
const HexUtilsScript: GDScript = preload("res://engine/src/map/hex_utils.gd")
const HappinessScript: GDScript = preload("res://engine/src/modules/empire/happiness.gd")
const AscensionRitualScript: GDScript = preload(
"res://engine/src/modules/victory/ascension_ritual.gd"
)
const GovernmentScript: GDScript = preload("res://engine/src/modules/empire/government.gd")
const ItemSystemScript: GDScript = preload("res://engine/src/modules/management/item_system.gd")
const SpellSystemScript: GDScript = preload("res://engine/src/modules/magic/spell_system.gd")
const BuildableHelperScript: GDScript = preload("res://engine/scenes/city/city_buildable_helper.gd")
const MarineHarvestScript: GDScript = preload("res://engine/src/modules/events/marine_harvest.gd")
const ClimateScript: GDScript = preload("res://engine/src/modules/climate/climate.gd")
const ClimateEffectsScript: GDScript = preload(
"res://engine/src/modules/climate/climate_effects.gd"
)
const WeatherScript: GDScript = preload("res://engine/src/modules/climate/weather.gd")
const RustFaunaIntegrationScript: GDScript = preload(
"res://engine/src/modules/management/rust_fauna_integration.gd"
)
const TurnProcessorHelpersScript: GDScript = preload(
"res://engine/src/modules/management/turn_processor_helpers.gd"
)
const TurnProcessorCityHelpersScript: GDScript = preload(
"res://engine/src/modules/management/turn_processor_city_helpers.gd"
)
var unit_manager: RefCounted # UnitManager — set by TurnManager._ready()
var spell_system: RefCounted # SpellSystem — set by TurnManager._ready()
var wild_ai: RefCounted # WildCreatureAI — set via TurnManager.set_wild_creature_ai()
var weather: RefCounted # Weather — set by TurnManager._ready()
var climate: RefCounted # Climate — set by TurnManager._ready()
var climate_effects: RefCounted # ClimateEffects — set by TurnManager._ready()
var marine_harvest: RefCounted # MarineHarvest — set by TurnManager._ready()
func _process_production(player: RefCounted) -> void: # Player
var game_map: RefCounted = GameState.get_game_map() # GameMap
if game_map == null:
return
# Effective per-yield mult composes static difficulty handicap +
# linear-per-turn growth (warcouncil p1-29 H4). Per-player overrides
# (batch testing) handled inside the helper.
var prod_modifier: float = GameState.get_effective_yield_mult(player, "production")
# Unhappy penalty: -25% production when happiness < 0
if player.happiness < 0:
prod_modifier *= 0.75
# Golden Age: +20% production
if player.golden_age_active:
prod_modifier *= 1.2
for city_ref: RefCounted in player.cities:
if not city_ref is CityScript:
continue
var c: CityScript = city_ref as CityScript
var tile_json: String = BuildableHelperScript.build_tile_yields_json(c, game_map)
var yields: Dictionary = c.get_yields(tile_json)
# Add building production bonuses (forge +2, barracks +1, etc.)
var building_prod: int = _sum_city_building_effect(c, "production")
# production_from_hills — +N prod per worked hills tile (first_mineshaft etc.).
var prod_hills: int = _sum_city_building_effect(c, "production_from_hills")
if prod_hills > 0:
for tile_pos: Vector2i in c.get_worked_tiles():
var tile: Resource = game_map.get_tile(tile_pos)
if tile != null and tile.biome_id == "hills":
building_prod += prod_hills
var prod_pct: float = _sum_city_building_effect_float(c, "production_percent")
# Occupation penalty: captured cities produce at 50% for 20 turns.
# Slows the attacker's production-snowball: capturing a city doesn't
# immediately double their output — they must garrison and stabilise first.
const OCCUPATION_TURNS: int = 20
var occupation_mult: float = 1.0
if c.captured_turn >= 0 and GameState.turn_number - c.captured_turn < OCCUPATION_TURNS:
occupation_mult = 0.5
var prod: int = int(
(yields.get("production", 1) + building_prod) * (1.0 + prod_pct) * prod_modifier * occupation_mult
)
# Capture current item before apply_production pops it on completion.
var current: Dictionary = (
c.production_queue.front() as Dictionary if not c.production_queue.is_empty() else {}
)
# Strategic resource gate: territory-ownership check (non-consumable model).
# A player can build a resource-gated unit if they own at least one tile
# with that resource — no slot depletion on build (Civ-6 style).
var pre_type: String = current.get("type", "")
var pre_id: String = current.get("id", "")
if pre_type == "unit" and pre_id != "":
var udata_pre: Dictionary = DataLoader.get_unit(pre_id)
var req_pre: String = str(udata_pre.get("requires_resource", ""))
if req_pre != "" and req_pre != "null" and req_pre != "<null>":
if not _player_owns_resource(player, req_pre):
EventBus.strategic_gate_rejected.emit(
player.index, c.city_name, pre_id, req_pre
)
continue
if not c.apply_production(prod):
continue
var item_type: String = current.get("type", "")
var item_id: String = current.get("id", "")
if item_type == "unit":
var unit: UnitScript = _spawn_unit(item_id, player, c.position)
if unit != null:
var xp_bonus: int = _sum_city_building_effect(c, "unit_xp_start_home_city")
if xp_bonus > 0:
unit.gain_xp(xp_bonus)
EventBus.city_unit_completed.emit(city_ref, unit)
elif item_type == "building":
var tile_pos: Vector2i = current.get("tile_pos", Vector2i(-1, -1)) as Vector2i
c.add_building_at(item_id, tile_pos)
_apply_building_bonuses(c, item_id)
EventBus.city_building_completed.emit(city_ref, item_id)
elif item_type == "item":
var i_data: Dictionary = DataLoader.get_item(item_id)
if not i_data.is_empty():
var charges: int = i_data.get("charges", -1)
if c.get("item_stockpile") is Array:
(
c
. item_stockpile
. append(
{
"item_id": item_id,
"charges_remaining": charges,
}
)
)
# Deduct mana cost on completion.
var mana_cost: Dictionary = i_data.get("cost_mana", {}) as Dictionary
if not mana_cost.is_empty():
var color: String = mana_cost.get("color", "")
var amount: int = mana_cost.get("amount", 0)
if color != "" and amount > 0:
player.mana_pool[color] = (player.mana_pool.get(color, 0.0) - float(amount))
EventBus.item_produced.emit(city_ref, item_id)
func _process_research(player: RefCounted) -> void: # Player
## Rail-1: science accumulation + spell/tech completion check delegated
## to Rust `GdTechWeb::process_research` (warcouncil p1-39 port,
## 2026-04-27). GDScript only assembles input JSON + applies
## completion side-effects (school_locked emit, _form_high_archon,
## tech_researched signal, resource reveals).
if player.researching.is_empty():
return
# Per-yield difficulty multiplier (composed by GameState).
var sci_modifier: float = GameState.get_effective_yield_mult(player, "research")
# Per-city science yields the Rust side will sum.
var game_map: RefCounted = GameState.get_game_map()
var yields_arr: Array = []
if game_map != null:
for city: Variant in player.cities:
if not city is CityScript:
continue
var c: CityScript = city as CityScript
var tile_json: String = BuildableHelperScript.build_tile_yields_json(c, game_map)
var ys: Dictionary = c.get_yields(tile_json)
yields_arr.append({
"science": int(ys.get("science", 0)),
"building_science": _sum_city_building_effect(c, "science"),
"science_percent": _sum_city_building_effect_float(c, "science_percent"),
})
# Player input. spell_cost is set when researching a spell so Rust runs
# the cheap counter branch (no TechWeb lookup).
var spell_data: Dictionary = DataLoader.get_spell(player.researching)
var researching_spell: bool = not spell_data.is_empty()
var researched_arr: Array = Array(player.researched_techs) if player.researched_techs != null else []
var player_dict: Dictionary = {
"researching": str(player.researching),
"research_progress": int(player.research_progress),
"science_per_turn": int(player.science_per_turn),
"researched_techs": researched_arr,
"instant_complete": EnvConfig.get_bool("FORCE_UNLIMITED_RESEARCH"),
}
if researching_spell:
player_dict["spell_cost"] = int(spell_data.get("research_cost", 999999))
var tw: RefCounted = TurnManager.get_tech_web()
if tw == null:
return
var result: Dictionary = tw.process_research(
JSON.stringify(player_dict), JSON.stringify(yields_arr), sci_modifier
)
if result.is_empty():
return
var err: String = str(result.get("error", ""))
if not err.is_empty():
push_warning("p1-39 _process_research: " + err)
return
player.research_progress = int(result.get("new_progress", 0))
player.researching = str(result.get("new_researching", ""))
var completed_spell: String = str(result.get("completed_spell", ""))
if not completed_spell.is_empty():
var sys: SpellSystemScript = spell_system as SpellSystemScript
sys.research_spell(player.index, completed_spell)
return
var completed_tech: String = str(result.get("completed_tech", ""))
if not completed_tech.is_empty():
var old_school_count: int = player.schools.size()
player.add_tech(completed_tech)
# Arcane Lore completion: transform leader into High Archon
if completed_tech == "arcane_lore":
_form_high_archon(player)
# Emit school_locked when the 2nd school is entered for the first time.
if player.schools.size() == 2 and old_school_count < 2:
EventBus.school_locked.emit(player.index, player.schools.duplicate())
EventBus.tech_researched.emit(completed_tech, player.index)
_check_resource_reveals(completed_tech, player.index)
func _check_resource_reveals(completed_tech: String, player_index: int) -> void:
TurnProcessorHelpersScript._check_resource_reveals(completed_tech, player_index)
func _spawn_unit(type_id: String, player: RefCounted, pos: Vector2i) -> UnitScript:
## Minimal unit spawn used when a city finishes building a unit.
## `UnitManager` does not own a `create_unit` method in the current engine;
## the world-map spawner uses the same pattern (new Unit + register into
## player.units and the primary layer). Kept inline here so arena matches
## do not depend on an out-of-scope unit-manager rewrite.
var unit: UnitScript = UnitScript.new(type_id, player.index, pos)
if unit == null:
return null
unit.id = "unit_p%d_%d_%d_%d" % [player.index, pos.x, pos.y, GameState.turn_number]
var data: Dictionary = DataLoader.get_unit(type_id)
unit.display_name = data.get("name", type_id)
player.units.append(unit)
var primary: Dictionary = GameState.get_primary_layer()
var layer_units: Array = primary.get("units", [])
layer_units.append(unit)
primary["units"] = layer_units
EventBus.unit_created.emit(unit, player.index)
return unit
func _process_growth(player: RefCounted) -> void: # Player
var game_map: RefCounted = GameState.get_game_map() # GameMap
if game_map == null:
return
# Apply the Happy / Golden Age +25% growth bonus; preserve the < -10 halt.
# The 0.5× Unhappy tier from mc_happiness::get_growth_modifier is held back
# pending balance-lead sign-off — flipping it would re-tune p1-05 bands.
var growth_modifier: float = 1.25 if player.happiness > 0 else 1.0
var skip_growth: bool = player.happiness < -10
for city: Variant in player.cities:
if not city is CityScript:
continue
var c: CityScript = city as CityScript
if skip_growth:
continue
var tile_json: String = BuildableHelperScript.build_tile_yields_json(c, game_map)
var prev_pop: int = c.population
# Small cities prioritize food growth; pop 4+ uses balanced Default focus
if c.population < 4 and c.has_method("set_focus"):
c.set_focus("food")
c.process_growth(tile_json, growth_modifier)
if c.population != prev_pop:
# Re-assign citizens to tiles after growth or starvation
c.auto_assign_citizens(tile_json)
EventBus.city_grew.emit(c, c.population)
func _sum_city_building_effect(city: CityScript, effect_type: String) -> int:
return TurnProcessorCityHelpersScript.sum_city_building_effect(city, effect_type)
func _sum_city_building_effect_float(city: CityScript, effect_type: String) -> float:
return TurnProcessorCityHelpersScript.sum_city_building_effect_float(city, effect_type)
func _apply_building_bonuses(city: CityScript, building_id: String) -> void:
var bdata: Dictionary = DataLoader.get_building(building_id)
var effects: Array = bdata.get("effects", [])
var owner_player: RefCounted = GameState.get_player(city.owner) if city.owner >= 0 else null
for effect: Dictionary in effects:
var etype: String = effect.get("type", "")
var value: int = int(effect.get("value", 0))
if etype == "hp_bonus" and value > 0:
city.set_max_hp(city.max_hp + value)
city.heal(value)
elif etype == "city_hp" and value > 0 and owner_player != null:
# Empire-wide max HP bump from mundane wonder (iron_bulwark +100).
for other_ref: Variant in owner_player.cities:
if other_ref is CityScript:
var other: CityScript = other_ref as CityScript
other.set_max_hp(other.max_hp + value)
other.heal(value)
elif etype == "free_tech" and value > 0 and owner_player != null:
_grant_free_tech(owner_player, value)
elif etype == "free_golden_age_on_build" and value > 0 and owner_player != null:
owner_player.golden_age_active = true
owner_player.golden_age_turns = HappinessScript.GOLDEN_AGE_DURATION
EventBus.golden_age_started.emit(owner_player.index)
func _grant_free_tech(player: RefCounted, count: int) -> void:
TurnProcessorCityHelpersScript.grant_free_tech(player, count)
func _process_city_healing(player: RefCounted) -> void:
for city_ref: Variant in player.cities:
if city_ref is CityScript:
(city_ref as CityScript).heal_per_turn(GameState.turn_number)
func _process_healing(player: RefCounted) -> void: # Player
var game_map: RefCounted = GameState.get_game_map() # GameMap
if game_map == null:
return
for unit: Variant in player.units:
if not unit is UnitScript:
continue
# Note: terrain power effects (TerrainAffinityScript) and dead-zone
# damage for summoned units are gated on subsystem rewrites that are
# out of scope for arena task #2. Arena healing is purely:
# "if the unit did not move or attack this turn, heal a little".
if unit.hp >= unit.max_hp:
continue
if unit.movement_remaining < unit.get_movement() or unit.has_attacked:
continue
var heal_amount: int = _get_healing_rate(unit, player, game_map)
if heal_amount > 0:
unit.heal(heal_amount)
EventBus.unit_healed.emit(unit, heal_amount)
func _get_healing_rate(unit: RefCounted, player: RefCounted, game_map: RefCounted) -> int:
return TurnProcessorHelpersScript._get_healing_rate(unit, player, game_map)
func _process_mana(player: RefCounted, game_map: RefCounted = null) -> void: # Player
TurnProcessorHelpersScript.process_mana(player, game_map)
func _process_economy(player: RefCounted, game_map: RefCounted) -> void: # Player, GameMap
## Delegate per-turn gold income, upkeep, golden-age bonus, and
## insolvency-driven unit disbanding to `Economy.process_turn`, which
## marshals inputs into the Rust `GdEconomy` bridge (mc-economy). Rail-1:
## no simulation logic in GDScript.
EconomyScript.process_turn(player, game_map)
func _process_culture(player: RefCounted, game_map: RefCounted) -> void:
## Expand city borders using Rust's expand_borders() method. Culture
## accumulation happens inside Rust's process_growth() each turn.
if game_map == null:
return
for city_ref: Variant in player.cities:
if not city_ref is CityScript:
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)
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()
if not can_expand:
continue
# Build candidates JSON for Rust border expansion
var candidates_json: String = _build_border_candidates_json(c, game_map, player)
var claimed: Vector2i = c.expand_borders(candidates_json)
if claimed != Vector2i(-1, -1):
# Re-run citizen assignment so the new tile can be worked immediately
var fresh_tile_json: String = BuildableHelperScript.build_tile_yields_json(c, game_map)
c.auto_assign_citizens(fresh_tile_json)
var tile: Resource = game_map.get_tile(claimed)
if tile != null:
tile.owner = player.index
EventBus.city_border_expanded.emit(c, claimed)
func _build_border_candidates_json(
city: CityScript, game_map: RefCounted, player: RefCounted
) -> String:
return TurnProcessorCityHelpersScript.build_border_candidates_json(city, game_map, player)
func _process_golden_age(player: RefCounted, game_map: RefCounted) -> void: # Player, GameMap
## Delegates to HappinessScript.process_turn, which wraps the mc-happiness
## Rust crate through GdHappiness (GDExtension). Method name kept so
## turn_manager.gd's existing call site does not need to change.
HappinessScript.process_turn(player, game_map)
func _process_wild_creatures() -> void:
## Run wild creature AI for all owner==-1 units once per game turn.
if wild_ai == null:
return
var game_map: RefCounted = GameState.get_game_map()
if game_map == null:
return
wild_ai.process_wild_turn(game_map)
func _process_rust_fauna_encounters() -> void:
## 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
## DISABLED: SpellSystem has no `overworld_queue` property on its current
## stub; the first access aborts the method. Pending-summon spawning also
## depends on a `unit_manager.create_unit` that does not exist. See the
## top-of-file out-of-scope list. Revive once SpellSystem is rebuilt and
## the unit-manager spawn helper ships.
pass
func _process_ascension(player: RefCounted) -> void: # Player
if not player.ascension_active:
return
var ritual: AscensionRitualScript = (
GameState.ascension_rituals.get(player.index, null) as AscensionRitualScript
)
if ritual == null:
return
var sys: SpellSystemScript = spell_system as SpellSystemScript
ritual.tick(player, sys)
func _form_high_archon(player: RefCounted) -> void:
TurnProcessorHelpersScript.form_high_archon(player)
func _process_government(_player: RefCounted) -> void: # Player
## DISABLED: GovernmentScript is an empty stub with no `process_anarchy`.
## See top-of-file out-of-scope list. Revive once government is rebuilt.
## Original body: GovernmentScript.process_anarchy(player)
pass
func _process_climate(game_map: RefCounted) -> void: # GameMap
## Order: marine_harvest → climate → weather → climate_effects.
## * `marine_harvest` seeds ocean_dead_fraction consumed by climate.
## * `climate` runs GdEcologyPhysics + GdClimatePhysics + ecological events
## (restored by p0-31) and leaves the shared GdGridState populated.
## * `weather` (p0-32) reads that grid to derive this turn's storm /
## heat_wave / blizzard events via GdWeatherPhysics.
## * `climate_effects` (p0-32) applies those events back onto tiles + units
## via GdClimateEffectsPhysics.
(marine_harvest as MarineHarvestScript).process_turn(game_map, GameState.players)
(climate as ClimateScript).ocean_dead_fraction = (
(marine_harvest as MarineHarvestScript).ocean_dead_fraction
)
(climate as ClimateScript).process_turn(game_map, GameState.turn_number, GameState.map_seed)
(weather as WeatherScript).process_turn(game_map)
(climate_effects as ClimateEffectsScript).process_turn(
game_map, weather, GameState.players
)
func _process_loot_decay() -> void:
## DISABLED: ItemSystemScript.process_loot_decay expects a different
## GameMap type than the live RefCounted GameMap wrapper, so the call
## raises a type-mismatch error. See top-of-file out-of-scope list.
## Revive once ItemSystem is updated to the current GameMap signature.
pass
func _process_improvements(player: RefCounted) -> void: # Player
TurnProcessorHelpersScript.process_improvements(player)
static func _player_owns_resource(player: RefCounted, resource_id: String) -> bool:
var gm: RefCounted = GameState.get_game_map()
if gm == null:
return false
for city: Variant in player.cities:
for tile_pos: Variant in city.get_owned_tiles():
var tile: RefCounted = gm.get_tile(tile_pos as Vector2i)
if tile != null and str(tile.get("resource_id")) == resource_id:
return true
return false