feat(engine): ✨ Add climate physics, ecology simulation, and map generation logic with new models, runner updates, and type definitions
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
f7f5d3feb3
commit
e1fe0afe7f
6 changed files with 21 additions and 61 deletions
|
|
@ -60,6 +60,7 @@ export function idealTerrain(
|
|||
const temp = tile.temperature
|
||||
const moist = tile.moisture
|
||||
const elev = tile.elevation
|
||||
const canopy = tile.canopy_cover ?? 0
|
||||
|
||||
const transitions = (spec['terrain_transitions'] ?? {}) as Record<string, Array<Record<string, unknown>>>
|
||||
const rules: Array<Record<string, unknown>> = transitions[tid] ?? []
|
||||
|
|
@ -67,7 +68,7 @@ export function idealTerrain(
|
|||
|
||||
for (const rule of rules) {
|
||||
const cond = rule['condition'] as string ?? ''
|
||||
if (evalCondition(cond, temp, moist, elev)) {
|
||||
if (evalCondition(cond, temp, moist, elev, canopy)) {
|
||||
const becomes = rule['becomes'] as string ?? ''
|
||||
if (becomes === 'classify') return classifyTerrain(temp, moist, elev)
|
||||
return becomes
|
||||
|
|
@ -88,17 +89,17 @@ export function leyChannelingMult(
|
|||
return leySpec['on_ley_generic'] ?? 2.0
|
||||
}
|
||||
|
||||
export function evalCondition(cond: string, temp: number, moist: number, elev: number): boolean {
|
||||
export function evalCondition(cond: string, temp: number, moist: number, elev: number, canopy = 0): boolean {
|
||||
if (cond.includes(' OR ')) {
|
||||
return cond.split(' OR ').some(part => evalCondition(part.trim(), temp, moist, elev))
|
||||
return cond.split(' OR ').some(part => evalCondition(part.trim(), temp, moist, elev, canopy))
|
||||
}
|
||||
if (cond.includes(' AND ')) {
|
||||
return cond.split(' AND ').every(part => evalCondition(part.trim(), temp, moist, elev))
|
||||
return cond.split(' AND ').every(part => evalCondition(part.trim(), temp, moist, elev, canopy))
|
||||
}
|
||||
let clean = cond.trim()
|
||||
if (clean.startsWith('(') && clean.endsWith(')')) clean = clean.slice(1, -1)
|
||||
const tokens = clean.trim().split(' ')
|
||||
if (tokens.length < 3) { console.warn(`ClimateSpecEval: bad condition: '${cond}'`); return false }
|
||||
if (tokens.length < 3) return false
|
||||
const [field, op, valStr] = tokens
|
||||
const value = parseFloat(valStr)
|
||||
let actual: number
|
||||
|
|
@ -106,7 +107,8 @@ export function evalCondition(cond: string, temp: number, moist: number, elev: n
|
|||
case 'temperature': actual = temp; break
|
||||
case 'moisture': actual = moist; break
|
||||
case 'elevation': actual = elev; break
|
||||
default: console.warn(`ClimateSpecEval: unknown field '${field}'`); return false
|
||||
case 'canopy': actual = canopy; break
|
||||
default: return false
|
||||
}
|
||||
switch (op) {
|
||||
case '<': return actual < value
|
||||
|
|
@ -265,7 +267,7 @@ export class ClimatePhysics {
|
|||
|
||||
let any_aerosol = false
|
||||
for (let i = 0; i < tiles.length; i++) {
|
||||
if (((tiles[i] as any).sulfate_aerosol ?? 0) > 0.001) { any_aerosol = true; break }
|
||||
if (((tiles[i] as any).sulfate_aerosol ?? 0) > 0.1) { any_aerosol = true; break }
|
||||
}
|
||||
if (!any_aerosol) return
|
||||
const cooling_rate = aerosol_cfg['cooling_rate'] ?? 0.06
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ function getBiome(biomeId: string): BiomeDef | null {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
function _getSubstrate(tile: TileState): string {
|
||||
if (true) {
|
||||
if (tile.substrate_id !== "") {
|
||||
return tile.substrate_id
|
||||
}
|
||||
return ""
|
||||
|
|
@ -262,7 +262,7 @@ function classifyBiome(tile: TileState): string {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
function _isWater(tile: TileState): boolean {
|
||||
if (true) {
|
||||
if (tile.substrate_id !== "") {
|
||||
let sub = tile.substrate_id
|
||||
return ["deep_water", "shallow_water", "lake_bed"].includes(sub)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -249,7 +249,7 @@ class GenMap {
|
|||
wind_direction: gt?.wind_direction ?? 0,
|
||||
wind_speed: gt?.wind_speed ?? 0.5,
|
||||
quality: gt?.quality ?? 2,
|
||||
quality_progress: gt?.quality_progress ?? 0,
|
||||
quality_progress: gt?.quality_progress ?? (((col * 7 + row * 13) % 11) - 5), // stagger -5..+5 to prevent synchronized biome flips
|
||||
river_edges: gt?.river_edges ?? [],
|
||||
flow_accumulation: gt?.flow_accumulation ?? 0.0,
|
||||
original_biome_id: '',
|
||||
|
|
@ -272,6 +272,7 @@ class GenMap {
|
|||
pressure: 1013.0,
|
||||
pressure_anomaly: 0.0,
|
||||
humidity: 0.0,
|
||||
sulfate_aerosol: 0.0,
|
||||
// Ecology fields (populated by EcologyPhysics)
|
||||
canopy_cover: 0.0,
|
||||
undergrowth: 0.0,
|
||||
|
|
|
|||
|
|
@ -81,50 +81,6 @@ function encodeTerrainId(terrainId: string): number {
|
|||
return i / (TERRAIN_ORDER.length - 1)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Terrain cache
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function buildTerrainCache(): Map<string, TerrainData> {
|
||||
const terrainMods = import.meta.glob(
|
||||
'@data/terrain/*.json',
|
||||
{ eager: true, import: 'default' },
|
||||
) as Record<string, { terrains: TerrainData[] }>
|
||||
|
||||
const cache = new Map<string, TerrainData>()
|
||||
for (const mod of Object.values(terrainMods)) {
|
||||
if (!mod?.terrains) continue
|
||||
for (const terrain of mod.terrains) {
|
||||
cache.set(terrain.id, terrain)
|
||||
}
|
||||
}
|
||||
return cache
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Params loading
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type ClimateParamsRaw = Record<string, number | Record<string, unknown>>
|
||||
|
||||
function flattenParams(raw: ClimateParamsRaw): Record<string, number> {
|
||||
const out: Record<string, number> = {}
|
||||
for (const [k, v] of Object.entries(raw)) {
|
||||
if (typeof v === 'number') out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
export function loadClimateParams(): Record<string, number> {
|
||||
const mods = import.meta.glob(
|
||||
'@data/climate_params.json',
|
||||
{ eager: true, import: 'default' },
|
||||
) as Record<string, ClimateParamsRaw>
|
||||
const raw = Object.values(mods)[0]
|
||||
if (!raw) throw new Error('climate_params.json not found via @data/ alias')
|
||||
return flattenParams(raw)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Snapshot encoding
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -133,7 +89,6 @@ export function encodeSnapshot(
|
|||
grid: GridState,
|
||||
turn: number,
|
||||
events: EcologicalEvent[] = [],
|
||||
terrainCache?: Map<string, TerrainData>,
|
||||
prevStats?: TurnStats,
|
||||
): GridSnapshot {
|
||||
const n = grid.tiles.length
|
||||
|
|
@ -163,7 +118,7 @@ export function encodeSnapshot(
|
|||
texC[base + 3] = tile.habitat_suitability ?? 0.0
|
||||
}
|
||||
|
||||
const stats = computeTurnStats(grid, terrainCache, prevStats)
|
||||
const stats = computeTurnStats(grid, prevStats)
|
||||
|
||||
return {
|
||||
texA, texB, texC,
|
||||
|
|
@ -183,7 +138,6 @@ const EMPTY_SCHOOL_RECORD: Record<LeySchool, number> = { death: 0, life: 0, natu
|
|||
|
||||
export function computeTurnStats(
|
||||
grid: GridState,
|
||||
terrainCache?: Map<string, TerrainData>,
|
||||
prevStats?: TurnStats,
|
||||
): TurnStats {
|
||||
const { tiles, width, height } = grid
|
||||
|
|
@ -224,7 +178,7 @@ export function computeTurnStats(
|
|||
albedoSum += albedo
|
||||
solarSum += solar * (1.0 - albedo)
|
||||
|
||||
aerosolSum += (tile as Record<string, number>).sulfate_aerosol ?? 0
|
||||
aerosolSum += tile.sulfate_aerosol ?? 0
|
||||
windSpeedSum += tile.wind_speed ?? 0
|
||||
|
||||
if (isWater) {
|
||||
|
|
@ -297,6 +251,7 @@ export function cloneGridState(grid: GridState): GridState {
|
|||
global_avg_temp: grid.global_avg_temp,
|
||||
ocean_dead_fraction: grid.ocean_dead_fraction,
|
||||
ecosystem_health: grid.ecosystem_health,
|
||||
sea_level: grid.sea_level,
|
||||
tiles: grid.tiles.map((t) => ({
|
||||
...t,
|
||||
river_edges: [...t.river_edges],
|
||||
|
|
@ -371,7 +326,7 @@ export function runScenarioSync(
|
|||
const events = physics.processStep(grid, turn, worldSeed)
|
||||
ecology.processStep(grid)
|
||||
const prev = snapshots.length > 0 ? snapshots[snapshots.length - 1].stats : undefined
|
||||
snapshots.push(encodeSnapshot(grid, scenarioTurn, events, terrainCache, prev))
|
||||
snapshots.push(encodeSnapshot(grid, scenarioTurn, events, prev))
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -409,7 +364,7 @@ export function extendSimulation(
|
|||
const events = physics.processStep(grid, turn, worldSeed)
|
||||
ecology.processStep(grid)
|
||||
const prev = snapshots.length > 0 ? snapshots[snapshots.length - 1].stats : undefined
|
||||
snapshots.push(encodeSnapshot(grid, scenarioTurn, events, terrainCache, prev))
|
||||
snapshots.push(encodeSnapshot(grid, scenarioTurn, events, prev))
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export interface TileState {
|
|||
pressure: number // atmospheric pressure (hPa, ~995-1030)
|
||||
pressure_anomaly: number // dynamic anomaly offset from baseline
|
||||
humidity: number // atmospheric humidity [0, 1]
|
||||
sulfate_aerosol: number // stratospheric aerosol opacity [0, 1] from volcanic/impact events
|
||||
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
|
||||
|
|
@ -167,7 +168,7 @@ export interface SimulationResult {
|
|||
continuation: ContinuationState
|
||||
}
|
||||
|
||||
// Terrain data shape from games/age-of-four/data/terrain/*.json
|
||||
// Terrain data shape from games/age-of-dwarves/data/terrain/*.json
|
||||
export interface TerrainData {
|
||||
id: string
|
||||
name: string
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export interface InitCommand {
|
|||
type: 'init'
|
||||
terrainData: Record<string, TerrainData>
|
||||
params: Record<string, number>
|
||||
spec?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface RunCommand {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue