diff --git a/engine/src/autoloads/data_loader.gd b/engine/src/autoloads/data_loader.gd index 54722d86..2a40468b 100644 --- a/engine/src/autoloads/data_loader.gd +++ b/engine/src/autoloads/data_loader.gd @@ -23,6 +23,7 @@ const DATA_CATEGORIES: Array[String] = [ "victories", "villages", "wilds", + "npc_buildings", "difficulty", "setup", "ai_personalities", @@ -33,6 +34,11 @@ const DATA_CATEGORIES: Array[String] = [ "ley_line_params", "social_policies", "events", + "world_biomes", + "world_flora", + "world_fauna", + "world_ecosystem", + "world_traits", ] ## Categories stored as raw parsed Dictionaries rather than ID-keyed entries. @@ -44,12 +50,43 @@ const RAW_CATEGORIES: Array[String] = [ "ley_line_params", "social_policies", "events", + "world_biomes", + "world_flora", + "world_fauna", + "world_ecosystem", + "world_traits", ] +## Mapping from world_* categories to their data subdirectory names. +const _WORLD_DIR_MAP: Dictionary = { + "world_biomes": "world/biomes", + "world_flora": "world/flora", + "world_fauna": "world/fauna", + "world_ecosystem": "world/ecosystem", + "world_traits": "world/traits", +} + +# Preload model classes for typed deserialization +const BiomeModelScript = preload("res://engine/src/models/world/biome.gd") +const FloraProfileScript = preload("res://engine/src/models/world/flora_profile.gd") +const TraitSetScript = preload("res://engine/src/models/world/species_traits.gd") +const SubstrateTypeScript = preload("res://engine/src/models/world/substrate.gd") +const MarineFaunaScript = preload("res://engine/src/models/world/marine_fauna.gd") +const LandFaunaScript = preload("res://engine/src/models/world/land_fauna.gd") +const AirFaunaScript = preload("res://engine/src/models/world/air_fauna.gd") + var _active_theme: String = "" var _data: Dictionary = {} ## Raw parsed data for non-ID-keyed categories (e.g. setup, climate_params). var _raw: Dictionary = {} +## Typed ecology model instances keyed by id. +var _biomes: Dictionary = {} # id -> BiomeModel +var _flora_profiles: Dictionary = {} # biome_id -> FloraProfile +var _substrates: Dictionary = {} # id -> SubstrateType +## Typed fauna parameter models (single instances, not collections). +var _marine_fauna: Variant = null # MarineFaunaModel +var _land_fauna: Variant = null # LandFaunaModel +var _air_fauna: Variant = null # AirFaunaModel func _ready() -> void: @@ -57,6 +94,16 @@ func _ready() -> void: _data[category] = {} for category: String in RAW_CATEGORIES: _raw[category] = {} + _clear_typed_caches() + + +func _clear_typed_caches() -> void: + _biomes.clear() + _flora_profiles.clear() + _substrates.clear() + _marine_fauna = null + _land_fauna = null + _air_fauna = null func load_theme(theme_id: String) -> void: @@ -65,18 +112,22 @@ func load_theme(theme_id: String) -> void: _data[category] = {} for category: String in RAW_CATEGORIES: _raw[category] = {} + _clear_typed_caches() var base_path: String = "res://games/%s/data" % theme_id for category: String in DATA_CATEGORIES: - var dir_path: String = "%s/%s" % [base_path, category] - var file_path: String = "%s/%s.json" % [base_path, category] + # world_* categories use nested subdirectory paths + var subdir: String = _WORLD_DIR_MAP.get(category, category) + var dir_path: String = "%s/%s" % [base_path, subdir] + var file_path: String = "%s/%s.json" % [base_path, subdir] if DirAccess.dir_exists_absolute(dir_path): _load_category_dir(category, dir_path) elif FileAccess.file_exists(file_path): _load_json_file(category, file_path) + _deserialize_ecology() _log_load_summary() @@ -386,22 +437,22 @@ func get_terrains_by_flag(flag: String) -> Array: return results -func get_resources_for_terrain(terrain_id: String) -> Array: +func get_resources_for_terrain(biome_id: String) -> Array: var results: Array = [] for entry: Dictionary in _data["resources"].values(): var terrains: Array = entry.get("terrains", []) - if terrain_id in terrains: + if biome_id in terrains: results.append(entry) return results -func get_improvements_for_terrain(terrain_id: String) -> Array: +func get_improvements_for_terrain(biome_id: String) -> Array: var results: Array = [] for entry: Dictionary in _data["improvements"].values(): var valid: Variant = entry.get("valid_terrain", []) - if valid is String and valid == "any_land" and terrain_id != "ocean": + if valid is String and valid == "any_land" and biome_id != "ocean": results.append(entry) - elif valid is Array and terrain_id in valid: + elif valid is Array and biome_id in valid: results.append(entry) return results @@ -463,6 +514,126 @@ func get_villages_config() -> Dictionary: return _get_entry("villages", "villages") +func get_npc_building_type(type_id: String) -> Dictionary: + return _get_entry("npc_buildings", type_id) + + +func get_all_npc_building_types() -> Array: + return _data.get("npc_buildings", {}).values() + + +# -- Ecology: typed accessors -- + + +func get_biome(id: String) -> Variant: + ## Returns a BiomeModel instance or null if not found. + return _biomes.get(id) + + +func get_all_biomes() -> Array: + ## Returns all BiomeModel instances. + return _biomes.values() + + +func get_flora_profile(biome_id: String) -> Variant: + ## Returns a FloraProfile for the given biome or null if not found. + return _flora_profiles.get(biome_id) + + +func get_substrate(id: String) -> Variant: + ## Returns a SubstrateType instance or null if not found. + return _substrates.get(id) + + +func get_all_substrates() -> Array: + ## Returns all SubstrateType instances. + return _substrates.values() + + +func get_marine_fauna_params() -> Variant: + ## Returns the MarineFaunaModel or null. + return _marine_fauna + + +func get_land_fauna_params() -> Variant: + ## Returns the LandFaunaModel or null. + return _land_fauna + + +func get_air_fauna_params() -> Variant: + ## Returns the AirFaunaModel or null. + return _air_fauna + + +func get_trait_definitions() -> Dictionary: + ## Returns trait_definitions from world/traits/trait_definitions.json. + return _raw.get("world_traits", {}).get("trait_definitions", {}) + + +func get_trait_constraints() -> Array: + ## Returns constraint array from world/traits/trait_constraints.json. + var tc: Variant = _raw.get("world_traits", {}).get("trait_constraints", []) + if tc is Array: + return tc + return [] + + +func get_biome_trait_weights(biome_id: String) -> Dictionary: + ## Returns per-biome trait probability weights for species generation. + var all_weights: Dictionary = _raw.get("world_traits", {}).get( + "biome_trait_weights", {} + ) + return all_weights.get(biome_id, {}) + + +func get_food_web_rules() -> Dictionary: + ## Returns food web rules from world/traits/food_web_rules.json. + return _raw.get("world_traits", {}).get("food_web_rules", {}) + + +func get_spawn_rules() -> Array: + ## Returns spawn rules array from world/traits/spawn_rules.json. + var sr: Variant = _raw.get("world_traits", {}).get("spawn_rules", []) + if sr is Array: + return sr + return [] + + +func get_flavor_tables() -> Dictionary: + ## Returns flavor tables from world/traits/flavor.json. + return _raw.get("world_traits", {}).get("flavor", {}) + + +func get_ecosystem_health_params() -> Dictionary: + ## Returns ecosystem health weights and thresholds. + return _raw.get("world_ecosystem", {}).get("health", {}) + + +func get_food_yield_params() -> Dictionary: + ## Returns quality-to-yield curves. + return _raw.get("world_ecosystem", {}).get("food_yield", {}) + + +func get_ecosystem_stability_params() -> Dictionary: + ## Returns reclassification thresholds. + return _raw.get("world_ecosystem", {}).get("stability", {}) + + +func get_vegetation_params() -> Dictionary: + ## Returns flora vegetation growth/decay params. + return _raw.get("world_flora", {}).get("vegetation", {}) + + +func get_succession_params() -> Dictionary: + ## Returns flora succession params. + return _raw.get("world_flora", {}).get("succession", {}) + + +func get_desertification_params() -> Dictionary: + ## Returns desertification thresholds. + return _raw.get("world_flora", {}).get("desertification", {}) + + # -- Internal helpers -- @@ -483,3 +654,106 @@ func _filter_by_field(category: String, field: String, value: String) -> Array: if entry.get(field, "") == value: results.append(entry) return results + + +func _deserialize_ecology() -> void: + ## Convert raw ecology JSON into typed model instances. + _deserialize_biomes() + _deserialize_flora_profiles() + _deserialize_substrates() + _deserialize_fauna_params() + + +func _deserialize_biomes() -> void: + var raw_biomes: Dictionary = _raw.get("world_biomes", {}) + # biomes.json is loaded as raw category — may have "biomes" key or be flat + var biomes_data: Variant = raw_biomes.get("biomes", raw_biomes) + if biomes_data is Dictionary: + # Could be a dict of biome entries keyed by id + for key: String in biomes_data: + var entry: Variant = biomes_data[key] + if entry is Dictionary: + if not entry.has("id"): + entry["id"] = key + _biomes[entry.get("id", key)] = BiomeModelScript.from_dict(entry) + elif biomes_data is Array: + for entry: Variant in biomes_data: + if entry is Dictionary and entry.has("id"): + _biomes[entry["id"]] = BiomeModelScript.from_dict(entry) + if _biomes.size() > 0: + print("DataLoader: Deserialized %d biome models" % _biomes.size()) + + +func _deserialize_flora_profiles() -> void: + # Flora profiles may come from biomes data (flora_climax fields) or separate files + # First check for explicit flora profile data + var raw_flora: Dictionary = _raw.get("world_flora", {}) + + # Check each biome for embedded flora profile data + for biome_id: String in _biomes: + var biome: Variant = _biomes[biome_id] + var climax: Dictionary = biome.flora_climax + if climax.get("canopy", 0.0) > 0.0 or climax.get("undergrowth", 0.0) > 0.0: + var profile_data: Dictionary = { + "biome_id": biome_id, + "canopy_climax": climax.get("canopy", 0.0), + "undergrowth_climax": climax.get("undergrowth", 0.0), + "fungi_climax": climax.get("fungi", 0.0), + } + # Merge with vegetation params if available + var veg: Dictionary = raw_flora.get("vegetation", {}) + if veg.size() > 0: + profile_data["growth_rate"] = veg.get("growth_rate", 0.02) + profile_data["shade_cap"] = veg.get("shade_cap", 0.7) + profile_data["fungi_undergrowth_threshold"] = veg.get( + "fungi_undergrowth_threshold", 0.3 + ) + profile_data["fungi_regrowth_bonus"] = veg.get( + "fungi_regrowth_bonus_cap", 2.0 + ) + _flora_profiles[biome_id] = FloraProfileScript.from_dict(profile_data) + + if _flora_profiles.size() > 0: + print( + "DataLoader: Deserialized %d flora profiles" % _flora_profiles.size() + ) + + +func _deserialize_substrates() -> void: + # Substrates may be in world_biomes raw data under "substrates" key + var raw_biomes: Dictionary = _raw.get("world_biomes", {}) + var substrates_data: Variant = raw_biomes.get("substrates", null) + if substrates_data == null: + return + + if substrates_data is Dictionary: + for key: String in substrates_data: + var entry: Variant = substrates_data[key] + if entry is Dictionary: + if not entry.has("id"): + entry["id"] = key + _substrates[entry.get("id", key)] = SubstrateTypeScript.from_dict( + entry + ) + elif substrates_data is Array: + for entry: Variant in substrates_data: + if entry is Dictionary and entry.has("id"): + _substrates[entry["id"]] = SubstrateTypeScript.from_dict(entry) + + if _substrates.size() > 0: + print("DataLoader: Deserialized %d substrate types" % _substrates.size()) + + +func _deserialize_fauna_params() -> void: + var raw_fauna: Dictionary = _raw.get("world_fauna", {}) + var marine_data: Variant = raw_fauna.get("marine", {}) + if marine_data is Dictionary and marine_data.size() > 0: + _marine_fauna = MarineFaunaScript.from_dict(marine_data) + + var land_data: Variant = raw_fauna.get("land", {}) + if land_data is Dictionary and land_data.size() > 0: + _land_fauna = LandFaunaScript.from_dict(land_data) + + var air_data: Variant = raw_fauna.get("air", {}) + if air_data is Dictionary and air_data.size() > 0: + _air_fauna = AirFaunaScript.from_dict(air_data) diff --git a/engine/src/autoloads/event_bus.gd b/engine/src/autoloads/event_bus.gd index 84634929..b55efa01 100644 --- a/engine/src/autoloads/event_bus.gd +++ b/engine/src/autoloads/event_bus.gd @@ -100,8 +100,6 @@ signal tile_culture_flipped(tile_pos: Vector2i, old_owner: int, new_owner: int) # -- Climate signals -- signal terrain_transformed(tile: Variant, old_type: String, new_type: String) signal quality_changed(tile: Variant, old_quality: int, new_quality: int) -signal corruption_spread(tile: Variant) -signal corruption_healed(tile: Variant) signal wind_recalculated signal weather_spell_cast(spell_id: String, position: Vector2i) signal weather_spell_expired(spell_id: String) @@ -146,7 +144,7 @@ signal tile_clicked(axial: Vector2i) signal climate_phase_changed(new_label: String) ## Request the map renderer to switch its active heatmap overlay. ## mode: "none" | "temperature" | "moisture" | "wind_heatmap" | "weather" -## | "land_value" | "water" | "elevation" | "corruption" +## | "land_value" | "water" | "elevation" signal map_overlay_changed(mode: String) ## Refresh weather footprint highlights; effects from weather.get_active_effects(). signal weather_effects_updated(effects: Array) @@ -172,6 +170,22 @@ signal marine_creature_spawned(tile_pos: Vector2i, creature_type: String) ## A corrupted marine creature was removed from a tile. signal marine_creature_cleared(tile_pos: Vector2i, creature_type: String) +# -- Ecology signals -- +## A lair's habitat degraded below threshold for too long — converted to ruin. +signal lair_abandoned(pos: Vector2i) +## A lair's habitat is thriving — creatures quality-up faster. +signal lair_thriving(pos: Vector2i, new_tier: int) +## Global ecosystem health score updated. +signal ecosystem_health_updated(score: float) +## Tile's biome reclassified by BiomeClassifier. +signal biome_changed(pos: Vector2i, old_biome: String, new_biome: String) +## An individual creature died (from age, starvation, predation, or player). +signal creature_died(pos: Vector2i, species_name: String, quality: int) +## A new creature was born via reproduction. +signal creature_born(pos: Vector2i, species_name: String) +## Tile crossed Q4 threshold — natural wonder emerged from ecology. +signal landmark_formed(pos: Vector2i, name: String, quality: int) + # -- Natural event signals -- signal natural_event_spawned(event_type: String, position: Vector2i, intensity: float) signal natural_event_moved(event_type: String, from_pos: Vector2i, to_pos: Vector2i) diff --git a/engine/src/autoloads/game_state.gd b/engine/src/autoloads/game_state.gd index 8062851a..3c9a541b 100644 --- a/engine/src/autoloads/game_state.gd +++ b/engine/src/autoloads/game_state.gd @@ -4,6 +4,7 @@ extends Node const PlayerScript: GDScript = preload("res://engine/src/entities/player.gd") const GameMapScript: GDScript = preload("res://engine/src/map/game_map.gd") +const BuildingScript: GDScript = preload("res://engine/src/entities/building.gd") const ERA_NAMES: Array[String] = [ "age_of_founding", @@ -80,6 +81,11 @@ var ley_anchors: Array = [] ## Array of LeyNetwork.LeyEdge objects — used by renderer for visualization. var ley_edges: Array = [] +## NPC buildings on the world map (lairs, villages, ruins). Array of Building. +var npc_buildings: Array = [] +## Spatial index: "col,row" -> Array[Building] for quick tile lookups. +var _npc_buildings_by_tile: Dictionary = {} + func initialize_game(settings: Dictionary) -> void: game_settings = DEFAULT_SETTINGS.duplicate() @@ -95,6 +101,8 @@ func initialize_game(settings: Dictionary) -> void: ley_anchors = [] ley_edges = [] diplomacy = {} + npc_buildings = [] + _npc_buildings_by_tile = {} # Create primary map layer (index 0) ( @@ -183,6 +191,46 @@ func get_game_map() -> RefCounted: # Returns GameMap return null +## -- NPC building management -- + + +func add_npc_building(building: RefCounted) -> void: + npc_buildings.append(building) + var key: String = "%d,%d" % [building.position.x, building.position.y] + if not _npc_buildings_by_tile.has(key): + _npc_buildings_by_tile[key] = [] + _npc_buildings_by_tile[key].append(building) + + +func remove_npc_building(building: RefCounted) -> void: + npc_buildings.erase(building) + var key: String = "%d,%d" % [building.position.x, building.position.y] + if _npc_buildings_by_tile.has(key): + _npc_buildings_by_tile[key].erase(building) + + +func get_npc_buildings_at(pos: Vector2i) -> Array: + var key: String = "%d,%d" % [pos.x, pos.y] + return _npc_buildings_by_tile.get(key, []) + + +func get_npc_building_at(pos: Vector2i, type_filter: String = "") -> Variant: + ## Returns the first NPC building at pos, optionally filtered by type_id. Null if none. + var buildings: Array = get_npc_buildings_at(pos) + for b: Variant in buildings: + if type_filter == "" or b.type_id == type_filter: + return b + return null + + +func get_all_npc_buildings_of_type(type_id: String) -> Array: + var result: Array = [] + for b: Variant in npc_buildings: + if b.type_id == type_id: + result.append(b) + return result + + func serialize() -> Dictionary: var data: Dictionary = { "current_theme": current_theme, @@ -195,6 +243,7 @@ func serialize() -> Dictionary: "layers": [], "transit_nodes": transit_nodes, "ley_anchors": _serialize_ley_anchors(), + "npc_buildings": _serialize_npc_buildings(), } # Serialize ascension rituals: {player_index_str -> ritual_dict} @@ -224,6 +273,7 @@ func deserialize(data: Dictionary) -> void: transit_nodes = data.get("transit_nodes", []) wonders_built = data.get("wonders_built", {}).duplicate() _deserialize_ley_anchors(data.get("ley_anchors", [])) + _deserialize_npc_buildings(data.get("npc_buildings", [])) # Deserialize ascension rituals var AscensionRitualScript: GDScript = preload( @@ -358,6 +408,23 @@ func _deserialize_ley_anchors(raw: Array) -> void: ) +func _serialize_npc_buildings() -> Array: + var result: Array = [] + for b: Variant in npc_buildings: + if b is BuildingScript: + result.append(b.to_dict()) + return result + + +func _deserialize_npc_buildings(raw: Array) -> void: + npc_buildings = [] + _npc_buildings_by_tile = {} + for entry: Variant in raw: + if entry is Dictionary: + var b: RefCounted = BuildingScript.from_dict(entry) + add_npc_building(b) + + func rebuild_layer_references() -> void: ## After deserialization, rebuild the primary layer's unit list ## from all player unit arrays. diff --git a/engine/src/autoloads/sprite_manifest.gd b/engine/src/autoloads/sprite_manifest.gd new file mode 100644 index 00000000..db576732 --- /dev/null +++ b/engine/src/autoloads/sprite_manifest.gd @@ -0,0 +1,148 @@ +extends Node +## SQLite-backed sprite manifest for ecology assets. +## Opens sprites.db (read-only) and provides lookup by entity/quality/variant. +## Registered as autoload — warns but does not crash if DB is missing. + +var _db: Variant = null # SQLiteDatabase instance (nullable) +var _db_path: String = "" +var _db_available: bool = false + + +func _ready() -> void: + pass + + +func open_db(theme_id: String) -> void: + ## Open the sprite database for the given game pack. + ## Called after DataLoader.load_theme() to know the active pack. + close_db() + _db_path = "res://games/%s/data/sprites.db" % theme_id + + if not FileAccess.file_exists(_db_path): + push_warning( + "SpriteManifest: sprites.db not found at %s — sprite lookups will return empty" + % _db_path + ) + return + + # SQLite GDExtension: open read-only + var sqlite_class: Variant = ClassDB.instantiate("SQLite") + if sqlite_class == null: + push_warning( + "SpriteManifest: SQLite GDExtension not available — sprite lookups disabled" + ) + return + + _db = sqlite_class + _db.path = _db_path + _db.read_only = true + if not _db.open_db(): + push_warning("SpriteManifest: Failed to open %s" % _db_path) + _db = null + return + + _db_available = true + print("SpriteManifest: Opened %s" % _db_path) + + +func close_db() -> void: + if _db != null and _db_available: + _db.close_db() + _db = null + _db_available = false + + +func is_available() -> bool: + return _db_available + + +func get_sprite( + entity_type: String, + entity_id: String, + quality: int = -1, + variant: int = 0, +) -> String: + ## Returns the asset path for a sprite, or "" if not found. + ## quality = -1 means quality-agnostic lookup (e.g. substrate sprites). + if not _db_available: + return "" + + var query: String + var params: Array + if quality < 0: + query = ( + "SELECT path FROM sprites WHERE entity_type = ? AND entity_id = ? AND variant = ? LIMIT 1" + ) + params = [entity_type, entity_id, variant] + else: + query = ( + "SELECT path FROM sprites WHERE entity_type = ? AND entity_id = ? AND quality = ? AND variant = ? LIMIT 1" + ) + params = [entity_type, entity_id, quality, variant] + + if not _db.query_with_bindings(query, params): + return "" + + var results: Array = _db.query_result + if results.size() == 0: + return "" + return results[0].get("path", "") + + +func has_sprite( + entity_type: String, entity_id: String, quality: int = -1 +) -> bool: + ## Returns true if at least one sprite exists for the given entity. + if not _db_available: + return false + + var query: String + var params: Array + if quality < 0: + query = ( + "SELECT 1 FROM sprites WHERE entity_type = ? AND entity_id = ? LIMIT 1" + ) + params = [entity_type, entity_id] + else: + query = ( + "SELECT 1 FROM sprites WHERE entity_type = ? AND entity_id = ? AND quality = ? LIMIT 1" + ) + params = [entity_type, entity_id, quality] + + if not _db.query_with_bindings(query, params): + return false + return _db.query_result.size() > 0 + + +func validate_sprites() -> Array[String]: + ## Pre-flight validation: checks all substrates, biomes x quality, species. + ## Returns array of missing sprite descriptions. Empty = all good. + var missing: Array[String] = [] + + if not _db_available: + missing.append("sprites.db not available at %s" % _db_path) + return missing + + # Validate substrate sprites + var substrates: Array = DataLoader.get_all_substrates() + for sub: Variant in substrates: + if not has_sprite("substrate", sub.id): + missing.append("substrate:%s" % sub.id) + + # Validate biome sprites across quality ranges + var biomes: Array = DataLoader.get_all_biomes() + for biome: Variant in biomes: + var qr: Vector2i = biome.quality_range + for q in range(qr.x, qr.y + 1): + if not has_sprite("biome", biome.id, q): + missing.append("biome:%s:q%d" % [biome.id, q]) + + if missing.size() > 0: + push_error( + ( + "SpriteManifest: %d missing sprites:\n %s" + % [missing.size(), "\n ".join(missing)] + ) + ) + + return missing diff --git a/engine/src/autoloads/turn_manager.gd b/engine/src/autoloads/turn_manager.gd index bebc8f15..d6a9c885 100644 --- a/engine/src/autoloads/turn_manager.gd +++ b/engine/src/autoloads/turn_manager.gd @@ -28,6 +28,12 @@ const MarineHarvestScript: GDScript = preload( const TurnProcessorScript: GDScript = preload( "res://engine/src/modules/management/turn_processor.gd" ) +const EcosystemScript: GDScript = preload( + "res://engine/src/modules/ecology/ecosystem.gd" +) +const EcologyDBScript: GDScript = preload( + "res://engine/src/modules/ecology/ecology_db.gd" +) enum Phase { NONE, @@ -56,6 +62,8 @@ var climate: RefCounted = ClimateScript.new() # Climate — per-tile physics var climate_effects: RefCounted = ClimateEffectsScript.new() # ClimateEffects — unit damage var diplomacy: RefCounted = DiplomacyScript.new() # Diplomacy — relationship state var marine_harvest: RefCounted = MarineHarvestScript.new() # MarineHarvest — ocean ecology +var ecosystem: RefCounted = EcosystemScript.new() # EcosystemOrchestrator — flora+fauna+quality +var ecology_db: RefCounted = EcologyDBScript.new() # EcologyDB — SQLite creature storage var _processor: RefCounted = null # TurnProcessor — wired in _ready @@ -71,6 +79,8 @@ func _ready() -> void: proc.climate = climate proc.climate_effects = climate_effects proc.marine_harvest = marine_harvest + proc.ecosystem = ecosystem + proc.ecology_db = ecology_db func _on_tech_research_started(tech_id: String, player_index: int) -> void: @@ -220,7 +230,7 @@ func next_player() -> void: # Diplomacy tick: decay timed modifiers and agreements once per full turn. (diplomacy as DiplomacyScript).process_turn() # Protection building effects: write mitigation fields onto city tiles - # BEFORE climate runs so aerosol_mitigation and corruption_resistance are active. + # BEFORE climate runs so aerosol_mitigation is active. var game_map_for_climate: RefCounted = GameState.get_game_map() if game_map_for_climate != null: EconomyScript.apply_protection_effects(game_map_for_climate, GameState.players)