fix(ecology): 🐛 Refine flora/fauna interaction rules to improve simulation accuracy
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
c96ea3a3da
commit
49e357d033
4 changed files with 73 additions and 77 deletions
|
|
@ -101,7 +101,7 @@ func _recompute_biomes(game_map: RefCounted) -> void:
|
|||
## classification. Reclassify if deltas exceed thresholds.
|
||||
for axial: Vector2i in game_map.tiles:
|
||||
var tile: Variant = game_map.tiles[axial]
|
||||
if _is_water(tile):
|
||||
if BiomeRegistry.has_tag(tile.biome_id, "is_water"):
|
||||
continue
|
||||
|
||||
var canopy_d: float = absf(
|
||||
|
|
@ -144,7 +144,7 @@ func _compute_tile_quality(
|
|||
## Capped by biome.quality_range.
|
||||
for axial: Vector2i in game_map.tiles:
|
||||
var tile: Variant = game_map.tiles[axial]
|
||||
if _is_water(tile):
|
||||
if BiomeRegistry.has_tag(tile.biome_id, "is_water"):
|
||||
continue
|
||||
|
||||
var biome: Variant = DataLoader.get_biome(tile.biome_id)
|
||||
|
|
@ -201,7 +201,7 @@ func _compute_global_health(game_map: RefCounted) -> void:
|
|||
var count: int = 0
|
||||
for axial: Vector2i in game_map.tiles:
|
||||
var tile: Variant = game_map.tiles[axial]
|
||||
if _is_water(tile):
|
||||
if BiomeRegistry.has_tag(tile.biome_id, "is_water"):
|
||||
continue
|
||||
total += float(tile.quality) / 5.0
|
||||
count += 1
|
||||
|
|
@ -214,7 +214,7 @@ func get_ecology_food_modifier(tile: Variant) -> float:
|
|||
var mult: Dictionary = _food_yield_mult()
|
||||
var base: float = mult.get(tile.quality, 1.0)
|
||||
# Land tiles: scale with undergrowth density
|
||||
if not _is_water(tile) and "undergrowth" in tile:
|
||||
if not BiomeRegistry.has_tag(tile.biome_id, "is_water") and "undergrowth" in tile:
|
||||
base *= lerpf(0.8, 1.2, tile.undergrowth)
|
||||
return base
|
||||
|
||||
|
|
@ -306,15 +306,3 @@ static func _population_balance(
|
|||
var deviation: float = absf(ratio - ideal) / ideal
|
||||
return clampf(1.0 - deviation * 0.5, 0.0, 1.0)
|
||||
|
||||
|
||||
|
||||
static func _is_water(tile: Variant) -> bool:
|
||||
if "substrate_id" in tile:
|
||||
var sub: String = tile.substrate_id
|
||||
return sub in [
|
||||
"deep_water", "shallow_water", "lake_bed",
|
||||
]
|
||||
return tile.biome_id in [
|
||||
"ocean", "coast", "deep_ocean", "shallow_ocean", "coral_reef",
|
||||
"estuary", "mangrove", "lake", "pond", "river", "inland_sea",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ static func compute_tile_quality(tiles: Array, biome_data: Dictionary, w: int, h
|
|||
var stability_score: float = 0.0
|
||||
var balance_score: float = 0.5
|
||||
|
||||
if _is_water(tile):
|
||||
if BiomeRegistry.has_tag(tile.biome_id, "is_water"):
|
||||
# Water tiles: quality from fish stock and reef health
|
||||
balance_score = _water_balance(tile)
|
||||
stability_score = _water_stability(tile)
|
||||
|
|
@ -206,7 +206,7 @@ static func recompute_biomes(tiles: Array, w: int, h: int, last_canopy: PackedFl
|
|||
|
||||
for i: int in n:
|
||||
var tile: Variant = tiles[i]
|
||||
if _is_water(tile):
|
||||
if BiomeRegistry.has_tag(tile.biome_id, "is_water"):
|
||||
continue
|
||||
var d_canopy: float = absf(tile.canopy_cover - last_canopy[i])
|
||||
var d_temp: float = absf(tile.temperature - last_temp[i])
|
||||
|
|
@ -223,48 +223,53 @@ static func recompute_biomes(tiles: Array, w: int, h: int, last_canopy: PackedFl
|
|||
|
||||
static func _classify_biome(tile: Variant) -> String:
|
||||
## Minimal inline classifier for recomputation. Mirrors biome_classifier.gd logic.
|
||||
if _is_water(tile):
|
||||
if BiomeRegistry.has_tag(tile.biome_id, "is_water"):
|
||||
return tile.biome_id
|
||||
var temp: float = tile.temperature
|
||||
var moist: float = tile.moisture
|
||||
var elev: float = tile.elevation
|
||||
var canopy: float = tile.canopy_cover
|
||||
if moist > 0.7 and elev < 0.4:
|
||||
if moist > 0.7 and elev < 0.4 and canopy > 0:
|
||||
if temp > 0.4:
|
||||
return "swamp"
|
||||
return "bog"
|
||||
if elev > 0.85:
|
||||
if temp < 0.1:
|
||||
return "permanent_ice"
|
||||
return "glacial"
|
||||
return "alpine_tundra"
|
||||
if elev > 0.70:
|
||||
if moist > 0.3:
|
||||
if canopy > 0 and moist > 0.3:
|
||||
return "alpine_meadow"
|
||||
return "alpine_tundra"
|
||||
if elev > 0.55:
|
||||
if canopy > 0.4:
|
||||
return "montane_forest"
|
||||
if moist > 0.7 and temp > 0.3:
|
||||
if canopy > 0 and moist > 0.7 and temp > 0.3:
|
||||
return "cloud_forest"
|
||||
return "alpine_meadow"
|
||||
if canopy > 0 and moist > 0.3:
|
||||
return "alpine_meadow"
|
||||
return "alpine_tundra"
|
||||
if temp > 0.55:
|
||||
if moist > 0.7 and canopy > 0.6:
|
||||
return "tropical_rainforest"
|
||||
if moist > 0.4:
|
||||
return "tropical_dry_forest"
|
||||
if moist > 0.2:
|
||||
return "savanna"
|
||||
if canopy > 0:
|
||||
if moist > 0.4:
|
||||
return "tropical_dry_forest"
|
||||
if moist > 0.2:
|
||||
return "savanna"
|
||||
return "desert"
|
||||
if temp > 0.25:
|
||||
if canopy > 0.5:
|
||||
return "temperate_forest"
|
||||
if moist > 0.3:
|
||||
if moist > 0.3 and canopy > 0:
|
||||
return "temperate_grassland"
|
||||
return "chaparral"
|
||||
if temp > 0.1:
|
||||
if canopy > 0.3:
|
||||
return "boreal_forest"
|
||||
return "tundra"
|
||||
if canopy > 0:
|
||||
return "tundra"
|
||||
return "polar_desert"
|
||||
return "polar_desert"
|
||||
|
||||
|
||||
|
|
@ -272,16 +277,8 @@ static func get_ecology_food_modifier(tile: Variant) -> float:
|
|||
## Food yield modifier based on ecology quality tier.
|
||||
var mult: Dictionary = {1: 0.5, 2: 1.0, 3: 1.5, 4: 2.0, 5: 2.5}
|
||||
var base: float = mult.get(tile.quality, 1.0)
|
||||
if not _is_water(tile):
|
||||
if not BiomeRegistry.has_tag(tile.biome_id, "is_water"):
|
||||
base *= 0.8 + 0.4 * tile.undergrowth
|
||||
return base
|
||||
|
||||
|
||||
static func _is_water(tile: Variant) -> bool:
|
||||
if "substrate_id" in tile:
|
||||
var sub: String = tile.substrate_id
|
||||
return sub in ["deep_water", "shallow_water", "lake_bed"]
|
||||
return tile.biome_id in [
|
||||
"ocean", "coast", "deep_ocean", "shallow_ocean", "coral_reef",
|
||||
"estuary", "mangrove", "lake", "pond", "river", "inland_sea",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -14,15 +14,14 @@ extends RefCounted
|
|||
|
||||
static func tick_fish_stock(tiles: Array, marine_params: Dictionary) -> void:
|
||||
## Logistic fish reproduction on water tiles.
|
||||
## Seed empty water tiles at 10% capacity x tempMult.
|
||||
## Requires existing population — no spontaneous generation.
|
||||
var repro_rate: float = marine_params.get("reproduction_rate", 0.05)
|
||||
var cap_base: float = marine_params.get("fish_capacity", 100.0)
|
||||
var reef_bonus: float = marine_params.get("reef_bonus", 0.5)
|
||||
var reef_penalty: float = marine_params.get("reef_penalty", -0.5)
|
||||
var seed_fraction: float = marine_params.get("seed_fraction", 0.1)
|
||||
|
||||
for tile: Variant in tiles:
|
||||
if not _is_water(tile):
|
||||
if not BiomeRegistry.has_tag(tile.biome_id, "is_water"):
|
||||
continue
|
||||
|
||||
var temp_mult: float = _temp_mult(tile.temperature)
|
||||
|
|
@ -34,9 +33,8 @@ static func tick_fish_stock(tiles: Array, marine_params: Dictionary) -> void:
|
|||
|
||||
var stock: float = float(tile.fish_stock)
|
||||
|
||||
# Seed empty water tiles
|
||||
# Population gate: no spontaneous generation
|
||||
if stock <= 0.0:
|
||||
tile.fish_stock = int(cap * seed_fraction * temp_mult)
|
||||
continue
|
||||
|
||||
var growth: float = repro_rate * temp_mult * stock * (1.0 - stock / cap)
|
||||
|
|
@ -48,7 +46,7 @@ static func tick_habitat_suitability(tiles: Array, w: int, h: int) -> void:
|
|||
## undergrowth x 0.6 + canopy x 0.2 + fungi x 0.2.
|
||||
for i: int in tiles.size():
|
||||
var tile: Variant = tiles[i]
|
||||
if _is_water(tile):
|
||||
if BiomeRegistry.has_tag(tile.biome_id, "is_water"):
|
||||
continue
|
||||
|
||||
var col: int = tile.col
|
||||
|
|
@ -69,7 +67,7 @@ static func tick_habitat_suitability(tiles: Array, w: int, h: int) -> void:
|
|||
if ni < 0 or ni >= tiles.size():
|
||||
continue
|
||||
var ntile: Variant = tiles[ni]
|
||||
if _is_water(ntile):
|
||||
if BiomeRegistry.has_tag(ntile.biome_id, "is_water"):
|
||||
continue
|
||||
total_ug += ntile.undergrowth
|
||||
total_ca += ntile.canopy_cover
|
||||
|
|
@ -87,12 +85,16 @@ static func tick_habitat_suitability(tiles: Array, w: int, h: int) -> void:
|
|||
|
||||
static func tick_reef_health(tiles: Array, marine_params: Dictionary) -> void:
|
||||
## Reef growth in ideal temperature range.
|
||||
## Requires existing population — reef can't grow from nothing.
|
||||
var growth_rate: float = marine_params.get("reef_growth_rate", 0.02)
|
||||
var ideal_min: float = marine_params.get("reef_ideal_min", 0.55)
|
||||
var ideal_max: float = marine_params.get("reef_ideal_max", 0.75)
|
||||
|
||||
for tile: Variant in tiles:
|
||||
if not _is_water(tile):
|
||||
if not BiomeRegistry.has_tag(tile.biome_id, "is_water"):
|
||||
continue
|
||||
# Population gate: reef can't grow from nothing
|
||||
if tile.reef_health <= 0.0:
|
||||
continue
|
||||
if tile.temperature >= ideal_min and tile.temperature <= ideal_max:
|
||||
tile.reef_health = minf(1.0, tile.reef_health + growth_rate)
|
||||
|
|
@ -112,15 +114,6 @@ static func _temp_mult(temperature: float) -> float:
|
|||
return 0.5
|
||||
|
||||
|
||||
static func _is_water(tile: Variant) -> bool:
|
||||
if "substrate_id" in tile:
|
||||
var sub: String = tile.substrate_id
|
||||
return sub in ["deep_water", "shallow_water", "lake_bed"]
|
||||
return tile.biome_id in [
|
||||
"ocean", "coast", "deep_ocean", "shallow_ocean", "coral_reef",
|
||||
"estuary", "mangrove", "lake", "pond", "river", "inland_sea",
|
||||
]
|
||||
|
||||
|
||||
static func _get_neighbor_offsets(col: int) -> Array:
|
||||
## Even-q offset hex neighbor deltas as [dc, dr] arrays.
|
||||
|
|
|
|||
|
|
@ -62,8 +62,9 @@ func process_turn(game_map: RefCounted) -> void:
|
|||
# Collect tiles into flat array for transpiler-friendly iteration
|
||||
var tiles: Array = game_map.tiles.values()
|
||||
|
||||
tick_canopy(tiles, _biome_flora, _veg)
|
||||
tick_undergrowth(tiles, _biome_flora, _veg)
|
||||
var o2: float = game_map.o2_fraction if "o2_fraction" in game_map else 0.21
|
||||
tick_canopy(tiles, _biome_flora, _veg, o2)
|
||||
tick_undergrowth(tiles, _biome_flora, _veg, o2)
|
||||
tick_fungi(tiles, _biome_flora, _veg)
|
||||
tick_succession(tiles, _suc)
|
||||
tick_desertification(tiles, _veg, _des)
|
||||
|
|
@ -79,42 +80,56 @@ func process_turn(game_map: RefCounted) -> void:
|
|||
# =========================================================================
|
||||
|
||||
|
||||
static func tick_canopy(tiles: Array, biome_flora: Dictionary, veg: Dictionary) -> void:
|
||||
static func tick_canopy(tiles: Array, biome_flora: Dictionary, veg: Dictionary, o2_fraction: float = 0.21) -> void:
|
||||
## Grow canopy toward biome climax. Decay when outside climate range.
|
||||
## Requires existing population (dP/dt = r*P) and sufficient atmospheric O2.
|
||||
var growth_rate: float = veg.get("growth_rate", 0.02)
|
||||
var decay_rate: float = veg.get("decay_rate", 0.03)
|
||||
var o2_mult: float = _o2_growth_mult(o2_fraction)
|
||||
|
||||
for tile: Variant in tiles:
|
||||
if _is_water(tile):
|
||||
if BiomeRegistry.has_tag(tile.biome_id, "is_water"):
|
||||
continue
|
||||
var bf: Dictionary = biome_flora.get(tile.biome_id, {})
|
||||
if bf.is_empty():
|
||||
continue
|
||||
var climax: float = bf.get("canopy", 0.0)
|
||||
|
||||
# Population gate: can't grow from nothing
|
||||
if tile.canopy_cover <= 0.0:
|
||||
continue
|
||||
|
||||
var match_mult: float = _climate_match_flat(tile, bf)
|
||||
var q_mult: float = _quality_mult(tile.quality)
|
||||
|
||||
if match_mult > 0.0:
|
||||
var delta: float = growth_rate * match_mult * q_mult
|
||||
var delta: float = growth_rate * match_mult * q_mult * o2_mult
|
||||
tile.canopy_cover = minf(tile.canopy_cover + delta, climax)
|
||||
else:
|
||||
tile.canopy_cover = maxf(tile.canopy_cover - decay_rate, 0.0)
|
||||
|
||||
|
||||
static func tick_undergrowth(tiles: Array, biome_flora: Dictionary, veg: Dictionary) -> void:
|
||||
static func tick_undergrowth(tiles: Array, biome_flora: Dictionary, veg: Dictionary, o2_fraction: float = 0.21) -> void:
|
||||
## Grow undergrowth, capped by canopy shade. Decays faster in drought.
|
||||
## Requires existing population (dP/dt = r*P) and sufficient atmospheric O2.
|
||||
var growth_rate: float = veg.get("growth_rate", 0.02)
|
||||
var decay_rate: float = veg.get("decay_rate", 0.03)
|
||||
var shade_cap: float = veg.get("shade_cap", 0.7)
|
||||
var drought_mult: float = veg.get("drought_decay_multiplier", 1.5)
|
||||
var o2_mult: float = _o2_growth_mult(o2_fraction)
|
||||
|
||||
for tile: Variant in tiles:
|
||||
if _is_water(tile):
|
||||
if BiomeRegistry.has_tag(tile.biome_id, "is_water"):
|
||||
continue
|
||||
var bf: Dictionary = biome_flora.get(tile.biome_id, {})
|
||||
if bf.is_empty():
|
||||
continue
|
||||
var climax: float = bf.get("undergrowth", 0.0)
|
||||
|
||||
# Population gate: can't grow from nothing
|
||||
if tile.undergrowth <= 0.0:
|
||||
continue
|
||||
|
||||
var match_mult: float = _climate_match_flat(tile, bf)
|
||||
var q_mult: float = _quality_mult(tile.quality)
|
||||
|
||||
|
|
@ -123,7 +138,7 @@ static func tick_undergrowth(tiles: Array, biome_flora: Dictionary, veg: Diction
|
|||
effective_cap = minf(climax, shade_cap)
|
||||
|
||||
if match_mult > 0.0:
|
||||
var delta: float = growth_rate * match_mult * q_mult
|
||||
var delta: float = growth_rate * match_mult * q_mult * o2_mult
|
||||
tile.undergrowth = minf(tile.undergrowth + delta, effective_cap)
|
||||
else:
|
||||
var rate: float = decay_rate
|
||||
|
|
@ -139,7 +154,7 @@ static func tick_fungi(tiles: Array, biome_flora: Dictionary, veg: Dictionary) -
|
|||
var ug_threshold: float = veg.get("fungi_undergrowth_threshold", 0.3)
|
||||
|
||||
for tile: Variant in tiles:
|
||||
if _is_water(tile):
|
||||
if BiomeRegistry.has_tag(tile.biome_id, "is_water"):
|
||||
continue
|
||||
var bf: Dictionary = biome_flora.get(tile.biome_id, {})
|
||||
if bf.is_empty():
|
||||
|
|
@ -173,7 +188,7 @@ static func tick_succession(tiles: Array, suc: Dictionary) -> void:
|
|||
var canopy_threshold: float = suc.get("canopy_threshold", 0.8)
|
||||
|
||||
for tile: Variant in tiles:
|
||||
if _is_water(tile):
|
||||
if BiomeRegistry.has_tag(tile.biome_id, "is_water"):
|
||||
continue
|
||||
if tile.regrowth_stage >= 0:
|
||||
continue
|
||||
|
|
@ -192,7 +207,7 @@ static func tick_desertification(tiles: Array, veg: Dictionary, des: Dictionary)
|
|||
var base_decay: float = veg.get("decay_rate", 0.03)
|
||||
|
||||
for tile: Variant in tiles:
|
||||
if _is_water(tile):
|
||||
if BiomeRegistry.has_tag(tile.biome_id, "is_water"):
|
||||
continue
|
||||
|
||||
if tile.moisture < moisture_thresh:
|
||||
|
|
@ -259,7 +274,7 @@ func _post_succession(tiles: Array) -> void:
|
|||
var stability_turns: int = _suc.get("stability_turns", 50)
|
||||
|
||||
for tile: Variant in tiles:
|
||||
if _is_water(tile):
|
||||
if BiomeRegistry.has_tag(tile.biome_id, "is_water"):
|
||||
continue
|
||||
if tile.succession_progress < stability_turns:
|
||||
continue
|
||||
|
|
@ -295,15 +310,6 @@ static func _get_stage(stage_index: int, stages: Array) -> Dictionary:
|
|||
return {}
|
||||
|
||||
|
||||
static func _is_water(tile: Variant) -> bool:
|
||||
if "substrate_id" in tile:
|
||||
var sub: String = tile.substrate_id
|
||||
return sub in ["deep_water", "shallow_water", "lake_bed"]
|
||||
return tile.biome_id in [
|
||||
"ocean", "coast", "deep_ocean", "shallow_ocean", "coral_reef",
|
||||
"estuary", "mangrove", "lake", "pond", "river", "inland_sea",
|
||||
]
|
||||
|
||||
|
||||
static func _climate_match_flat(tile: Variant, bf: Dictionary) -> float:
|
||||
## Climate match using pre-resolved flat biome data (no BiomeModel).
|
||||
|
|
@ -337,3 +343,15 @@ static func _quality_mult(quality: int) -> float:
|
|||
4: return 1.2
|
||||
5: return 1.4
|
||||
_: return 1.0
|
||||
|
||||
|
||||
static func _o2_growth_mult(o2: float) -> float:
|
||||
## Complex plant growth scaling by atmospheric O2.
|
||||
## Below 2%: no growth. 2-10%: ramp to 0.5. 10-18%: ramp to 1.0. 18%+: full.
|
||||
if o2 < 0.02:
|
||||
return 0.0
|
||||
if o2 < 0.10:
|
||||
return (o2 - 0.02) / 0.08 * 0.5
|
||||
if o2 < 0.18:
|
||||
return 0.5 + (o2 - 0.10) / 0.08 * 0.5
|
||||
return 1.0
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue