From ddf41217cafa8298f6675bc2657db65355263715 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 25 Mar 2026 23:53:21 -0700 Subject: [PATCH] =?UTF-8?q?feat(climate):=20=E2=9C=A8=20Add=20ecological?= =?UTF-8?q?=20event=20handlers=20and=20evaluation=20utilities=20for=20enha?= =?UTF-8?q?nced=20climate=20simulation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- engine/src/modules/climate/atmosphere.gd | 6 +- engine/src/modules/climate/climate.gd | 15 +- engine/src/modules/climate/climate_base.gd | 206 +++--------------- .../src/modules/climate/climate_spec_eval.gd | 9 +- .../climate/ecological_event_handlers_a.gd | 49 ++--- .../climate/ecological_event_handlers_b.gd | 48 ++-- .../modules/climate/ecological_event_utils.gd | 8 +- 7 files changed, 91 insertions(+), 250 deletions(-) diff --git a/engine/src/modules/climate/atmosphere.gd b/engine/src/modules/climate/atmosphere.gd index 73958a44..6dfffe73 100644 --- a/engine/src/modules/climate/atmosphere.gd +++ b/engine/src/modules/climate/atmosphere.gd @@ -209,7 +209,7 @@ func _step4_humidity(game_map: RefCounted) -> void: hum += ocean_evap * tile.temperature else: hum += soil_rate * tile.moisture - match tile.terrain_id: + match tile.biome_id: "forest", "enchanted_forest": hum += forest_et "jungle": @@ -251,7 +251,7 @@ func _is_windward_of_mountain(tile: Variant, game_map: RefCounted) -> bool: var downwind_tile: Variant = game_map.get_tile(downwind_pos) if downwind_tile == null: return false - return downwind_tile.terrain_id == "mountains" or downwind_tile.elevation > 0.8 + return downwind_tile.biome_id == "mountains" or downwind_tile.elevation > 0.8 func _is_leeward_of_mountain(tile: Variant, game_map: RefCounted) -> bool: @@ -261,7 +261,7 @@ func _is_leeward_of_mountain(tile: Variant, game_map: RefCounted) -> bool: var upwind_tile: Variant = game_map.get_tile(upwind_pos) if upwind_tile == null: return false - return upwind_tile.terrain_id == "mountains" or upwind_tile.elevation > 0.8 + return upwind_tile.biome_id == "mountains" or upwind_tile.elevation > 0.8 # ----------------------------------------------------------------------- diff --git a/engine/src/modules/climate/climate.gd b/engine/src/modules/climate/climate.gd index cc8e8bae..49027bd8 100644 --- a/engine/src/modules/climate/climate.gd +++ b/engine/src/modules/climate/climate.gd @@ -38,8 +38,7 @@ func process_turn(game_map: RefCounted, turn: int = 0, seed: int = 42) -> void: _update_lake_evaporation(game_map) _update_deep_earth_water(game_map) _update_precipitation(game_map) - _check_terrain_evolution(game_map) - _update_corruption(game_map) + # Quality evolution removed — now owned by ecosystem.gd _tick_ley_residue(game_map) EcologicalEventsScript.process_events( game_map, turn, seed, _spec, DataLoader.get_ecological_events() @@ -167,7 +166,7 @@ func _update_lake_evaporation(game_map: RefCounted) -> void: for axial: Vector2i in game_map.tiles: var tile: Variant = game_map.tiles[axial] - var tid: String = tile.terrain_id + var tid: String = tile.biome_id if tid != "lake" and tid != "ocean" and tid != "coast": continue @@ -192,7 +191,7 @@ func _update_lake_evaporation(game_map: RefCounted) -> void: var hop_tile: Variant = game_map.tiles.get(hop_pos) if hop_tile == null: break - var hop_tid: String = hop_tile.terrain_id + var hop_tid: String = hop_tile.biome_id # Stop if we hit another water tile (no double-injection) if hop_tid == "ocean" or hop_tid == "coast" or hop_tid == "lake": break @@ -225,7 +224,7 @@ func _update_deep_earth_water(game_map: RefCounted) -> void: for axial: Vector2i in game_map.tiles: var tile: Variant = game_map.tiles[axial] - if tile.terrain_id == "volcano": + if tile.biome_id == "volcano": tile.moisture = clampf(tile.moisture + vol_self, 0.0, 1.0) for nb_pos: Vector2i in HexUtilsScript.get_neighbors(axial): var nb: Variant = game_map.tiles.get(nb_pos) @@ -301,7 +300,7 @@ func _compute_global_stats(game_map: RefCounted) -> void: var count: int = 0 for axial: Vector2i in game_map.tiles: var tile: Variant = game_map.tiles[axial] - if tile.terrain_id != "ocean": + if tile.biome_id != "ocean": total += tile.temperature count += 1 global_avg_temp = total / float(count) if count > 0 else 0.5 @@ -316,7 +315,7 @@ func _compute_global_stats(game_map: RefCounted) -> void: var dead_count: int = 0 for axial: Vector2i in game_map.tiles: var tile: Variant = game_map.tiles[axial] - if tile.terrain_id != "coast": + if tile.biome_id != "coast": continue coast_count += 1 if tile.temperature > bleach_thresh: @@ -381,7 +380,7 @@ func _ensure_ocean_dist(game_map: RefCounted) -> void: # Seed BFS from all water tiles (distance 0) var queue: Array[Vector2i] = [] for axial: Vector2i in game_map.tiles: - var tid: String = game_map.tiles[axial].terrain_id + var tid: String = game_map.tiles[axial].biome_id if tid == "ocean" or tid == "coast" or tid == "lake" or tid == "inland_sea": _ocean_dist_cache[axial] = 0 queue.append(axial) diff --git a/engine/src/modules/climate/climate_base.gd b/engine/src/modules/climate/climate_base.gd index 5dc0eb00..c20731db 100644 --- a/engine/src/modules/climate/climate_base.gd +++ b/engine/src/modules/climate/climate_base.gd @@ -19,26 +19,23 @@ const LeyNetworkScript: GDScript = preload("res://engine/src/modules/ley/ley_net # Fallback defaults — used only when climate_params.json absent (logs warning) const _DEFAULTS: Dictionary = { "wind_conductivity": 0.1, - "energy_scale": 0.01, - "equilibrium_relaxation": 0.05, + "energy_scale": 0.005, + "equilibrium_relaxation": 0.08, "evaporation_rate": 0.05, "moisture_transport": 0.15, "precipitation_threshold": 0.7, - "moisture_decay": 0.98, - "moisture_relaxation": 0.02, - "ocean_evaporation_hops": 3, + "moisture_decay": 0.995, + "moisture_relaxation": 0.04, + "ocean_evaporation_hops": 4, "ocean_evaporation_hop_decay": 0.5, - "atmospheric_loss_rate": 0.001, + "atmospheric_loss_rate": 0.0003, "quality_up_threshold": 10, "quality_down_threshold": 5, - "corruption_spread_rate": 0.02, - "corruption_flip_threshold": 0.5, - "corruption_decay_rate": 0.004, - "corruption_heal_rate": 0.008, - "corruption_heal_threshold": 0.15, "lake_thermal_conductivity": 0.05, "river_moisture_transport": 0.075, "mountain_rain_shadow_block": 0.9, + "solar_min": 0.15, + "solar_max": 0.70, } const _DEW_DEFAULTS: Dictionary = { @@ -66,7 +63,7 @@ var _params: Dictionary = {} var _spec: Dictionary = {} var _params_loaded: bool = false -# Per-terrain data cache (albedo, evapotranspiration) keyed by terrain_id string +# Per-terrain data cache (albedo, evapotranspiration) keyed by biome_id string var _terrain_cache: Dictionary = {} # Ocean proximity cache: axial → int (hex distance to nearest water tile). @@ -106,7 +103,7 @@ func _update_temperatures(game_map: RefCounted) -> void: var solar: float = solar_by_row[clampi(offset.y, 0, h - 1)] var current_temp: float = old_temp[axial] - var terrain_data: Dictionary = _get_terrain_data(tile.terrain_id) + var terrain_data: Dictionary = _get_terrain_data(tile.biome_id) var albedo: float = terrain_data.get("albedo", 0.4) var net_solar: float = solar * (1.0 - albedo) * energy_scale @@ -141,7 +138,7 @@ func _update_lake_thermal_effects(game_map: RefCounted) -> void: for axial: Vector2i in game_map.tiles: var tile: Variant = game_map.tiles[axial] - if tile.terrain_id != "lake": + if tile.biome_id != "lake": continue var lake_temp: float = tile.temperature for nb_pos: Vector2i in HexUtilsScript.get_neighbors(axial): @@ -184,7 +181,7 @@ func _update_moisture_wind(game_map: RefCounted) -> void: if old_moisture.has(upwind_pos): var upwind_tile: Variant = game_map.tiles.get(upwind_pos) var upwind_is_mountain: bool = ( - upwind_tile != null and upwind_tile.terrain_id == "mountains" + upwind_tile != null and upwind_tile.biome_id == "mountains" ) var block: float = rain_shadow_block if upwind_is_mountain else 0.0 transported = ( @@ -192,7 +189,7 @@ func _update_moisture_wind(game_map: RefCounted) -> void: ) # Evapotranspiration and magic forcing are per-tile local effects — safe to add here - var terrain_data: Dictionary = _get_terrain_data(tile.terrain_id) + var terrain_data: Dictionary = _get_terrain_data(tile.biome_id) var evapotrans: float = terrain_data.get("evapotranspiration", 0.0) # Moisture equilibrium relaxation — pull toward climate baseline (like temperature) @@ -223,179 +220,26 @@ func _update_moisture_wind(game_map: RefCounted) -> void: # -- Step 8: Terrain quality evolution -- -func _check_terrain_evolution(game_map: RefCounted) -> void: - var up_thresh: int = _params.get("quality_up_threshold", _DEFAULTS["quality_up_threshold"]) - var down_thresh: int = _params.get( - "quality_down_threshold", _DEFAULTS["quality_down_threshold"] - ) - - for axial: Vector2i in game_map.tiles: - var tile: Variant = game_map.tiles[axial] - var tid: String = tile.terrain_id - - # Natural wonders are geological formations — quality evolves but terrain doesn't flip - if tile.is_natural_wonder(): - continue - - # Water, corrupted, and fixed-form terrains don't evolve via climate - if ( - tid == "ocean" - or tid == "coast" - or tid == "lake" - or tid == "corrupted_land" - or tid == "volcano" - ): - continue - - var ideal: String = _ideal_terrain(tile) - - if ideal == tid: - tile.quality_progress += 1 - if tile.quality_progress >= up_thresh: - tile.quality_progress = 0 - if tile.quality < 5: - var old_q: int = tile.quality - tile.quality += 1 - EventBus.quality_changed.emit(tile, old_q, tile.quality) - else: - tile.quality_progress -= 1 - if tile.quality_progress <= -down_thresh: - tile.quality_progress = 0 - if tile.quality > 1: - var old_q: int = tile.quality - tile.quality -= 1 - EventBus.quality_changed.emit(tile, old_q, tile.quality) - else: - # Quality at floor — terrain flips one step along its chain - var old_type: String = tid - tile.terrain_id = ideal - tile.quality = 1 - tile.quality_progress = 0 - EventBus.terrain_transformed.emit(tile, old_type, ideal) - - -# -- Step 9: Corruption spreading -- - - -func _update_corruption(game_map: RefCounted) -> void: - var spread_rate: float = _params.get( - "corruption_spread_rate", _DEFAULTS["corruption_spread_rate"] - ) - var flip_threshold: float = _params.get( - "corruption_flip_threshold", _DEFAULTS["corruption_flip_threshold"] - ) - var decay_rate: float = _params.get("corruption_decay_rate", _DEFAULTS["corruption_decay_rate"]) - var heal_rate: float = _params.get("corruption_heal_rate", _DEFAULTS["corruption_heal_rate"]) - var heal_threshold: float = _params.get( - "corruption_heal_threshold", _DEFAULTS["corruption_heal_threshold"] - ) - - # Accumulate pressure increments into a separate dict — one O(n) read pass, - # then one O(n) write pass. Avoids double-counting and in-place mutation. - var pressure_deltas: Dictionary = {} # axial → float - - for axial: Vector2i in game_map.tiles: - var tile: Variant = game_map.tiles[axial] - if tile.corruption_pressure <= 0.0: - continue - # terrain_power corruption_spread_modifier on the SOURCE tile scales spread rate. - # e.g. jungle = 0.5x (resists spread), swamp = 1.5x (accelerates spread). - var source_modifier: float = TerrainAffinityScript.get_corruption_spread_modifier( - tile.terrain_id - ) - var base_spread: float = spread_rate * source_modifier - for nb_pos: Vector2i in HexUtilsScript.get_neighbors(axial): - if not game_map.tiles.has(nb_pos): - continue - var nb: Variant = game_map.tiles[nb_pos] - if nb.terrain_id == "corrupted_land": - continue - # Ley line channeling: spread rate is modified by the ley properties of the - # receiving tile. Death ley = 3x, generic ley edge = 2x, - # Nature/Life ley = 0.5x (resists). Off-ley = no channeling modifier. - var ley_mult: float = _ley_channeling_mult(nb) - # Moisture resistance: high-moisture terrain (jungle, swamp, water) resists - # corruption biologically. Marine life (fish, reefs, algae) can still carry - # corruption through water — so water isn't immune, just dampened. - var moisture_resist: float = 1.0 - nb.moisture * 0.4 - # City protection buildings write corruption_resistance_pct to scale down spread - var corruption_resist: float = 1.0 - clampf(nb.corruption_resistance_pct, 0.0, 1.0) - pressure_deltas[nb_pos] = ( - pressure_deltas.get(nb_pos, 0.0) - + base_spread * ley_mult * moisture_resist * corruption_resist - ) - - # Apply pressure deltas + natural decay + Life/Nature ley active healing - for axial: Vector2i in game_map.tiles: - var tile: Variant = game_map.tiles[axial] - var incoming: float = pressure_deltas.get(axial, 0.0) - - # Natural pressure decay — corruption dissipates without active feeding sources - var drain: float = decay_rate - - # Life/Nature ley lines actively heal (bonus drain, capped at 3 stacks) - if tile.ley_line_count > 0 and (tile.ley_school == "life" or tile.ley_school == "nature"): - drain += heal_rate * minf(float(tile.ley_line_count), 3.0) - - if incoming == 0.0 and tile.corruption_pressure == 0.0: - continue - tile.corruption_pressure = clampf(tile.corruption_pressure + incoming - drain, 0.0, 1.0) - - # Terrain flip: pressure crosses threshold → corrupted_land - # Water tiles (ocean, lake, inland_sea, coast) never flip terrain — corruption - # in water expresses through the marine ecosystem (reef_health, fish_stock) instead. - for axial: Vector2i in game_map.tiles: - var tile: Variant = game_map.tiles[axial] - var tid: String = tile.terrain_id - if ( - tid == "corrupted_land" - or tid == "ocean" - or tid == "lake" - or tid == "inland_sea" - or tid == "coast" - ): - continue - if tile.corruption_pressure > flip_threshold: - if tile.original_terrain_id == "": - tile.original_terrain_id = tile.terrain_id - tile.terrain_id = "corrupted_land" - EventBus.corruption_spread.emit(tile) - - # Marine corruption: coast tiles with elevated pressure degrade reef and fish stock - # (corrupted fish and algae carry corruption through the marine ecosystem) - for axial: Vector2i in game_map.tiles: - var tile: Variant = game_map.tiles[axial] - if tile.terrain_id != "coast": - continue - if tile.corruption_pressure > 0.2: - tile.reef_health = maxf(0.0, tile.reef_health - tile.corruption_pressure * 0.015) - if tile.fish_stock > 0: - tile.fish_stock = maxf(0.0, tile.fish_stock - tile.corruption_pressure * 0.01) - - # Terrain healing: corrupted tile's pressure falls below heal threshold → restore - for axial: Vector2i in game_map.tiles: - var tile: Variant = game_map.tiles[axial] - if tile.terrain_id != "corrupted_land" or tile.corruption_pressure > heal_threshold: - continue - var original: String = tile.original_terrain_id - var restore_to: String = original if original != "" else "grassland" - tile.terrain_id = restore_to - tile.original_terrain_id = "" - EventBus.corruption_healed.emit(tile) # -- Internal helpers -- -func _get_terrain_data(terrain_id: String) -> Dictionary: - if not _terrain_cache.has(terrain_id): - _terrain_cache[terrain_id] = DataLoader.get_terrain(terrain_id) - return _terrain_cache[terrain_id] +func _get_terrain_data(biome_id: String) -> Dictionary: + if not _terrain_cache.has(biome_id): + _terrain_cache[biome_id] = DataLoader.get_terrain(biome_id) + return _terrain_cache[biome_id] func _solar_by_latitude(row: int, center_row: float) -> float: - ## Solar input at a given map row: 1.0 at equator (center), 0.0 at poles. - return 1.0 - absf((float(row) - center_row) / center_row) + ## Solar input at a given map row, clamped to habitable range. + ## Raw latitude factor: 1.0 at equator, 0.0 at poles. + ## Mapped via solar_min/solar_max params to produce stable equilibrium + ## temperatures that support reef survival and biome diversity. + var raw: float = 1.0 - absf((float(row) - center_row) / center_row) + var solar_min: float = _params.get("solar_min", _DEFAULTS.get("solar_min", 0.15)) + var solar_max: float = _params.get("solar_max", _DEFAULTS.get("solar_max", 0.70)) + return solar_min + (solar_max - solar_min) * raw func _upwind_pos(axial: Vector2i, wind_dir: int) -> Vector2i: diff --git a/engine/src/modules/climate/climate_spec_eval.gd b/engine/src/modules/climate/climate_spec_eval.gd index 0d28ad09..8cd64004 100644 --- a/engine/src/modules/climate/climate_spec_eval.gd +++ b/engine/src/modules/climate/climate_spec_eval.gd @@ -4,20 +4,21 @@ extends RefCounted ## Pure logic — no side effects, no state mutation. Used by Climate and the transpiler. const MapGeneratorScript: GDScript = preload("res://engine/src/generation/map_generator.gd") +const BiomeClassifierScript = preload("res://engine/src/models/world/biome_classifier.gd") static func ideal_terrain(tile: Variant, spec: Dictionary) -> String: ## Determine the climate-ideal terrain type for a tile. Reads transition rules ## from climate_spec.json terrain_transitions. If a terrain has no spec entry, - ## falls back to classify_terrain. - var tid: String = tile.terrain_id + ## falls back to BiomeClassifier.classify(). + var tid: String = tile.biome_id var temp: float = tile.temperature var moist: float = tile.moisture var transitions: Dictionary = spec.get("terrain_transitions", {}) var rules: Array = transitions.get(tid, []) if rules.is_empty(): - return MapGeneratorScript.classify_terrain(temp, moist, tile.elevation) + return BiomeClassifierScript.classify(tile) for rule: Variant in rules: if not rule is Dictionary: @@ -25,7 +26,7 @@ static func ideal_terrain(tile: Variant, spec: Dictionary) -> String: if eval_condition(rule.get("condition", ""), temp, moist, tile.elevation): var becomes: String = rule.get("becomes", "") if becomes == "classify": - return MapGeneratorScript.classify_terrain(temp, moist, tile.elevation) + return BiomeClassifierScript.classify(tile) return becomes return tid diff --git a/engine/src/modules/climate/ecological_event_handlers_a.gd b/engine/src/modules/climate/ecological_event_handlers_a.gd index 56015be6..8db61a72 100644 --- a/engine/src/modules/climate/ecological_event_handlers_a.gd +++ b/engine/src/modules/climate/ecological_event_handlers_a.gd @@ -27,7 +27,7 @@ static func process_volcanic( var center: Variant = EcoUtils.pick_land(game_map, w, h, turn_seed, channel) if center == null or center.is_natural_wonder(): return - center.terrain_id = "volcano" + center.biome_id = "volcano" center.quality = 1 center.quality_progress = 0 @@ -41,8 +41,8 @@ static func process_volcanic( var t: Variant = game_map.tiles[axial] if t == center or t.is_natural_wonder(): continue - if t.terrain_id != "ocean" and t.terrain_id != "coast": - t.terrain_id = scorched_terrain + if t.biome_id != "ocean" and t.biome_id != "coast": + t.biome_id = scorched_terrain t.moisture = maxf(0.0, t.moisture - moisture_loss) t.quality = 1 scorched += 1 @@ -105,13 +105,12 @@ static func process_impact( var t: Variant = game_map.tiles[naxial] if t.is_natural_wonder(): continue - if t.terrain_id != "ocean" and t.terrain_id != "coast": - t.terrain_id = "desert" + if t.biome_id != "ocean" and t.biome_id != "coast": + t.biome_id = "desert" t.elevation = maxf(0.0, t.elevation - elev_loss) t.moisture = 0.0 t.quality = 1 t.resource_id = "" - t.corruption_pressure = 1.0 # Phase 2: Global aerosol for naxial: Vector2i in game_map.tiles: game_map.tiles[naxial].sulfate_aerosol += aerosol_strength @@ -120,18 +119,18 @@ static func process_impact( var t: Variant = game_map.tiles[naxial] if t.is_natural_wonder(): continue - if t.terrain_id in living_terrains: + if t.biome_id in living_terrains: t.quality = maxi(1, t.quality - biome_kill_quality) - if t.terrain_id == "jungle": - t.terrain_id = "grassland" + if t.biome_id == "jungle": + t.biome_id = "grassland" t.moisture = maxf(0.0, t.moisture - 0.15) - elif t.terrain_id == "enchanted_forest": - t.terrain_id = "forest" - elif t.terrain_id == "swamp": - t.terrain_id = "desert" + elif t.biome_id == "enchanted_forest": + t.biome_id = "forest" + elif t.biome_id == "swamp": + t.biome_id = "desert" t.moisture = 0.0 # Phase 4: Impact site becomes mana node - center.terrain_id = "mana_node" + center.biome_id = "mana_node" center.quality = 5 EcoUtils.set_wonder_anchor(center, tier_cfg, 5, ["death", "chaos"]) EcoUtils.spawn_resource(center, tier_cfg) @@ -159,13 +158,13 @@ static func process_impact( if elev_delta > 0.0: # Raises terrain (e.g. mountain ridge) var crater_terrain: String = tier_cfg.get("crater_terrain", "mountains") - center.terrain_id = crater_terrain + center.biome_id = crater_terrain center.elevation = minf(1.0, center.elevation + elev_delta) center.moisture = maxf(0.0, center.moisture - 0.1) center.quality = 3 else: # Depresses terrain (crater lake or barren) - center.terrain_id = "lake" if center.elevation < 0.15 else "desert" + center.biome_id = "lake" if center.elevation < 0.15 else "desert" center.elevation = maxf(0.0, center.elevation + elev_delta) center.quality = 1 @@ -194,7 +193,7 @@ static func process_impact( var t: Variant = game_map.tiles[naxial] if t.is_natural_wonder() or t.resource_id != "": continue - if t.terrain_id == "ocean" or t.terrain_id == "coast": + if t.biome_id == "ocean" or t.biome_id == "coast": continue var tile_roll: float = EcoUtils.hash_noise( float(scatter_idx), float(naxial.x + naxial.y), turn_seed + 7.0 @@ -282,7 +281,7 @@ static func process_wildfire( var forest_types: Array = cat_cfg.get( "target_terrain", ["forest", "jungle", "boreal_forest", "enchanted_forest"] ) - if not (center.terrain_id in forest_types): + if not (center.biome_id in forest_types): return var radius: int = tier_cfg.get("radius", 1) @@ -296,9 +295,9 @@ static func process_wildfire( var t: Variant = game_map.tiles[axial] if t.is_natural_wonder(): continue - if t.terrain_id in forest_types: + if t.biome_id in forest_types: if becomes != null and str(becomes) != "null": - t.terrain_id = str(becomes) + t.biome_id = str(becomes) if quality_loss > 0: t.quality = maxi(1, t.quality - quality_loss) t.quality_progress = 0 @@ -343,7 +342,7 @@ static func process_drought( if not game_map.tiles.has(axial): continue var t: Variant = game_map.tiles[axial] - if t.terrain_id != "ocean" and t.terrain_id != "coast": + if t.biome_id != "ocean" and t.biome_id != "coast": t.moisture = maxf(0.0, t.moisture - moisture_loss) var tier_name: String = tier_cfg.get("name", "Drought") @@ -373,7 +372,7 @@ static func process_plague( var target_terrain: Array = tier_cfg.get( "target_terrain", ["grassland", "plains", "forest", "jungle", "enchanted_forest"] ) - if not (center.terrain_id in target_terrain): + if not (center.biome_id in target_terrain): return var radius: int = tier_cfg.get("radius", 2) @@ -386,11 +385,11 @@ static func process_plague( var t: Variant = game_map.tiles[axial] if t.is_natural_wonder(): continue - if not (t.terrain_id in target_terrain): + if not (t.biome_id in target_terrain): continue t.quality = maxi(1, t.quality - quality_loss) - if terrain_downgrade.has(t.terrain_id): - t.terrain_id = str(terrain_downgrade[t.terrain_id]) + if terrain_downgrade.has(t.biome_id): + t.biome_id = str(terrain_downgrade[t.biome_id]) affected += 1 if affected == 0: diff --git a/engine/src/modules/climate/ecological_event_handlers_b.gd b/engine/src/modules/climate/ecological_event_handlers_b.gd index a008b233..95f5a827 100644 --- a/engine/src/modules/climate/ecological_event_handlers_b.gd +++ b/engine/src/modules/climate/ecological_event_handlers_b.gd @@ -56,7 +56,7 @@ static func _apply_magical_positive( ) -> void: var center_terrain: Variant = tier_cfg.get("center_terrain", null) if center_terrain != null and str(center_terrain) != "null": - center.terrain_id = str(center_terrain) + center.biome_id = str(center_terrain) var center_quality: int = tier_cfg.get("center_quality", 3) center.quality = center_quality @@ -70,13 +70,13 @@ static func _apply_magical_positive( var t: Variant = game_map.tiles[axial] if t == center or t.is_natural_wonder(): continue - if t.terrain_id == "ocean" or t.terrain_id == "coast": + if t.biome_id == "ocean" or t.biome_id == "coast": continue if moisture_gain > 0.0: t.moisture = minf(1.0, t.moisture + moisture_gain) if grassland_upgrade != null and str(grassland_upgrade) != "null": - if t.terrain_id == "grassland" or t.terrain_id == "plains": - t.terrain_id = str(grassland_upgrade) + if t.biome_id == "grassland" or t.biome_id == "plains": + t.biome_id = str(grassland_upgrade) # Location-based school selection for wellspring var schools: Array[String] = [] @@ -122,26 +122,22 @@ static func _apply_magical_negative( ) -> void: var center_terrain: Variant = tier_cfg.get("center_terrain", null) if center_terrain != null and str(center_terrain) != "null": - center.terrain_id = str(center_terrain) + center.biome_id = str(center_terrain) var center_quality: int = tier_cfg.get("center_quality", 1) center.quality = center_quality var radius: int = tier_cfg.get("radius", 0) - var corruption_gain: float = tier_cfg.get("corruption_gain", 0.0) var heat_delta: float = tier_cfg.get("heat_delta", 0.0) - if radius > 0: + if radius > 0 and heat_delta != 0.0: for axial: Vector2i in EcoUtils.tiles_in_radius(center, radius): if not game_map.tiles.has(axial): continue var t: Variant = game_map.tiles[axial] if t == center or t.is_natural_wonder(): continue - if t.terrain_id == "ocean" or t.terrain_id == "coast": + if t.biome_id == "ocean" or t.biome_id == "coast": continue - if corruption_gain > 0.0: - t.corruption_pressure = minf(1.0, t.corruption_pressure + corruption_gain) - if heat_delta != 0.0: - t.magic_heat_delta += heat_delta + t.magic_heat_delta += heat_delta var raw_schools: Array = tier_cfg.get("anchor_schools", []) var default_schools: Array = [] @@ -156,7 +152,7 @@ static func _apply_magical_negative( turn, event_type, center, - "%s — T%d anchor, corruption spreads" % [tier_name, center.wonder_tier] + "%s — T%d anchor" % [tier_name, center.wonder_tier] ) ) @@ -179,7 +175,7 @@ static func process_marine( if not game_map.tiles.has(axial): return var center: Variant = game_map.tiles[axial] - if center.terrain_id != "coast" and center.terrain_id != "ocean": + if center.biome_id != "coast" and center.biome_id != "ocean": return var radius: int = tier_cfg.get("radius", 2) @@ -192,19 +188,16 @@ static func process_marine( var fish_stock_loss: int = tier_cfg.get("fish_stock_loss", 0) var fish_stock_kill: bool = tier_cfg.get("fish_stock_kill", false) var moisture_gain: float = tier_cfg.get("moisture_gain", 0.0) - var coast_corruption: float = tier_cfg.get("coast_corruption", 0.0) for naxial: Vector2i in EcoUtils.tiles_in_radius(center, radius): if not game_map.tiles.has(naxial): continue var t: Variant = game_map.tiles[naxial] - if t.terrain_id == "coast": + if t.biome_id == "coast": if reef_health_gain > 0.0: t.reef_health = minf(1.0, t.reef_health + reef_health_gain) if reef_health_loss > 0.0: t.reef_health = maxf(0.0, t.reef_health - reef_health_loss) - if coast_corruption > 0.0: - t.corruption_pressure = minf(1.0, t.corruption_pressure + coast_corruption) if t.fish_stock >= 0: if fish_stock_gain > 0: t.fish_stock = mini(100, t.fish_stock + fish_stock_gain) @@ -212,7 +205,7 @@ static func process_marine( t.fish_stock = maxi(0, t.fish_stock - fish_stock_loss) elif fish_stock_kill: t.fish_stock = 0 - elif t.terrain_id == "ocean": + elif t.biome_id == "ocean": if moisture_gain > 0.0: t.moisture = minf(1.0, t.moisture + moisture_gain) if t.fish_stock >= 0: @@ -222,7 +215,7 @@ static func process_marine( t.fish_stock = maxi(0, t.fish_stock - fish_stock_loss) elif fish_stock_kill: t.fish_stock = 0 - elif moisture_gain > 0.0 and t.terrain_id != "ocean": + elif moisture_gain > 0.0 and t.biome_id != "ocean": t.moisture = minf(1.0, t.moisture + moisture_gain) var polarity: String = "enriches" if positive else "harms" @@ -295,7 +288,7 @@ static func process_glacial( if warm_delta != 0.0: t.magic_heat_delta += warm_delta t.glacial_forcing += warm_delta - if moisture_surge > 0.0 and t.terrain_id == "coast": + if moisture_surge > 0.0 and t.biome_id == "coast": t.moisture = minf(1.0, t.moisture + moisture_surge) var coastal_reach: int = tier_cfg.get("coastal_flood_reach", 2) events.append( @@ -341,8 +334,8 @@ static func process_glacial( for edge: int in t.river_edges: if t.river_flow.has(edge): t.river_flow[edge] = -abs(t.river_flow[edge]) - if tundra_expansion and (t.terrain_id == "grassland" or t.terrain_id == "plains"): - t.terrain_id = "tundra" + if tundra_expansion and (t.biome_id == "grassland" or t.biome_id == "plains"): + t.biome_id = "tundra" if quality_loss > 0: t.quality = maxi(1, t.quality - quality_loss) if moisture_loss > 0.0: @@ -377,7 +370,7 @@ static func process_tsunami( if not game_map.tiles.has(axial): return var coast_tile: Variant = game_map.tiles[axial] - if coast_tile.terrain_id != "coast": + if coast_tile.biome_id != "coast": return var inland_reach: int = tier_cfg.get("inland_reach", 1) @@ -392,7 +385,7 @@ static func process_tsunami( if not game_map.tiles.has(naxial): continue var t: Variant = game_map.tiles[naxial] - if t.terrain_id == "ocean" or t.terrain_id == "coast": + if t.biome_id == "ocean" or t.biome_id == "coast": if reef_destruction: t.reef_health = maxf(0.0, t.reef_health - 0.5) continue @@ -449,6 +442,11 @@ static func process_pandemic( ) if lair_roll < lair_kill_chance: t.lair_type = "" + # Convert NPC building to ruin + for b: Variant in GameState.get_npc_buildings_at(axial): + if b.type_id != "village" and b.type_id != "ruin": + b.type_id = "ruin" + b.name = "Ruin" killed_lairs += 1 if fish_stock_loss > 0 and t.fish_stock >= 0: t.fish_stock = maxi(0, t.fish_stock - fish_stock_loss) diff --git a/engine/src/modules/climate/ecological_event_utils.gd b/engine/src/modules/climate/ecological_event_utils.gd index 08f1add3..e0a57a4d 100644 --- a/engine/src/modules/climate/ecological_event_utils.gd +++ b/engine/src/modules/climate/ecological_event_utils.gd @@ -53,7 +53,7 @@ static func pick_land( if not game_map.tiles.has(axial): return null var tile: Variant = game_map.tiles[axial] - if tile.terrain_id == "ocean" or tile.terrain_id == "coast": + if tile.biome_id == "ocean" or tile.biome_id == "coast": return null return tile @@ -68,7 +68,7 @@ static func spawn_resource(tile: Variant, cfg: Dictionary) -> void: ## Handles two config shapes: ## spawns_resource + resource_terrain → always spawn that resource ## resource_table [{ resource, weight, resource_terrain }] → weighted random pick - ## resource_terrain: if non-null, the tile's terrain_id is set to that value first. + ## resource_terrain: if non-null, the tile's biome_id is set to that value first. var table: Array = cfg.get("resource_table", []) if not table.is_empty(): spawn_resource_weighted(tile, table) @@ -79,7 +79,7 @@ static func spawn_resource(tile: Variant, cfg: Dictionary) -> void: return var resource_terrain: Variant = cfg.get("resource_terrain", null) if resource_terrain != null and str(resource_terrain) != "null": - tile.terrain_id = str(resource_terrain) + tile.biome_id = str(resource_terrain) tile.resource_id = resource_id @@ -102,7 +102,7 @@ static func spawn_resource_weighted(tile: Variant, table: Array) -> void: var resource_id: String = str(entry.get("resource", "")) var resource_terrain: Variant = entry.get("resource_terrain", null) if resource_terrain != null and str(resource_terrain) != "null": - tile.terrain_id = str(resource_terrain) + tile.biome_id = str(resource_terrain) if resource_id != "": tile.resource_id = resource_id return