feat(climate): ✨ Add ecological event handlers and evaluation utilities for enhanced climate simulation
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
a289fd2e65
commit
ddf41217ca
7 changed files with 91 additions and 250 deletions
|
|
@ -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
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue