perf(engine-ts): ⚡ Regenerate TypeScript bindings for climate/ecology physics and hex grid logic to optimize simulation performance
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
f65a169f8c
commit
82c074f658
6 changed files with 600 additions and 9 deletions
|
|
@ -27,6 +27,8 @@ const CLIMATE_DEFAULTS: Record<string, number> = {
|
|||
lake_thermal_conductivity: 0.05,
|
||||
river_moisture_transport: 0.075,
|
||||
mountain_rain_shadow_block: 0.9,
|
||||
solar_min: 0.15,
|
||||
solar_max: 0.70,
|
||||
}
|
||||
|
||||
const DEW_DEFAULTS: Record<string, number> = {
|
||||
|
|
@ -283,7 +285,7 @@ export class ClimatePhysics {
|
|||
for (let i = 0; i < tiles.length; i++) {
|
||||
const tile = tiles[i]
|
||||
const { col, row } = tile
|
||||
let solar = solarByRow(row, h)
|
||||
let solar = solarByRow(row, h, this.p('solar_min', 0.15), this.p('solar_max', 0.70))
|
||||
let current_temp = oldTemp[i]
|
||||
|
||||
let terrain_data = (this.terrainCache.get(tile.biome_id) ?? {})
|
||||
|
|
|
|||
588
packages/engine-ts/src/EcologyPhysics.generated.ts
Normal file
588
packages/engine-ts/src/EcologyPhysics.generated.ts
Normal file
|
|
@ -0,0 +1,588 @@
|
|||
// 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 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<string, BiomeDef> = {
|
||||
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
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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 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 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)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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<number, number> = {
|
||||
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 — 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 }
|
||||
|
|
@ -83,10 +83,11 @@ export function neighborInDir(
|
|||
return { col: nc, row: nr }
|
||||
}
|
||||
|
||||
/** Solar insolation at a row: 1.0 at equator (center), 0.0 at poles. */
|
||||
export function solarByRow(row: number, height: number): number {
|
||||
/** Solar insolation at a row, mapped to habitable range [solarMin, solarMax]. */
|
||||
export function solarByRow(row: number, height: number, solarMin = 0.15, solarMax = 0.70): number {
|
||||
const centerRow = height / 2.0
|
||||
return 1.0 - Math.abs((row - centerRow) / centerRow)
|
||||
const raw = 1.0 - Math.abs((row - centerRow) / centerRow)
|
||||
return solarMin + (solarMax - solarMin) * raw
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ const DEFAULT_TYPE_DATA: Record<string, unknown> = {
|
|||
continent_count: { min: 2, max: 4 },
|
||||
terrain_fractions: {
|
||||
enchanted_forest: 0.01, desert: 0.10, swamp: 0.07,
|
||||
corrupted_land: 0.04, volcano: 0.02,
|
||||
volcano: 0.02,
|
||||
},
|
||||
generation_params: {
|
||||
num_landmass: 35, steepness: 0.20, prevailing_wind_direction: 0,
|
||||
|
|
@ -251,7 +251,6 @@ class GenMap {
|
|||
quality_progress: gt?.quality_progress ?? 0,
|
||||
river_edges: gt?.river_edges ?? [],
|
||||
flow_accumulation: gt?.flow_accumulation ?? 0.0,
|
||||
corruption_pressure: 0.0,
|
||||
original_terrain_id: '',
|
||||
ley_line_count: 0,
|
||||
ley_school: '',
|
||||
|
|
@ -779,7 +778,7 @@ function assignTerrainPatches(
|
|||
const defaultFractions: Record<string, number> = {
|
||||
forest: 0.12, jungle: 0.06, boreal_forest: 0.05,
|
||||
enchanted_forest: 0.01, desert: 0.10, swamp: 0.07,
|
||||
corrupted_land: 0.04, volcano: 0.02,
|
||||
volcano: 0.02,
|
||||
}
|
||||
const fractions = (typeData['terrain_fractions'] ?? defaultFractions) as Record<string, number>
|
||||
|
||||
|
|
@ -790,7 +789,7 @@ function assignTerrainPatches(
|
|||
|
||||
const order: string[] = [
|
||||
'volcano', 'jungle', 'forest', 'boreal_forest', 'enchanted_forest',
|
||||
'desert', 'swamp', 'corrupted_land', 'tundra', 'snow', 'grassland',
|
||||
'desert', 'swamp', 'tundra', 'snow', 'grassland',
|
||||
]
|
||||
|
||||
for (const terrainId of order) {
|
||||
|
|
@ -904,7 +903,6 @@ function isEligible(
|
|||
case 'enchanted_forest': return m >= 0.40
|
||||
case 'desert': return t >= 0.25 && m < 0.25
|
||||
case 'swamp': return t > 0.25 && m > 0.75 && e < 0.48
|
||||
case 'corrupted_land': return true
|
||||
case 'tundra': return t >= 0.10 && t < 0.25
|
||||
case 'snow': return t < 0.10
|
||||
case 'grassland': return true
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ export * from './types'
|
|||
export * from './HexGrid'
|
||||
export * from './ClimatePhysics.generated'
|
||||
export * from './MapGenerator.generated'
|
||||
export * from './EcologyPhysics.generated'
|
||||
export * from './runner'
|
||||
export * from './scenarios'
|
||||
export * from './worker-protocol'
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ export interface FramesResponse {
|
|||
export interface FramePayload {
|
||||
texA: Float32Array
|
||||
texB: Float32Array
|
||||
texC: Float32Array
|
||||
width: number
|
||||
height: number
|
||||
turn: number
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue