feat(ecology-specific): Update flora simulation logic and sprite generation tooling for ecology module

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-29 05:16:33 -07:00
parent c5d0f3c1ea
commit d6af1519bb
5 changed files with 68 additions and 24 deletions

View file

@ -82,28 +82,38 @@ func process_turn(game_map: RefCounted) -> void:
static func tick_pioneer(tiles: Array, biome_flora: Dictionary, veg: Dictionary) -> void:
## Pioneer colonization: allows bare ground to acquire initial flora.
## Applies a small spontaneous seeding rate (additive, not multiplicative) to
## overcome the population gate in tick_undergrowth. Models bacteria/lichen/spore
## colonization of virgin substrate. Does NOT run for abiotic worlds (ecology
## is disabled entirely when abioticWorld = true in the scenario config).
## Pioneer colonization: seeds bare ground with initial flora using raw climate values.
## Uses temperature/moisture directly (not biome climate range) because abiotic-classified
## tiles may have narrow climate ranges that don't match their actual climate conditions
## (e.g., a temperate tile classified as polar_desert before biology establishes).
## Does NOT run for abiotic worlds (ecology is disabled when abioticWorld = true).
var pioneer_rate: float = veg.get("pioneer_rate", 0.002)
for tile: Variant in tiles:
if BiomeRegistry.has_tag(tile.biome_id, "is_water"):
continue
if tile.undergrowth > 0.0:
# Only seed completely bare tiles
if tile.undergrowth > 0.0 and tile.canopy_cover > 0.0:
continue
# Raw habitability: minimum conditions for pioneer life
if tile.temperature < 0.10 or tile.moisture < 0.15:
continue
var bf: Dictionary = biome_flora.get(tile.biome_id, {})
if bf.is_empty():
continue
var climax_ug: float = bf.get("undergrowth", 0.0)
# No seeding for biomes with negligible undergrowth potential (desert, ice)
if climax_ug <= 0.01:
# Habitat quality: scale by how far above survival thresholds, capped at 1
var hab_temp: float = minf(1.0, (tile.temperature - 0.10) / 0.40)
var hab_moist: float = minf(1.0, (tile.moisture - 0.15) / 0.40)
var hab: float = hab_temp * hab_moist
if hab <= 0.0:
continue
var match_mult: float = _climate_match_flat(tile, bf)
if match_mult > 0.0:
tile.undergrowth = pioneer_rate * match_mult
# Seed undergrowth on bare tiles
if tile.undergrowth <= 0.0:
tile.undergrowth = pioneer_rate * hab
# Seed canopy on tiles with tree/shrub potential (climax canopy >= 5%)
var climax_ca: float = bf.get("canopy", 0.0)
if climax_ca >= 0.05 and tile.canopy_cover <= 0.0 and tile.undergrowth >= pioneer_rate * 0.5:
tile.canopy_cover = pioneer_rate * 0.5 * hab
static func tick_canopy(tiles: Array, biome_flora: Dictionary, veg: Dictionary, o2_fraction: float = 0.21) -> void:

View file

