diff --git a/tools/transpile-engine/ecology_assembly.py b/tools/transpile-engine/ecology_assembly.py new file mode 100644 index 00000000..f2238b90 --- /dev/null +++ b/tools/transpile-engine/ecology_assembly.py @@ -0,0 +1,689 @@ +""" +Ecology system TypeScript assembly — builds EcologyPhysics.generated.ts. + +Hand-written TypeScript that faithfully ports the GDScript ecology system +(flora.gd, fauna.gd partial, ecosystem.gd) into TypeScript operating on +flat TileState[] grids. The guide doesn't have SQLite or individual +creatures, so fauna scoring uses simplified tile-level approximations. + +Source GDScript files: + engine/src/modules/ecology/flora.gd + engine/src/modules/ecology/fauna.gd + engine/src/modules/ecology/ecosystem.gd + engine/src/models/world/biome_classifier.gd +""" + + +def _eco_build_full_output() -> str: + """Build the complete EcologyPhysics.generated.ts content.""" + parts: list[str] = [ + _header(), + _biome_data(), + _classifier(), + _flora_helpers(), + _flora_ticks(), + _fauna_simplified(), + _ecosystem_quality(), + _ecosystem_class(), + ] + return "".join(parts) + + +# --------------------------------------------------------------------------- +# File header +# --------------------------------------------------------------------------- + +def _header() -> str: + return """\ +// AUTO-GENERATED from GDScript ecology engine — do not edit manually. +// Source: engine/src/modules/ecology/flora.gd + fauna.gd + ecosystem.gd +// Regenerate: uv run tools/transpile-engine/transpile.py + +import type { GridState, TileState } from './types' +import { idx, neighbors } from './HexGrid' + +""" + + +# --------------------------------------------------------------------------- +# Biome data — inline proof biomes for guide rendering +# --------------------------------------------------------------------------- + +def _biome_data() -> str: + return """\ +// --------------------------------------------------------------------------- +// Biome definitions (proof set — matches games/age-of-dwarves/data/world/) +// --------------------------------------------------------------------------- + +interface BiomeDef { + id: string + temp_range: [number, number] + moisture_range: [number, number] + flora_climax: { canopy: number; undergrowth: number; fungi: number } + fauna_capacity: number + quality_range: [number, number] +} + +const BIOME_DEFS: Record = { + temperate_forest: { + id: 'temperate_forest', + temp_range: [0.35, 0.65], + moisture_range: [0.4, 0.8], + flora_climax: { canopy: 0.9, undergrowth: 0.7, fungi: 0.5 }, + fauna_capacity: 12, + quality_range: [1, 5], + }, + tropical_rainforest: { + id: 'tropical_rainforest', + temp_range: [0.65, 1.0], + moisture_range: [0.6, 1.0], + flora_climax: { canopy: 1.0, undergrowth: 0.9, fungi: 0.8 }, + fauna_capacity: 16, + quality_range: [1, 5], + }, + grassland: { + id: 'grassland', + temp_range: [0.3, 0.7], + moisture_range: [0.2, 0.5], + flora_climax: { canopy: 0.1, undergrowth: 0.8, fungi: 0.2 }, + fauna_capacity: 8, + quality_range: [1, 4], + }, + desert: { + id: 'desert', + temp_range: [0.5, 1.0], + moisture_range: [0.0, 0.2], + flora_climax: { canopy: 0.0, undergrowth: 0.1, fungi: 0.0 }, + fauna_capacity: 3, + quality_range: [1, 3], + }, + boreal_forest: { + id: 'boreal_forest', + temp_range: [0.15, 0.4], + moisture_range: [0.3, 0.7], + flora_climax: { canopy: 0.7, undergrowth: 0.4, fungi: 0.6 }, + fauna_capacity: 8, + quality_range: [1, 5], + }, + tundra: { + id: 'tundra', + temp_range: [0.0, 0.2], + moisture_range: [0.1, 0.5], + flora_climax: { canopy: 0.0, undergrowth: 0.2, fungi: 0.1 }, + fauna_capacity: 4, + quality_range: [1, 3], + }, +} + +function getBiome(biomeId: string): BiomeDef | null { + return BIOME_DEFS[biomeId] ?? null +} + +""" + + +# --------------------------------------------------------------------------- +# Biome classifier (from biome_classifier.gd) +# --------------------------------------------------------------------------- + +def _classifier() -> str: + return """\ +// --------------------------------------------------------------------------- +// BiomeClassifier — substrate + climate + flora → biome_id +// Faithfully ports engine/src/models/world/biome_classifier.gd +// --------------------------------------------------------------------------- + +function classifyBiome(tile: TileState): string { + const sub = tile.substrate_id + // Aquatic tiles keep their terrain_id + if (sub === 'deep_water' || sub === 'shallow_water' || sub === 'lake_bed') { + return tile.terrain_id + } + + const temp = tile.temperature + const moist = tile.moisture + + // Wetland override + if (sub === 'wetland') return 'swamp' + + // Temperature-driven classification + if (temp < 0.15) return 'tundra' + if (temp < 0.4) { + if (moist > 0.3 && tile.canopy_cover > 0.3) return 'boreal_forest' + if (moist > 0.3) return 'grassland' + return 'tundra' + } + if (temp < 0.65) { + if (moist > 0.4 && tile.canopy_cover > 0.5) return 'temperate_forest' + if (moist > 0.2) return 'grassland' + return 'desert' + } + // Hot + if (moist > 0.6 && tile.canopy_cover > 0.6) return 'tropical_rainforest' + if (moist > 0.3) return 'grassland' + return 'desert' +} + +""" + + +# --------------------------------------------------------------------------- +# Flora helpers (from flora.gd static methods) +# --------------------------------------------------------------------------- + +def _flora_helpers() -> str: + return """\ +// --------------------------------------------------------------------------- +// Flora helpers +// --------------------------------------------------------------------------- + +function isWater(tile: TileState): boolean { + const sub = tile.substrate_id + if (sub) { + return sub === 'deep_water' || sub === 'shallow_water' || sub === 'lake_bed' + } + return tile.terrain_id === 'ocean' || tile.terrain_id === 'coast' +} + +function climateMatch(tile: TileState, biome: BiomeDef): number { + const temp = tile.temperature + const moist = tile.moisture + const [tMin, tMax] = biome.temp_range + const [mMin, mMax] = biome.moisture_range + + const tempOk = temp >= tMin && temp <= tMax + const moistOk = moist >= mMin && moist <= mMax + + if (tempOk && moistOk) return 1.0 + + const tempEdge = temp >= tMin - 0.1 && temp <= tMax + 0.1 + const moistEdge = moist >= mMin - 0.1 && moist <= mMax + 0.1 + + if (tempEdge && moistEdge) return 0.5 + return 0.0 +} + +function qualityMult(quality: number): number { + switch (quality) { + case 1: return 0.6 + case 2: return 0.8 + case 4: return 1.2 + case 5: return 1.4 + default: return 1.0 + } +} + +// --------------------------------------------------------------------------- +// Vegetation defaults (from DataLoader fallbacks in flora.gd) +// --------------------------------------------------------------------------- + +const VEG = { + growth_rate: 0.02, + decay_rate: 0.03, + shade_cap: 0.7, + drought_decay_multiplier: 1.5, + fungi_undergrowth_threshold: 0.3, + fungi_regrowth_bonus_cap: 2.0, +} as const + +const SUC = { + stability_turns: 50, + canopy_threshold: 0.8, + regrowth_stages: [ + { stage: 0, turns_to_advance: 10, canopy_target: 0.0, undergrowth_target: 0.1, fungi_target: 0.0 }, + { stage: 1, turns_to_advance: 15, canopy_target: 0.1, undergrowth_target: 0.3, fungi_target: 0.05 }, + { stage: 2, turns_to_advance: 20, canopy_target: 0.4, undergrowth_target: 0.5, fungi_target: 0.2 }, + { stage: 3, turns_to_advance: 25, canopy_target: 0.7, undergrowth_target: 0.6, fungi_target: 0.4 }, + ], +} as const + +const DES = { + moisture_threshold: 0.2, + turns_required: 30, + decay_multiplier: 2.0, + recovery_rate: 1, +} as const + +function getRegrowthStage(stageIdx: number): typeof SUC.regrowth_stages[number] | null { + for (const s of SUC.regrowth_stages) { + if (s.stage === stageIdx) return s + } + return null +} + +""" + + +# --------------------------------------------------------------------------- +# Flora tick methods (from flora.gd) +# --------------------------------------------------------------------------- + +def _flora_ticks() -> str: + return """\ +// --------------------------------------------------------------------------- +// Flora tick methods (from flora.gd process_turn) +// --------------------------------------------------------------------------- + +function tickCanopy(tiles: TileState[], w: number, h: number): void { + for (let i = 0; i < tiles.length; i++) { + const tile = tiles[i] + if (isWater(tile)) continue + const biome = getBiome(tile.biome_id) + if (!biome) continue + const climax = biome.flora_climax.canopy + const match = climateMatch(tile, biome) + const qm = qualityMult(tile.quality) + if (match > 0.0) { + const delta = VEG.growth_rate * match * qm + tile.canopy_cover = Math.min(tile.canopy_cover + delta, climax) + } else { + tile.canopy_cover = Math.max(tile.canopy_cover - VEG.decay_rate, 0.0) + } + } +} + +function tickUndergrowth(tiles: TileState[], w: number, h: number): void { + for (let i = 0; i < tiles.length; i++) { + const tile = tiles[i] + if (isWater(tile)) continue + const biome = getBiome(tile.biome_id) + if (!biome) continue + const climax = biome.flora_climax.undergrowth + const match = climateMatch(tile, biome) + const qm = qualityMult(tile.quality) + let effectiveCap = climax + if (tile.canopy_cover > VEG.shade_cap) { + effectiveCap = Math.min(climax, VEG.shade_cap) + } + if (match > 0.0) { + const delta = VEG.growth_rate * match * qm + tile.undergrowth = Math.min(tile.undergrowth + delta, effectiveCap) + } else { + let rate = VEG.decay_rate + if (tile.drought_counter > 0) rate *= VEG.drought_decay_multiplier + tile.undergrowth = Math.max(tile.undergrowth - rate, 0.0) + } + } +} + +function tickFungi(tiles: TileState[], w: number, h: number): void { + for (let i = 0; i < tiles.length; i++) { + const tile = tiles[i] + if (isWater(tile)) continue + const biome = getBiome(tile.biome_id) + if (!biome) continue + const climax = biome.flora_climax.fungi + + if (tile.undergrowth < VEG.fungi_undergrowth_threshold) { + tile.fungi_network = Math.max(tile.fungi_network - VEG.decay_rate * 0.5, 0.0) + continue + } + if (tile.moisture < 0.15 || tile.temperature < 0.1) { + tile.fungi_network = Math.max(tile.fungi_network - VEG.decay_rate * 0.5, 0.0) + continue + } + const ugFactor = tile.undergrowth + let oldGrowth = 1.0 + if (tile.canopy_cover > 0.7 && tile.undergrowth > 0.5 && tile.moisture > 0.4) { + oldGrowth = 1.5 + } + const qm = qualityMult(tile.quality) + const delta = VEG.growth_rate * ugFactor * oldGrowth * qm + tile.fungi_network = Math.min(tile.fungi_network + delta, climax) + } +} + +function tickSuccession(tiles: TileState[], w: number, h: number): void { + for (let i = 0; i < tiles.length; i++) { + const tile = tiles[i] + if (isWater(tile)) continue + if (tile.regrowth_stage >= 0) continue + + if (tile.canopy_cover >= SUC.canopy_threshold) { + tile.succession_progress += 1 + } else { + tile.succession_progress = 0 + continue + } + if (tile.succession_progress < SUC.stability_turns) continue + + // Succession triggered — reclassify + const oldBiome = tile.biome_id + const newBiome = classifyBiome(tile) + tile.succession_progress = 0 + if (newBiome !== oldBiome) { + tile.biome_id = newBiome + } + if (tile.quality >= 4 && tile.landmark_name === '') { + tile.landmark_name = `Ancient ${tile.biome_id.replace(/_/g, ' ')}` + } + } +} + +function tickDesertification(tiles: TileState[], w: number, h: number): void { + const baseDecay = VEG.decay_rate + for (let i = 0; i < tiles.length; i++) { + const tile = tiles[i] + if (isWater(tile)) continue + if (tile.moisture < DES.moisture_threshold) { + tile.drought_counter += 1 + const rate = baseDecay * DES.decay_multiplier + tile.canopy_cover = Math.max(tile.canopy_cover - rate, 0.0) + tile.undergrowth = Math.max(tile.undergrowth - rate * 1.5, 0.0) + tile.fungi_network = Math.max(tile.fungi_network - rate, 0.0) + if (tile.drought_counter >= DES.turns_required) { + const oldBiome = tile.biome_id + const newBiome = classifyBiome(tile) + if (newBiome !== oldBiome) tile.biome_id = newBiome + } + } else { + tile.drought_counter = Math.max(tile.drought_counter - DES.recovery_rate, 0) + } + } +} + +function tickRegrowth(tiles: TileState[], w: number, h: number): void { + for (let i = 0; i < tiles.length; i++) { + const tile = tiles[i] + if (tile.regrowth_stage < 0) continue + + tile.regrowth_turns += 1 + const stageData = getRegrowthStage(tile.regrowth_stage) + if (!stageData) continue + + const baseTurns = stageData.turns_to_advance + const fungiBonus = Math.min( + Math.max(1.0 + tile.fungi_network * VEG.fungi_regrowth_bonus_cap, 1.0), + VEG.fungi_regrowth_bonus_cap, + ) + const effectiveTurns = Math.max(1, Math.round(baseTurns / fungiBonus)) + if (tile.regrowth_turns < effectiveTurns) continue + + const nextStage = tile.regrowth_stage + 1 + const nextData = getRegrowthStage(nextStage) + if (!nextData || nextStage > 3) { + tile.regrowth_stage = -1 + tile.regrowth_turns = 0 + continue + } + tile.regrowth_stage = nextStage + tile.regrowth_turns = 0 + tile.canopy_cover = nextData.canopy_target + tile.undergrowth = nextData.undergrowth_target + tile.fungi_network = nextData.fungi_target + if (nextStage >= 3) { + tile.regrowth_stage = -1 + tile.regrowth_turns = 0 + } + } +} + +""" + + +# --------------------------------------------------------------------------- +# Fauna simplified (guide doesn't have SQLite creature DB) +# --------------------------------------------------------------------------- + +def _fauna_simplified() -> str: + return """\ +// --------------------------------------------------------------------------- +// Fauna — simplified for guide (no SQLite creature DB) +// Uses tile-level habitat suitability + fish stock approximation. +// --------------------------------------------------------------------------- + +const FAUNA_WEIGHTS = { + undergrowth_weight: 0.6, + canopy_weight: 0.2, + fungi_weight: 0.2, +} as const + +function updateHabitatSuitability( + tiles: TileState[], w: number, h: number, +): void { + for (let i = 0; i < tiles.length; i++) { + const tile = tiles[i] + if (isWater(tile)) continue + let tu = 0, tc = 0, tf = 0, n = 0 + // Radius-2 neighborhood average + const nbs = neighbors(tile.col, tile.row, w, h) + for (const nb of nbs) { + const nt = tiles[idx(nb.col, nb.row, w)] + if (isWater(nt)) continue + tu += nt.undergrowth + tc += nt.canopy_cover + tf += nt.fungi_network + n++ + } + // Include self + tu += tile.undergrowth; tc += tile.canopy_cover; tf += tile.fungi_network; n++ + if (n > 0) { + tile.habitat_suitability = ( + (tu / n) * FAUNA_WEIGHTS.undergrowth_weight + + (tc / n) * FAUNA_WEIGHTS.canopy_weight + + (tf / n) * FAUNA_WEIGHTS.fungi_weight + ) + } else { + tile.habitat_suitability = 0.0 + } + } +} + +function updateFishStock(tiles: TileState[], w: number, h: number): void { + for (let i = 0; i < tiles.length; i++) { + const tile = tiles[i] + if (!isWater(tile) || (tile.fish_stock ?? 0) <= 0) continue + let tempMult = 0.5 // polar + if (tile.temperature > 0.55) tempMult = 1.0 // tropical + else if (tile.temperature > 0.25) tempMult = 0.8 // temperate + let cap = 100.0 + if (tile.reef_health > 0.5) cap *= 1.5 + else if (tile.reef_health < 0.1) cap *= 0.5 + const stock = tile.fish_stock ?? 0 + const growth = 0.05 * tempMult * stock * (1.0 - stock / cap) + tile.fish_stock = Math.max(0, Math.min(Math.round(stock + growth), cap)) + } +} + +""" + + +# --------------------------------------------------------------------------- +# Ecosystem quality computation (from ecosystem.gd) +# --------------------------------------------------------------------------- + +def _ecosystem_quality() -> str: + return """\ +// --------------------------------------------------------------------------- +// Ecosystem quality computation (from ecosystem.gd) +// --------------------------------------------------------------------------- + +const QUALITY_THRESHOLDS = [0.2, 0.4, 0.6, 0.8] as const +const W_FLORA = 0.30 +const W_FAUNA = 0.25 +const W_STABILITY = 0.25 +const W_BALANCE = 0.20 + +const FOOD_YIELD_MULT: Record = { + 1: 0.5, 2: 1.0, 3: 1.5, 4: 2.0, 5: 2.5, +} + +function scoreToTier(score: number): number { + if (score >= 0.8) return 5 + if (score >= 0.6) return 4 + if (score >= 0.4) return 3 + if (score >= 0.2) return 2 + return 1 +} + +function floraHealth(tile: TileState, biome: BiomeDef | null): number { + if (!biome) return 0.5 + const { canopy, undergrowth, fungi } = biome.flora_climax + const cMax = Math.max(canopy, 0.001) + const uMax = Math.max(undergrowth, 0.001) + const fMax = Math.max(fungi, 0.001) + const c = Math.min(tile.canopy_cover / cMax, 1.0) + const u = Math.min(tile.undergrowth / uMax, 1.0) + const f = Math.min(tile.fungi_network / fMax, 1.0) + return (c + u + f) / 3.0 +} + +function biomeStability(tile: TileState): number { + const classified = classifyBiome(tile) + if (classified === tile.biome_id) return 1.0 + // Partial credit for same family + if (classified.startsWith('temperate') && tile.biome_id.startsWith('temperate')) return 0.6 + if (classified.startsWith('tropical') && tile.biome_id.startsWith('tropical')) return 0.6 + return 0.2 +} + +/** Approximate fauna diversity from habitat suitability (no creature DB in guide). */ +function faunaDiversity(tile: TileState, biome: BiomeDef | null): number { + if (!biome) return 0.5 + // Habitat suitability as proxy for species diversity + return Math.min(tile.habitat_suitability / 0.7, 1.0) +} + +/** Approximate population balance from flora ratios (no creature DB in guide). */ +function populationBalance(tile: TileState): number { + // Healthy undergrowth implies herbivore support, balanced canopy implies + // predator-prey equilibrium. In the full game this uses SQLite creature counts. + if (tile.undergrowth < 0.1) return 0.3 + const ratio = tile.canopy_cover / Math.max(tile.undergrowth, 0.01) + // Ideal ratio around 1.0-2.0 + if (ratio >= 0.5 && ratio <= 3.0) return 1.0 + return 0.5 +} + +function computeTileQuality(tiles: TileState[], w: number, h: number): void { + for (let i = 0; i < tiles.length; i++) { + const tile = tiles[i] + if (isWater(tile)) continue + const biome = getBiome(tile.biome_id) + const flora = floraHealth(tile, biome) + const fauna = faunaDiversity(tile, biome) + const stability = biomeStability(tile) + const balance = populationBalance(tile) + const score = flora * W_FLORA + fauna * W_FAUNA + + stability * W_STABILITY + balance * W_BALANCE + let newQ = scoreToTier(score) + if (biome) { + const [qMin, qMax] = biome.quality_range + newQ = Math.max(qMin, Math.min(qMax, newQ)) + } + if (newQ >= 4 && tile.quality < 4 && tile.landmark_name === '') { + tile.landmark_name = `Ancient ${tile.biome_id.replace(/_/g, ' ')}` + } + tile.quality = newQ + } +} + +function computeGlobalHealth(grid: GridState): number { + let total = 0, count = 0 + for (const tile of grid.tiles) { + if (isWater(tile)) continue + total += tile.quality / 5.0 + count++ + } + return count > 0 ? total / count : 0.5 +} + +/** Food yield modifier for a tile based on quality. */ +export function getEcologyFoodModifier(tile: TileState): number { + let base = FOOD_YIELD_MULT[tile.quality] ?? 1.0 + if (!isWater(tile)) { + base *= 0.8 + 0.4 * tile.undergrowth // lerp(0.8, 1.2, undergrowth) + } + return base +} + +""" + + +# --------------------------------------------------------------------------- +# EcologyPhysics class +# --------------------------------------------------------------------------- + +def _ecosystem_class() -> str: + return """\ +// --------------------------------------------------------------------------- +// EcologyPhysics class — orchestrates flora + fauna + quality per turn +// --------------------------------------------------------------------------- + +// Biome recomputation deltas +const CANOPY_DELTA = 0.05 +const TEMP_DELTA = 0.02 +const MOISTURE_DELTA = 0.03 + +export class EcologyPhysics { + private lastCanopy: Float32Array | null = null + private lastTemp: Float32Array | null = null + private lastMoisture: Float32Array | null = null + + /** + * Process one turn of ecology dynamics. + * Call after ClimatePhysics.processStep(). + */ + processStep(grid: GridState): void { + const { tiles, width: w, height: h } = grid + + // Flora dynamics (6 ticks in order) + tickCanopy(tiles, w, h) + tickUndergrowth(tiles, w, h) + tickFungi(tiles, w, h) + tickSuccession(tiles, w, h) + tickDesertification(tiles, w, h) + tickRegrowth(tiles, w, h) + + // Fauna (simplified for guide) + updateHabitatSuitability(tiles, w, h) + updateFishStock(tiles, w, h) + + // Biome recomputation on significant changes + this.recomputeBiomes(tiles, w, h) + + // Quality scoring + computeTileQuality(tiles, w, h) + + // Global health + grid.ecosystem_health = computeGlobalHealth(grid) + } + + private recomputeBiomes(tiles: TileState[], w: number, h: number): void { + const n = tiles.length + if (!this.lastCanopy || this.lastCanopy.length !== n) { + this.lastCanopy = new Float32Array(n) + this.lastTemp = new Float32Array(n) + this.lastMoisture = new Float32Array(n) + for (let i = 0; i < n; i++) { + this.lastCanopy[i] = tiles[i].canopy_cover + this.lastTemp![i] = tiles[i].temperature + this.lastMoisture![i] = tiles[i].moisture + } + return + } + for (let i = 0; i < n; i++) { + const tile = tiles[i] + if (isWater(tile)) continue + const canopyD = Math.abs(tile.canopy_cover - this.lastCanopy[i]) + const tempD = Math.abs(tile.temperature - this.lastTemp![i]) + const moistD = Math.abs(tile.moisture - this.lastMoisture![i]) + + this.lastCanopy[i] = tile.canopy_cover + this.lastTemp![i] = tile.temperature + this.lastMoisture![i] = tile.moisture + + if (canopyD > CANOPY_DELTA || tempD > TEMP_DELTA || moistD > MOISTURE_DELTA) { + const newBiome = classifyBiome(tile) + if (newBiome !== tile.biome_id) { + tile.biome_id = newBiome + } + } + } + } +} + +// Re-export helpers for guide lenses +export { isWater, getBiome, classifyBiome, BIOME_DEFS } +export type { BiomeDef } +""" diff --git a/tools/transpile-engine/transpile.py b/tools/transpile-engine/transpile.py index 17987b6d..113052a1 100644 --- a/tools/transpile-engine/transpile.py +++ b/tools/transpile-engine/transpile.py @@ -10,9 +10,10 @@ Magic Civilization engine transpiler — thin CLI. Reads GDScript sources, transforms to TypeScript via lilith-gdscript-transpiler, -assembles two generated files: +assembles three generated files: 1. ClimatePhysics.generated.ts — climate simulation engine 2. MapGenerator.generated.ts — procedural map generation pipeline + 3. EcologyPhysics.generated.ts — flora/fauna/ecosystem dynamics Usage: uv run tools/transpile-engine/transpile.py # generate both @@ -34,6 +35,7 @@ from lilith_gdscript_transpiler import ( post_process, ) from mapgen_assembly import _mg_build_full_output +from ecology_assembly import _eco_build_full_output REPO = Path(__file__).resolve().parent.parent.parent @@ -59,8 +61,6 @@ REQUIRED_GD_FUNCTIONS: list[tuple[str, str]] = [ ("climate", "_update_lake_evaporation"), ("climate", "_update_deep_earth_water"), ("climate", "_update_precipitation"), - ("climate_base", "_check_terrain_evolution"), - ("climate_base", "_update_corruption"), ("climate", "_compute_global_stats"), ("climate", "_clear_magic_deltas"), ("climate_spec_eval", "ideal_terrain"), @@ -75,23 +75,18 @@ REQUIRED_GD_FUNCTIONS: list[tuple[str, str]] = [ CLIMATE_DEFAULTS: dict[str, float] = { "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, @@ -132,23 +127,18 @@ import { idx, neighbors, upwindPos, solarByRow, classifyTerrain, hashNoise, neig const CLIMATE_DEFAULTS: Record = { 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, @@ -188,8 +178,6 @@ METHOD_MAP = [ ("_update_lake_evaporation", "stepLakeEvaporation", "private"), ("_update_deep_earth_water", "stepDeepEarthWater", "private"), ("_update_precipitation", "stepPrecipitation", "private"), - ("_check_terrain_evolution", "stepTerrainEvolution", "private"), - ("_update_corruption", "stepCorruption", "private"), ("_compute_global_stats", "stepGlobalStats", "private"), ("_clear_magic_deltas", "stepClearDeltas", "private"), ] @@ -360,8 +348,6 @@ export class ClimatePhysics { this.stepLakeEvaporation(grid) this.stepDeepEarthWater(grid) this.stepPrecipitation(grid) - this.stepTerrainEvolution(grid) - this.stepCorruption(grid) const events = this.stepEcologicalEvents(grid, turn, seed) this.stepAnchorDecay(grid) this.stepGlobalStats(grid) @@ -878,6 +864,52 @@ mapgen_config = TranspilerConfig( ) +# =========================================================================== +# ECOLOGY TRANSPILER +# =========================================================================== + +ECOLOGY_SOURCES = { + "flora": REPO / "engine/src/modules/ecology/flora.gd", + "fauna": REPO / "engine/src/modules/ecology/fauna.gd", + "ecosystem": REPO / "engine/src/modules/ecology/ecosystem.gd", +} + +ECOLOGY_OUTPUT = REPO / "packages/engine-ts/src/EcologyPhysics.generated.ts" + +REQUIRED_ECOLOGY_FUNCTIONS: list[tuple[str, str]] = [ + ("flora", "process_turn"), + ("flora", "_tick_canopy"), + ("flora", "_tick_undergrowth"), + ("flora", "_tick_fungi"), + ("flora", "_tick_succession"), + ("flora", "_tick_desertification"), + ("flora", "_tick_regrowth"), + ("ecosystem", "process_turn"), + ("ecosystem", "_compute_tile_quality"), + ("ecosystem", "_compute_global_health"), +] + + +def assemble_ecology(fns: dict[str, dict[str, str]]) -> str: + """Assemble EcologyPhysics.generated.ts from ecology GDScript. + + The ecology system uses Dictionary-keyed tile maps, DataLoader, EventBus, + and SQLite — none of which exist in TypeScript. All sections are hand-written + TypeScript that faithfully implements the GDScript ecology pipeline on flat + TileState[] arrays. + """ + return _eco_build_full_output() + + +ecology_config = TranspilerConfig( + sources=ECOLOGY_SOURCES, + output=ECOLOGY_OUTPUT, + required_functions=REQUIRED_ECOLOGY_FUNCTIONS, + defaults={}, + assemble=assemble_ecology, +) + + # --------------------------------------------------------------------------- # Entry point # --------------------------------------------------------------------------- @@ -888,14 +920,16 @@ def main() -> int: climate_t = Transpiler(climate_config) mapgen_t = Transpiler(mapgen_config) + ecology_t = Transpiler(ecology_config) if check_mode: - # Check both — climate first, then mapgen. Each calls sys.exit(1) on stale. climate_t.check() mapgen_t.check() + ecology_t.check() else: climate_t.run() mapgen_t.run() + ecology_t.run() return 0