diff --git a/tools/transpile-engine/ecology_assembly.py b/tools/transpile-engine/ecology_assembly.py index f2238b90..9266d4cb 100644 --- a/tools/transpile-engine/ecology_assembly.py +++ b/tools/transpile-engine/ecology_assembly.py @@ -135,9 +135,9 @@ def _classifier() -> str: function classifyBiome(tile: TileState): string { const sub = tile.substrate_id - // Aquatic tiles keep their terrain_id + // Aquatic tiles keep their biome_id if (sub === 'deep_water' || sub === 'shallow_water' || sub === 'lake_bed') { - return tile.terrain_id + return tile.biome_id } const temp = tile.temperature @@ -182,7 +182,7 @@ function isWater(tile: TileState): boolean { if (sub) { return sub === 'deep_water' || sub === 'shallow_water' || sub === 'lake_bed' } - return tile.terrain_id === 'ocean' || tile.terrain_id === 'coast' + return tile.biome_id === 'ocean' || tile.biome_id === 'coast' } function climateMatch(tile: TileState, biome: BiomeDef): number { diff --git a/tools/transpile-engine/mapgen_assembly.py b/tools/transpile-engine/mapgen_assembly.py index 668dfac8..0d8c5e7a 100644 --- a/tools/transpile-engine/mapgen_assembly.py +++ b/tools/transpile-engine/mapgen_assembly.py @@ -233,6 +233,7 @@ class GenMap { readonly width: number readonly height: number readonly tiles: Map = new Map() + sea_level = 0.0 constructor(width: number, height: number) { this.width = width @@ -300,6 +301,8 @@ class GenMap { height: this.height, global_avg_temp: 0.5, ocean_dead_fraction: 0.0, + ecosystem_health: 1.0, + sea_level: this.sea_level, } } } @@ -630,6 +633,7 @@ function assignSeaLevel( Math.max(Math.round(oceanTarget * allElevs.length), 0), allElevs.length - 1, ) const seaLevel = allElevs[seaIdx] + gm.sea_level = seaLevel for (const tile of gm.tiles.values()) { const elev = elevation.get(axialKey(tile.axial)) ?? 0.0 diff --git a/tools/transpile-engine/transpile.py b/tools/transpile-engine/transpile.py index afc3dfa6..5df57350 100644 --- a/tools/transpile-engine/transpile.py +++ b/tools/transpile-engine/transpile.py @@ -356,6 +356,7 @@ export class ClimatePhysics { this.stepDeepEarthWater(grid) this.stepPrecipitation(grid) this.stepTerrainEvolution(grid) + this.stepSeaLevel(grid) const events = this.stepEcologicalEvents(grid, turn, seed) this.stepAnchorDecay(grid) this.stepGlobalStats(grid) @@ -771,7 +772,31 @@ def _emit_terrain_evolution() -> str: const tid = tile.biome_id if (tile.is_natural_wonder) continue - if (tid === 'ocean' || tid === 'coast' || tid === 'lake' || tid === 'volcano') continue + + // Water freezing/thawing — ice forms below freeze threshold, thaws above + const freezeTemp = this.p('water_freeze_threshold', 0.12) + const thawTemp = freezeTemp + 0.03 // hysteresis prevents oscillation + const isWater = tid === 'ocean' || tid === 'coast' || tid === 'lake' || tid === 'inland_sea' + if (isWater) { + if (tile.temperature < freezeTemp) { + tile.original_biome_id = tid + tile.biome_id = 'ice' + tile.quality = 1 + tile.quality_progress = 0 + } + continue + } + if (tid === 'ice') { + if (tile.temperature > thawTemp && tile.original_biome_id) { + tile.biome_id = tile.original_biome_id + tile.original_biome_id = '' + tile.quality = 1 + tile.quality_progress = 0 + } + continue + } + + if (tid === 'volcano') continue const ideal = idealTerrain(tile, this.spec) @@ -800,6 +825,52 @@ def _emit_terrain_evolution() -> str: """ +def _emit_sea_level() -> str: + return """\ + private stepSeaLevel(grid: GridState): void { + // Sea level responds to global temperature: warmer → thermal expansion + ice melt → higher. + // Each turn, sea_level drifts toward an equilibrium determined by global_avg_temp. + // Tiles below sea_level flood to coast/ocean; tiles above expose to land. + const sensitivity = this.p('sea_level_temp_sensitivity', 0.0008) + const eqTemp = this.p('sea_level_equilibrium_temp', 0.45) + const { tiles, width: w, height: h } = grid + + // Adjust sea level: positive when warmer than equilibrium, negative when cooler + const tempAnomaly = grid.global_avg_temp - eqTemp + grid.sea_level += tempAnomaly * sensitivity + + // Flood low land / expose high water + let changed = false + for (let i = 0; i < tiles.length; i++) { + const tile = tiles[i] + const isWater = tile.biome_id === 'ocean' || tile.biome_id === 'coast' || + tile.biome_id === 'lake' || tile.biome_id === 'inland_sea' + + if (!isWater && tile.elevation < grid.sea_level) { + // Land tile floods — becomes coast if adjacent to land, ocean otherwise + if (tile.is_natural_wonder) continue + tile.original_biome_id = tile.biome_id + tile.biome_id = 'coast' + tile.quality = 1 + tile.quality_progress = 0 + changed = true + } else if (tile.biome_id === 'coast' && tile.elevation >= grid.sea_level + 0.02) { + // Coast tile exposed — reclassify as land based on climate + // Small hysteresis buffer (0.02) prevents oscillation at the boundary + tile.biome_id = classifyTerrain(tile.temperature, tile.moisture, tile.elevation) + tile.quality = 1 + tile.quality_progress = 0 + changed = true + } + } + + // Invalidate ocean distance cache if coastline changed + if (changed) this.oceanDistGridId = -1 + } + +""" + + def _emit_anchor_decay() -> str: return """\ private stepAnchorDecay(grid: GridState): void { @@ -848,6 +919,7 @@ def assemble(fns: dict[str, dict[str, str]]) -> str: parts.append(f" // NOTE: {gd_name} not found in climate.gd\n") parts.append(f" {visibility} {ts_name}(grid: GridState): void {{ }}\n\n") + parts.append(_emit_sea_level()) parts.append(_emit_ecological_events()) parts.append("\n") parts.append(_emit_anchor_decay())