diff --git a/engine/src/modules/ecology/ecosystem.gd b/engine/src/modules/ecology/ecosystem.gd index 70dd90d1..84c69b84 100644 --- a/engine/src/modules/ecology/ecosystem.gd +++ b/engine/src/modules/ecology/ecosystem.gd @@ -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", - ] diff --git a/engine/src/modules/ecology/ecosystem_simplified.gd b/engine/src/modules/ecology/ecosystem_simplified.gd index 86cc7567..78759a76 100644 --- a/engine/src/modules/ecology/ecosystem_simplified.gd +++ b/engine/src/modules/ecology/ecosystem_simplified.gd @@ -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", - ] diff --git a/engine/src/modules/ecology/fauna_simplified.gd b/engine/src/modules/ecology/fauna_simplified.gd index d701fdb3..e4b597bb 100644 --- a/engine/src/modules/ecology/fauna_simplified.gd +++ b/engine/src/modules/ecology/fauna_simplified.gd @@ -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. diff --git a/engine/src/modules/ecology/flora.gd b/engine/src/modules/ecology/flora.gd index 9333d4e9..867fe261 100644 --- a/engine/src/modules/ecology/flora.gd +++ b/engine/src/modules/ecology/flora.gd @@ -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