@ -392,32 +392,44 @@ function _getStage(stage_index: number, stages: Record<string, number>[]): Recor
// ---------------------------------------------------------------------------
function tickPioneer(tiles: TileState[], biomeFlora: Record<string, Record<string, number>>, veg: Record<string, number>): void {
// Pioneer colonization: allows bare ground to acquire initial flora.
// Applies a small spontaneous seeding rate (additive, not multiplicative) to
// overcome the population gate in tick_undergrowth. Models bacteria/lichen/spore
// colonization of virgin substrate. Does NOT run for abiotic worlds (ecology
// is disabled entirely when abioticWorld = true in the scenario config).
// Pioneer colonization: seeds bare ground with initial flora using raw climate values.
// Uses temperature/moisture directly (not biome climate range) because abiotic-classified
// tiles may have narrow climate ranges that don't match their actual climate conditions
// (e.g., a temperate tile classified as polar_desert before biology establishes).
// Does NOT run for abiotic worlds (ecology is disabled when abioticWorld = true).
let pioneer_rate = (veg as any)["pioneer_rate"] ?? 0.002
for (const tile of tiles) {
if (hasTag(tile.biome_id, "is_water")) {
continue
}
if (tile.undergrowth > 0.0) {
// Only seed completely bare tiles
if (tile.undergrowth > 0.0 && tile.canopy_cover > 0.0) {
continue
}
// Raw habitability: minimum conditions for pioneer life
if (tile.temperature < 0.10 || tile.moisture < 0.15) {
continue
}
let bf = biomeFlora[tile.biome_id] ?? {}
if (Object.keys(bf).length === 0) {
continue
}
let climax_ug = bf["undergrowth"] ?? 0.0
// No seeding for biomes with negligible undergrowth potential (desert, ice)
if (climax_ug <= 0.01) {
// Habitat quality: scale by how far above survival thresholds, capped at 1
let hab_temp = Math.min(1.0, (tile.temperature - 0.10) / 0.40)
let hab_moist = Math.min(1.0, (tile.moisture - 0.15) / 0.40)
let hab = hab_temp * hab_moist
if (hab <= 0.0) {
continue
}
let match_mult = _climateMatchFlat(tile, bf)
if (match_mult > 0.0) {
tile.undergrowth = pioneer_rate * match_mult
// Seed undergrowth on bare tiles
if (tile.undergrowth <= 0.0) {
tile.undergrowth = pioneer_rate * hab
}
// Seed canopy on tiles with tree/shrub potential (climax canopy >= 5%)
let climax_ca = bf["canopy"] ?? 0.0
if (climax_ca >= 0.05 && tile.canopy_cover <= 0.0 && tile.undergrowth >= pioneer_rate * 0.5) {
tile.canopy_cover = pioneer_rate * 0.5 * hab
}
@ -1149,6 +1161,17 @@ export class EcologyPhysics {
// Flora dynamics (order matches flora.gd process_turn)
const o2 = grid.o2_fraction ?? 0.21
tickPioneer(tiles, bf, veg)
// Reclassify pioneer-seeded tiles immediately so subsequent ticks use the correct biome.
// A tile seeded from canopy=0 to canopy>0 may be in an abiotic biome (polar_desert,
// chaparral) that won't match its actual climate, causing tick_canopy/tick_undergrowth
// to decay the pioneer growth. Reclassifying here lets the same-turn tick functions
// use the correct biotic biome and grow instead of decay.
for (const tile of tiles) {
if (!hasTag(tile.biome_id, 'is_water') && (tile.undergrowth > 0 || tile.canopy_cover > 0)) {
const _newBiome = _classifyBiomeInline(tile)
if (_newBiome !== tile.biome_id) tile.biome_id = _newBiome
}
}
tickCanopy(tiles, bf, veg, o2)
tickUndergrowth(tiles, bf, veg, o2)
tickFungi(tiles, bf, veg)

View file

@ -658,6 +658,17 @@ export class EcologyPhysics {{
// Flora dynamics (order matches flora.gd process_turn)
const o2 = grid.o2_fraction ?? 0.21
tickPioneer(tiles, bf, veg)
// Reclassify pioneer-seeded tiles immediately so subsequent ticks use the correct biome.
// A tile seeded from canopy=0 to canopy>0 may be in an abiotic biome (polar_desert,
// chaparral) that won't match its actual climate, causing tick_canopy/tick_undergrowth
// to decay the pioneer growth. Reclassifying here lets the same-turn tick functions
// use the correct biotic biome and grow instead of decay.
for (const tile of tiles) {{
if (!hasTag(tile.biome_id, 'is_water') && (tile.undergrowth > 0 || tile.canopy_cover > 0)) {{
const _newBiome = _classifyBiomeInline(tile)
if (_newBiome !== tile.biome_id) tile.biome_id = _newBiome
}}
}}
tickCanopy(tiles, bf, veg, o2)
tickUndergrowth(tiles, bf, veg, o2)
tickFungi(tiles, bf, veg)