diff --git a/packages/engine-ts/src/ClimatePhysics.generated.ts b/packages/engine-ts/src/ClimatePhysics.generated.ts index 3cb81516..848be6f9 100644 --- a/packages/engine-ts/src/ClimatePhysics.generated.ts +++ b/packages/engine-ts/src/ClimatePhysics.generated.ts @@ -12,23 +12,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, @@ -206,8 +201,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) @@ -271,8 +264,8 @@ export class ClimatePhysics { private stepTemperature(grid: GridState): void { const { tiles, width: w, height: h } = grid let conductivity = (this.params as any)["wind_conductivity"] ?? 0.1 - let energy_scale = (this.params as any)["energy_scale"] ?? 0.01 - let relaxation = (this.params as any)["equilibrium_relaxation"] ?? 0.05 + let energy_scale = (this.params as any)["energy_scale"] ?? 0.005 + let relaxation = (this.params as any)["equilibrium_relaxation"] ?? 0.08 // Pre-compute solar baseline per row — same value for every tile in a row for (let row = 0; row < h; row++) { @@ -293,7 +286,7 @@ export class ClimatePhysics { let solar = solarByRow(row, h) let current_temp = oldTemp[i] - let terrain_data = (this.terrainCache.get(tile.terrain_id) ?? {}) + let terrain_data = (this.terrainCache.get(tile.biome_id) ?? {}) let albedo = (terrain_data as any)["albedo"] ?? 0.4 let net_solar = solar * (1.0 - albedo) * energy_scale @@ -325,7 +318,7 @@ export class ClimatePhysics { for (let i = 0; i < tiles.length; i++) { const tile = tiles[i] const { col, row } = tile - if (tile.terrain_id !== "lake") { + if (tile.biome_id !== "lake") { continue } let lake_temp = tile.temperature @@ -344,10 +337,10 @@ export class ClimatePhysics { private stepMoistureWind(grid: GridState): void { const { tiles, width: w, height: h } = grid let transport_rate = (this.params as any)["moisture_transport"] ?? 0.15 - let decay = (this.params as any)["moisture_decay"] ?? 0.98 + let decay = (this.params as any)["moisture_decay"] ?? 0.995 let rain_shadow_block = (this.params as any)["mountain_rain_shadow_block"] ?? 0.9 - let relaxation = (this.params as any)["moisture_relaxation"] ?? 0.02 - let atmo_loss = (this.params as any)["atmospheric_loss_rate"] ?? 0.001 + let relaxation = (this.params as any)["moisture_relaxation"] ?? 0.04 + let atmo_loss = (this.params as any)["atmospheric_loss_rate"] ?? 0.0003 // Snapshot moisture before decay so all tiles decay from the same baseline const oldMoisture = new Float32Array(tiles.length) @@ -368,13 +361,13 @@ export class ClimatePhysics { let transported = 0.0 if (upwind_pos !== null) { const upwind_tile = tiles[idx(upwind_pos.col, upwind_pos.row, w)] - let upwind_is_mountain = ( upwind_tile !== null && upwind_tile.terrain_id === "mountains" ) + let upwind_is_mountain = ( upwind_tile !== null && upwind_tile.biome_id === "mountains" ) let block = (upwind_is_mountain ? rain_shadow_block : 0.0) transported = ( oldMoisture[idx(upwind_pos.col, upwind_pos.row, w)] * tile.wind_speed * transport_rate * (1.0 - block) ) } // Evapotranspiration and magic forcing are per-tile local effects — safe to add here - let terrain_data = (this.terrainCache.get(tile.terrain_id) ?? {}) + let terrain_data = (this.terrainCache.get(tile.biome_id) ?? {}) let evapotrans = (terrain_data as any)["evapotranspiration"] ?? 0.0 // Moisture equilibrium relaxation — pull toward climate baseline (like temperature) @@ -423,7 +416,7 @@ export class ClimatePhysics { private stepLakeEvaporation(grid: GridState): void { const { tiles, width: w, height: h } = grid let base_evap = (this.params as any)["evaporation_rate"] ?? 0.05 - let max_hops = Math.floor( (this.params as any)["ocean_evaporation_hops"] ?? 3 ) + let max_hops = Math.floor( (this.params as any)["ocean_evaporation_hops"] ?? 4 ) let hop_decay = (this.params as any)["ocean_evaporation_hop_decay"] ?? 0.5 let rain_shadow_block = (this.params as any)["mountain_rain_shadow_block"] ?? 0.9 // Biological pump failure: dead ocean reduces evaporation → inland forests dry out. @@ -436,7 +429,7 @@ export class ClimatePhysics { for (let i = 0; i < tiles.length; i++) { const tile = tiles[i] const { col, row } = tile - let tid = tile.terrain_id + let tid = tile.biome_id if (tid !== "lake" && tid !== "ocean" && tid !== "coast") { continue @@ -468,7 +461,7 @@ export class ClimatePhysics { if (hop_tile === null) { break } - let hop_tid = hop_tile.terrain_id + let hop_tid = hop_tile.biome_id // Stop if we hit another water tile (no double-injection) if (hop_tid === "ocean" || hop_tid === "coast" || hop_tid === "lake") { break @@ -506,7 +499,7 @@ export class ClimatePhysics { const tile = tiles[i] const { col, row } = tile - if (tile.terrain_id === "volcano") { + if (tile.biome_id === "volcano") { tile.moisture = Math.min(1.0, Math.max(0.0, tile.moisture + vol_self)) for (const nb_pos of neighbors(col, row, w, h)) { const nb = tiles[idx(nb_pos.col, nb_pos.row, w)] @@ -566,170 +559,6 @@ export class ClimatePhysics { } } - private stepTerrainEvolution(grid: GridState): void { - const { tiles, width: w, height: h } = grid - let up_thresh = (this.params as any)["quality_up_threshold"] ?? 10 - let down_thresh = (this.params as any)["quality_down_threshold"] ?? 5 - - for (let i = 0; i < tiles.length; i++) { - const tile = tiles[i] - const { col, row } = tile - let tid = tile.terrain_id - - // Natural wonders are geological formations — quality evolves but terrain doesn't flip - if (tile.is_natural_wonder) { - continue - - } - // Water, corrupted, and fixed-form terrains don't evolve via climate - if (( tid === "ocean" || tid === "coast" || tid === "lake" || tid === "corrupted_land" || tid === "volcano" )) { - continue - - } - let ideal = idealTerrain(tile, this.spec) - - if (ideal === tid) { - tile.quality_progress += 1 - if (tile.quality_progress >= up_thresh) { - tile.quality_progress = 0 - if (tile.quality < 5) { - let old_q = tile.quality - tile.quality += 1 - } else { - tile.quality_progress -= 1 - if (tile.quality_progress <= -down_thresh) { - tile.quality_progress = 0 - if (tile.quality > 1) { - let old_q = tile.quality - tile.quality -= 1 - } else { - // Quality at floor — terrain flips one step along its chain - let old_type = tid - tile.terrain_id = ideal - tile.quality = 1 - tile.quality_progress = 0 - - } - } - } - } - } - } - } - - private stepCorruption(grid: GridState): void { - const { tiles, width: w, height: h } = grid - let spread_rate = (this.params as any)["corruption_spread_rate"] ?? 0.02 - let flip_threshold = (this.params as any)["corruption_flip_threshold"] ?? 0.5 - let decay_rate = (this.params as any)["corruption_decay_rate"] ?? 0.004 - let heal_rate = (this.params as any)["corruption_heal_rate"] ?? 0.008 - let heal_threshold = (this.params as any)["corruption_heal_threshold"] ?? 0.15 - - // Accumulate pressure increments into a separate dict — one O(n) read pass, - // then one O(n) write pass. Avoids double-counting and in-place mutation. - const pressureDeltas = new Float32Array(tiles.length) - - for (let i = 0; i < tiles.length; i++) { - const tile = tiles[i] - const { col, row } = tile - if (tile.corruption_pressure <= 0.0) { - continue - } - // terrain_power corruption_spread_modifier on the SOURCE tile scales spread rate. - // e.g. jungle = 0.5x (resists spread), swamp = 1.5x (accelerates spread). - let source_modifier = 1.0 - let base_spread = spread_rate * source_modifier - for (const nb_pos of neighbors(col, row, w, h)) { - if (!true) { - continue - } - let nb = tiles[idx(nb_pos.col, nb_pos.row, w)] - if (nb.terrain_id === "corrupted_land") { - continue - } - // Ley line channeling: spread rate is modified by the ley properties of the - // receiving tile. Death ley = 3x, generic ley edge = 2x, - // Nature/Life ley = 0.5x (resists). Off-ley = no channeling modifier. - let ley_mult = leyChannelingMult(nb, this.spec) - // Moisture resistance: high-moisture terrain (jungle, swamp, water) resists - // corruption biologically. Marine life (fish, reefs, algae) can still carry - // corruption through water — so water isn't immune, just dampened. - let moisture_resist = 1.0 - nb.moisture * 0.4 - // City protection buildings write corruption_resistance_pct to scale down spread - let corruption_resist = 1.0 - Math.min(1.0, Math.max(0.0, (nb.corruption_resistance_pct ?? 0))) - pressureDeltas[idx(nb_pos.col, nb_pos.row, w)] += base_spread * ley_mult * moisture_resist * corruption_resist - - } - } - // Apply pressure deltas + natural decay + Life/Nature ley active healing - for (let i = 0; i < tiles.length; i++) { - const tile = tiles[i] - const { col, row } = tile - let incoming = pressureDeltas[i] - - // Natural pressure decay — corruption dissipates without active feeding sources - let drain = decay_rate - - // Life/Nature ley lines actively heal (bonus drain, capped at 3 stacks) - if (tile.ley_line_count > 0 && (tile.ley_school === "life" || tile.ley_school === "nature")) { - drain += heal_rate * Math.min(tile.ley_line_count, 3.0) - - } - if (incoming === 0.0 && tile.corruption_pressure === 0.0) { - continue - } - tile.corruption_pressure = Math.min(1.0, Math.max(0.0, tile.corruption_pressure + incoming - drain)) - - } - // Terrain flip: pressure crosses threshold → corrupted_land - // Water tiles (ocean, lake, inland_sea, coast) never flip terrain — corruption - // in water expresses through the marine ecosystem (reef_health, fish_stock) instead. - for (let i = 0; i < tiles.length; i++) { - const tile = tiles[i] - const { col, row } = tile - let tid = tile.terrain_id - if (( tid === "corrupted_land" || tid === "ocean" || tid === "lake" || tid === "inland_sea" || tid === "coast" )) { - continue - } - if (tile.corruption_pressure > flip_threshold) { - if (tile.original_terrain_id === "") { - tile.original_terrain_id = tile.terrain_id - } - tile.terrain_id = "corrupted_land" - - } - } - // Marine corruption: coast tiles with elevated pressure degrade reef and fish stock - // (corrupted fish and algae carry corruption through the marine ecosystem) - for (let i = 0; i < tiles.length; i++) { - const tile = tiles[i] - const { col, row } = tile - if (tile.terrain_id !== "coast") { - continue - } - if (tile.corruption_pressure > 0.2) { - tile.reef_health = Math.max(0.0, tile.reef_health - tile.corruption_pressure * 0.015) - if ((tile.fish_stock ?? 0) > 0) { - tile.fish_stock = Math.max(0.0, (tile.fish_stock ?? 0) - tile.corruption_pressure * 0.01) - - } - } - } - // Terrain healing: corrupted tile's pressure falls below heal threshold → restore - for (let i = 0; i < tiles.length; i++) { - const tile = tiles[i] - const { col, row } = tile - if (tile.terrain_id !== "corrupted_land" || tile.corruption_pressure > heal_threshold) { - continue - } - let original = tile.original_terrain_id - let restore_to = (original !== "" ? original : "grassland") - tile.terrain_id = restore_to - tile.original_terrain_id = "" - - } - } - private stepGlobalStats(grid: GridState): void { const { tiles, width: w, height: h } = grid let total = 0.0 @@ -737,7 +566,7 @@ export class ClimatePhysics { for (let i = 0; i < tiles.length; i++) { const tile = tiles[i] const { col, row } = tile - if (tile.terrain_id !== "ocean") { + if (tile.biome_id !== "ocean") { total += tile.temperature count += 1 } @@ -755,7 +584,7 @@ export class ClimatePhysics { for (let i = 0; i < tiles.length; i++) { const tile = tiles[i] const { col, row } = tile - if (tile.terrain_id !== "coast") { + if (tile.biome_id !== "coast") { continue } coast_count += 1 diff --git a/packages/engine-ts/src/runner.ts b/packages/engine-ts/src/runner.ts index 2cf0b456..05a95faf 100644 --- a/packages/engine-ts/src/runner.ts +++ b/packages/engine-ts/src/runner.ts @@ -20,7 +20,7 @@ import { WORLD_SEED, DEFAULT_SCENARIO_TURNS } from './configs' export const TERRAIN_ORDER: readonly string[] = [ 'ocean', 'coast', 'lake', 'inland_sea', 'ice', 'snow', 'tundra', 'desert', 'plains', 'grassland', 'forest', 'boreal_forest', 'jungle', 'enchanted_forest', - 'hills', 'mountains', 'swamp', 'corrupted_land', 'volcano', + 'hills', 'mountains', 'swamp', 'volcano', // Natural wonders (geological/biological formations) 'mana_node', 'ley_nexus', 'lodestone_spire', 'crystal_cavern', 'worldroot', 'primordial_spring', 'abyssal_vortex', @@ -91,6 +91,7 @@ export function encodeSnapshot( const n = grid.tiles.length const texA = new Float32Array(n * 4) const texB = new Float32Array(n * 4) + const texC = new Float32Array(n * 4) for (let i = 0; i < n; i++) { const tile = grid.tiles[i] @@ -98,7 +99,7 @@ export function encodeSnapshot( texA[base + 0] = tile.temperature texA[base + 1] = tile.moisture - texA[base + 2] = tile.corruption_pressure + texA[base + 2] = tile.canopy_cover ?? 0.0 texA[base + 3] = tile.reef_health texB[base + 0] = tile.wind_direction / 5 @@ -107,12 +108,17 @@ export function encodeSnapshot( let riverMask = 0 for (const e of tile.river_edges) riverMask |= (1 << e) texB[base + 3] = riverMask / 63 + + texC[base + 0] = tile.undergrowth ?? 0.0 + texC[base + 1] = tile.fungi_network ?? 0.0 + texC[base + 2] = (tile.quality ?? 1) / 5.0 + texC[base + 3] = tile.habitat_suitability ?? 0.0 } const stats = computeTurnStats(grid) return { - texA, texB, + texA, texB, texC, width: grid.width, height: grid.height, turn, @@ -132,7 +138,6 @@ export function computeTurnStats(grid: GridState): TurnStats { let tempSum = 0 let moistSum = 0 let landCount = 0 - let corruptedCount = 0 const terrain_counts: Record = {} for (const tile of tiles) { @@ -143,14 +148,12 @@ export function computeTurnStats(grid: GridState): TurnStats { landCount++ tempSum += tile.temperature moistSum += tile.moisture - if (tile.terrain_id === 'corrupted_land') corruptedCount++ } } return { avg_temp: landCount > 0 ? tempSum / landCount : 0.5, avg_moisture: landCount > 0 ? moistSum / landCount : 0.5, - corrupted_pct: landCount > 0 ? corruptedCount / landCount : 0, total_ley_strength: 0, dominant_ley_school: '', ley_school_strengths: { ...EMPTY_SCHOOL_RECORD }, @@ -178,9 +181,11 @@ export function cloneGridState(grid: GridState): GridState { height: grid.height, global_avg_temp: grid.global_avg_temp, ocean_dead_fraction: grid.ocean_dead_fraction, + ecosystem_health: grid.ecosystem_health, tiles: grid.tiles.map((t) => ({ ...t, river_edges: [...t.river_edges], + wonder_anchor_schools: [...t.wonder_anchor_schools], })), } } diff --git a/packages/engine-ts/src/types.ts b/packages/engine-ts/src/types.ts index 8b66a792..f16c145f 100644 --- a/packages/engine-ts/src/types.ts +++ b/packages/engine-ts/src/types.ts @@ -8,13 +8,13 @@ export interface TileState { moisture: number // [0, 1] elevation: number // [0, 1] terrain_id: string + biome_id: string // computed biome from substrate + climate + flora wind_direction: number // [0, 5] axial direction index wind_speed: number // [0, 1] - quality: number // 1 | 2 | 3 + quality: number // 1-5 (Q1 prolific .. Q5 epic) quality_progress: number // counter toward next quality change river_edges: number[] // edge indices [0-5] where rivers flow flow_accumulation: number - corruption_pressure: number // [0, 1] original_terrain_id: string ley_line_count: number ley_school: LeySchool | '' @@ -26,10 +26,24 @@ export interface TileState { wonder_anchor_school: School // LEGACY — single school (kept for anchor decay compat) wonder_anchor_schools: LeySchool[] // multi-school affinities for ley network wonder_tier: number // 1-5, aligned with eras (separate from terrain quality) + // Substrate fields (set at map gen, rarely change) + substrate_id: string // geological substrate from elevation + water_body_id: number // water body index (-1 if land) + depth_from_coast: number // BFS distance from land (-1 if land) + // Flora fields (updated per turn by ecology system) + canopy_cover: number // [0, 1] forest canopy density + undergrowth: number // [0, 1] ground vegetation density + fungi_network: number // [0, 1] mycorrhizal network density + drought_counter: number // turns of consecutive low moisture + succession_progress: number // turns of stable high canopy + regrowth_stage: number // -1 = none, 0-3 = barren→forest + regrowth_turns: number // turns in current regrowth stage + // Fauna fields (updated per turn by ecology system) + habitat_suitability: number // [0, 1] weighted flora average + landmark_name: string // Q4+ tile name from FlavorGenerator // Optional fields written by subsystems (not present on all tiles) river_source_type?: string // 'snowmelt' | 'spring' | 'hot_spring' | 'glacial' | undefined fish_stock?: number // marine ecosystem fish population [0, 1] - corruption_resistance_pct?: number // city building protection [0, 1] } export interface GridState { @@ -38,12 +52,12 @@ export interface GridState { height: number global_avg_temp: number ocean_dead_fraction: number + ecosystem_health: number // [0, 1] global ecology health } export interface TurnStats { avg_temp: number avg_moisture: number - corrupted_pct: number total_ley_strength: number dominant_ley_school: LeySchool | '' ley_school_strengths: Record @@ -52,12 +66,14 @@ export interface TurnStats { terrain_counts: Record } -// Packed per-tile data for rendering: 2 RGBA float textures -// texA: [temperature, moisture, corruption_pressure, reef_health] +// Packed per-tile data for rendering: 3 RGBA float textures +// texA: [temperature, moisture, canopy_cover, reef_health] // texB: [wind_direction/5, wind_speed, terrain_encoded, river_bitmask/63] +// texC: [undergrowth, fungi_network, quality/5, habitat_suitability] export interface GridSnapshot { texA: Float32Array // width * height * 4 texB: Float32Array // width * height * 4 + texC: Float32Array // width * height * 4 (ecology) width: number height: number turn: number @@ -134,7 +150,7 @@ export interface TerrainData { color: [number, number, number] flags: string[] climate_zone: string - terrain_power?: { spell_school?: string; corruption_spread_modifier?: number } + terrain_power?: { spell_school?: string } mana_major?: { school: string; amount: number } ley_anchor_strength?: number ley_anchor_school?: string