feat(engine-core): Update climate stats dashboard and core simulation engine logic, including physics, map generation, and sprite asset management

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-26 01:12:44 -07:00
parent cfe859bfea
commit e1880d53b1
8 changed files with 106 additions and 125 deletions

View file

@ -56,116 +56,67 @@ const PRIMARY_METRICS: MetricDef[] = [
{
key: 'temp', label: 'Temp', tooltip: 'Average land temperature (0=frozen, 1=scorching). Driven by solar input, albedo, and orbital cycles.', color: '#E85D3A',
getValue: (s) => s.avg_temp,
formatValue: (v) => v.toFixed(2),
formatDelta: (d) => (d >= 0 ? '+' : '') + d.toFixed(2),
formatValue: fmt2,
formatDelta: fmtDelta2,
phaseBands: TEMP_PHASE_BANDS,
},
{
key: 'moisture', label: 'Moisture', tooltip: 'Average land moisture (0=bone dry, 1=saturated). Driven by ocean evaporation, wind transport, and precipitation.', color: '#26A69A',
getValue: (s) => s.avg_moisture,
formatValue: (v) => v.toFixed(2),
formatDelta: (d) => (d >= 0 ? '+' : '') + d.toFixed(2),
formatValue: fmt2,
formatDelta: fmtDelta2,
},
m('sea_level'),
]
// Left column: LAND metrics
const COMPACT_LEFT: MetricDef[] = [
{
key: 'land_flora', label: 'Flora', tooltip: 'Average canopy cover across land tiles (0=barren, 1=full canopy). Forests and jungles are high; deserts and tundra near zero.', color: '#66BB6A',
getValue: (s) => s.avg_land_flora,
formatValue: (v) => v.toFixed(3),
formatDelta: (d) => (d >= 0 ? '+' : '') + d.toFixed(3),
},
{
key: 'land_fauna', label: 'Fauna', tooltip: 'Average habitat suitability across land tiles (0=inhospitable, 1=thriving). Combines flora density, moisture, and temperature.', color: '#8D6E63',
getValue: (s) => s.avg_land_fauna,
formatValue: (v) => v.toFixed(3),
formatDelta: (d) => (d >= 0 ? '+' : '') + d.toFixed(3),
},
{
key: 'land_quality', label: 'Quality', tooltip: 'Average tile quality across land (1-5). Rises when climate matches ideal biome, falls when it shifts away. Higher = better yields.', color: '#FFD54F',
getValue: (s) => s.avg_land_quality,
formatValue: (v) => v.toFixed(2),
formatDelta: (d) => (d >= 0 ? '+' : '') + d.toFixed(2),
},
{
key: 'evapotrans', label: 'ET', tooltip: 'Average evapotranspiration across land tiles. Moisture recycled by vegetation per turn. Forests contribute most; deserts are negative.', color: '#80DEEA',
getValue: (s) => s.avg_evapotranspiration,
formatValue: (v) => v.toFixed(4),
formatDelta: (d) => (d >= 0 ? '+' : '') + d.toFixed(4),
},
// ── Metric catalog (single source of truth) ─────────────────────────────────
// Each metric defined ONCE. Dashboard layouts select by key.
const fmt3 = (v: number) => v.toFixed(3)
const fmt2 = (v: number) => v.toFixed(2)
const fmt4 = (v: number) => v.toFixed(4)
const fmtDelta3 = (d: number) => (d >= 0 ? '+' : '') + d.toFixed(3)
const fmtDelta2 = (d: number) => (d >= 0 ? '+' : '') + d.toFixed(2)
const fmtDelta4 = (d: number) => (d >= 0 ? '+' : '') + d.toFixed(4)
const METRIC_CATALOG: Record<string, MetricDef> = {
// -- Climate / atmosphere --
sea_level: { key: 'sea_level', label: 'Sea Lvl', tooltip: 'Elevation threshold for water. Rises with warming, falls with cooling.', color: '#5C6BC0', getValue: (s) => s.sea_level, formatValue: fmt3, formatDelta: fmtDelta3 },
et: { key: 'evapotrans', label: 'ET', tooltip: 'Average evapotranspiration across land. Moisture recycled by vegetation per turn.', color: '#80DEEA', getValue: (s) => s.avg_evapotranspiration, formatValue: fmt4, formatDelta: fmtDelta4 },
albedo: { key: 'albedo', label: 'Albedo', tooltip: 'Global average surface reflectivity (0=absorbs all, 1=reflects all). Ice/snow raise it, forests/water lower it.', color: '#BDBDBD', getValue: (s) => s.avg_albedo, formatValue: fmt3, formatDelta: fmtDelta3 },
aerosol: { key: 'aerosol', label: 'Aerosol', tooltip: 'Global average sulfate aerosol opacity. Volcanic eruptions inject aerosols that cool and dry the atmosphere.', color: '#90A4AE', getValue: (s) => s.avg_aerosol, formatValue: fmt4, formatDelta: fmtDelta4 },
// -- Land ecology --
land_canopy: { key: 'land_canopy', label: 'Canopy', tooltip: 'Average tree canopy cover across land (0=barren, 1=full canopy). Drives succession and shading.', color: '#2E7D32', getValue: (s) => s.avg_land_canopy, formatValue: fmt3, formatDelta: fmtDelta3 },
land_under: { key: 'land_undergrowth', label: 'Undergrowth', tooltip: 'Average ground vegetation across land (0=bare, 1=dense). Drives food yield and habitat quality.', color: '#66BB6A', getValue: (s) => s.avg_land_undergrowth, formatValue: fmt3, formatDelta: fmtDelta3 },
land_fungi: { key: 'land_fungi', label: 'Fungi', tooltip: 'Average mycorrhizal network across land (0=none, 1=dense). Accelerates forest regrowth, boosts ecosystem resilience.', color: '#8D6E63', getValue: (s) => s.avg_land_fungi, formatValue: fmt3, formatDelta: fmtDelta3 },
land_habitat: { key: 'land_habitat', label: 'Habitat', tooltip: 'Average habitat suitability across land (0=inhospitable, 1=thriving). Combines flora density, moisture, temperature.', color: '#A1887F', getValue: (s) => s.avg_land_habitat, formatValue: fmt3, formatDelta: fmtDelta3 },
land_quality: { key: 'land_quality', label: 'Quality', tooltip: 'Average tile quality across land (1=Prolific, 5=Epic). Ecology composite of flora health, fauna diversity, biome stability.', color: '#FFD54F', getValue: (s) => s.avg_land_quality, formatValue: fmt2, formatDelta: fmtDelta2 },
// -- Water ecology --
water_reef: { key: 'water_reef', label: 'Reef', tooltip: 'Average reef health across water tiles (0=dead, 1=pristine). Bleaching from high temps (>0.75). Dead reefs halve fish capacity.', color: '#29B6F6', getValue: (s) => s.avg_water_reef, formatValue: fmt3, formatDelta: fmtDelta3 },
water_fish: { key: 'water_fish', label: 'Fish', tooltip: 'Average fish stock across water tiles (0=empty, 100+=abundant). Logistic reproduction, temperature-scaled.', color: '#26C6DA', getValue: (s) => s.avg_water_fish, formatValue: fmt2, formatDelta: fmtDelta2 },
water_quality:{ key: 'water_quality', label: 'Quality', tooltip: 'Average tile quality across water tiles (1=Prolific, 5=Epic). Reflects marine ecosystem health.', color: '#42A5F5', getValue: (s) => s.avg_water_quality, formatValue: fmt2, formatDelta: fmtDelta2 },
}
const m = (key: string): MetricDef => METRIC_CATALOG[key]
// Life mode: LAND column (fauna quality first, then flora quality, then details)
const LIFE_LEFT: MetricDef[] = [
m('land_habitat'), m('land_quality'), m('land_canopy'), m('land_under'), m('land_fungi'),
]
// Right column: WATER + atmosphere metrics
const COMPACT_RIGHT: MetricDef[] = [
{
key: 'marine_flora', label: 'Flora', tooltip: 'Average reef health across coastal tiles (1=healthy coral, 0=dead). Bleaching from high temps (>0.75) destroys reefs. Dead reefs reduce evaporation.', color: '#29B6F6',
getValue: (s) => s.avg_marine_flora,
formatValue: (v) => v.toFixed(3),
formatDelta: (d) => (d >= 0 ? '+' : '') + d.toFixed(3),
},
{
key: 'marine_fauna', label: 'Fauna', tooltip: 'Average fish stock across coastal tiles (0=depleted, 1=abundant). Depends on reef health and water temperature.', color: '#26C6DA',
getValue: (s) => s.avg_marine_fauna,
formatValue: (v) => v.toFixed(3),
formatDelta: (d) => (d >= 0 ? '+' : '') + d.toFixed(3),
},
{
key: 'water_quality', label: 'Quality', tooltip: 'Average tile quality across water tiles (1-5). Reflects ocean and coastal ecosystem health.', color: '#42A5F5',
getValue: (s) => s.avg_water_quality,
formatValue: (v) => v.toFixed(2),
formatDelta: (d) => (d >= 0 ? '+' : '') + d.toFixed(2),
},
{
key: 'sea_level', label: 'Sea Lvl', tooltip: 'Elevation threshold for water. Rises with warming (thermal expansion + ice melt), falls with cooling. Land below this floods.', color: '#5C6BC0',
getValue: (s) => s.sea_level,
formatValue: (v) => v.toFixed(3),
formatDelta: (d) => (d >= 0 ? '+' : '') + d.toFixed(3),
},
// Life mode: WATER column (fauna quality first, then flora quality, then details)
const LIFE_RIGHT: MetricDef[] = [
m('water_fish'), m('water_quality'), m('water_reef'),
]
// Environment mode right column
const ENV_RIGHT_METRICS: MetricDef[] = [
{
key: 'sea_level', label: 'Sea Lvl', tooltip: 'Elevation threshold for water. Rises with warming (thermal expansion + ice melt), falls with cooling. Land below this floods.', color: '#5C6BC0',
getValue: (s) => s.sea_level,
formatValue: (v) => v.toFixed(3),
formatDelta: (d) => (d >= 0 ? '+' : '') + d.toFixed(3),
},
{
key: 'evapotrans', label: 'ET', tooltip: 'Average evapotranspiration across land tiles. Moisture recycled by vegetation per turn.', color: '#80DEEA',
getValue: (s) => s.avg_evapotranspiration,
formatValue: (v) => v.toFixed(4),
formatDelta: (d) => (d >= 0 ? '+' : '') + d.toFixed(4),
},
{
key: 'land_quality', label: 'Land Qlty', tooltip: 'Average tile quality across land (1-5). Rises when climate matches ideal biome.', color: '#FFD54F',
getValue: (s) => s.avg_land_quality,
formatValue: (v) => v.toFixed(2),
formatDelta: (d) => (d >= 0 ? '+' : '') + d.toFixed(2),
},
// Environment mode: left column (energy budget)
const ENV_LEFT: MetricDef[] = [
m('albedo'), m('et'), m('aerosol'),
]
// Environment mode left column: atmosphere
const ATMOSPHERE_METRICS: MetricDef[] = [
{
key: 'solar', label: 'Solar', tooltip: 'Average absorbed solar energy after albedo reflection. The net heat input driving temperature.', color: '#FFA726',
getValue: (s) => s.avg_solar,
formatValue: (v) => v.toFixed(3),
formatDelta: (d) => (d >= 0 ? '+' : '') + d.toFixed(3),
},
{
key: 'albedo', label: 'Albedo', tooltip: 'Global average surface reflectivity (0=absorbs all, 1=reflects all). Ice/snow raise it, forests/water lower it.', color: '#BDBDBD',
getValue: (s) => s.avg_albedo,
formatValue: (v) => v.toFixed(3),
formatDelta: (d) => (d >= 0 ? '+' : '') + d.toFixed(3),
},
{
key: 'aerosol', label: 'Aerosol', tooltip: 'Sulfate aerosol opacity from volcanic/impact events. Blocks sunlight, causing cooling and drying. Decays over ~20 turns.', color: '#AB47BC',
getValue: (s) => s.avg_aerosol,
formatValue: (v) => v.toFixed(4),
formatDelta: (d) => (d >= 0 ? '+' : '') + d.toFixed(4),
},
// Environment mode: right column (land/water state)
const ENV_RIGHT: MetricDef[] = [
m('land_quality'), m('water_quality'),
]
// ── terrain groups ─────────────────────────────────────────────────────────
@ -178,15 +129,17 @@ interface TerrainGroup {
}
const TERRAIN_GROUPS: TerrainGroup[] = [
{ label: 'Water', abbr: 'Wtr', ids: ['ocean', 'coast', 'lake', 'inland_sea'], color: 'rgb(61,120,209)' },
{ label: 'Frozen', abbr: 'Frz', ids: ['ice', 'snow'], color: 'rgb(224,240,255)' },
{ label: 'Ocean', abbr: 'Ocn', ids: ['ocean', 'coast', 'deep_ocean', 'shallow_ocean', 'coral_reef', 'estuary', 'mangrove'], color: 'rgb(61,120,209)' },
{ label: 'Fresh', abbr: 'Frs', ids: ['lake', 'pond', 'river', 'inland_sea'], color: 'rgb(100,160,230)' },
{ label: 'Ice', abbr: 'Ice', ids: ['permanent_ice', 'ice', 'snow', 'alpine_tundra', 'polar_desert'], color: 'rgb(224,240,255)' },
{ label: 'Tundra', abbr: 'Tnd', ids: ['tundra'], color: 'rgb(184,194,166)' },
{ label: 'Arid', abbr: 'Ard', ids: ['desert'], color: 'rgb(222,199,128)' },
{ label: 'Grassland', abbr: 'Grs', ids: ['plains', 'grassland'], color: 'rgb(141,197,112)' },
{ label: 'Forest', abbr: 'For', ids: ['forest', 'boreal_forest', 'jungle', 'enchanted_forest'], color: 'rgb(51,140,64)' },
{ label: 'Arid', abbr: 'Ard', ids: ['desert', 'chaparral'], color: 'rgb(222,199,128)' },
{ label: 'Grass', abbr: 'Grs', ids: ['plains', 'grassland', 'temperate_grassland', 'savanna', 'alpine_meadow'], color: 'rgb(141,197,112)' },
{ label: 'Forest', abbr: 'For', ids: ['forest', 'temperate_forest', 'boreal_forest', 'tropical_rainforest', 'tropical_dry_forest', 'montane_forest', 'cloud_forest', 'jungle', 'enchanted_forest'], color: 'rgb(51,140,64)' },
{ label: 'Rough', abbr: 'Rgh', ids: ['hills', 'mountains'], color: 'rgb(158,153,148)' },
{ label: 'Wetland', abbr: 'Wet', ids: ['swamp'], color: 'rgb(61,79,36)' },
{ label: 'Wetland', abbr: 'Wet', ids: ['swamp', 'bog'], color: 'rgb(61,79,36)' },
{ label: 'Volcanic', abbr: 'Vol', ids: ['volcano'], color: 'rgb(191,51,20)' },
{ label: 'Cave', abbr: 'Cav', ids: ['subterranean'], color: 'rgb(90,70,60)' },
]
// ── props ──────────────────────────────────────────────────────────────────
@ -236,7 +189,7 @@ export function StatsDashboard({ stats, currentTurn, onScrub, category = 'enviro
<CompactColumns>
<CompactCol>
<ColHeader>Land</ColHeader>
{COMPACT_LEFT.map((def) => {
{LIFE_LEFT.map((def) => {
const val = currentStats ? def.getValue(currentStats) : 0
return (
<CompactCell key={def.key}>
@ -249,7 +202,7 @@ export function StatsDashboard({ stats, currentTurn, onScrub, category = 'enviro
</CompactCol>
<CompactCol>
<ColHeader>Water</ColHeader>
{COMPACT_RIGHT.map((def) => {
{LIFE_RIGHT.map((def) => {
const val = currentStats ? def.getValue(currentStats) : 0
return (
<CompactCell key={def.key}>
@ -265,7 +218,7 @@ export function StatsDashboard({ stats, currentTurn, onScrub, category = 'enviro
/* Environment mode: atmosphere + sea level metrics */
<CompactColumns>
<CompactCol>
{ATMOSPHERE_METRICS.map((def) => {
{ENV_LEFT.map((def) => {
const val = currentStats ? def.getValue(currentStats) : 0
return (
<CompactCell key={def.key}>
@ -277,7 +230,7 @@ export function StatsDashboard({ stats, currentTurn, onScrub, category = 'enviro
})}
</CompactCol>
<CompactCol>
{ENV_RIGHT_METRICS.map((def) => {
{ENV_RIGHT.map((def) => {
const val = currentStats ? def.getValue(currentStats) : 0
return (
<CompactCell key={def.key}>
@ -406,7 +359,7 @@ function TerrainDistChart({ stats, currentTurn, onScrub }: StatsDashboardProps):
// Match total height of primary + compact sections
// Height matches the tallest possible content (life mode with headers)
const maxCompactRows = Math.max(COMPACT_LEFT.length, COMPACT_RIGHT.length) + 1
const maxCompactRows = Math.max(LIFE_LEFT.length, LIFE_RIGHT.length) + 1
const chartH = PRIMARY_METRICS.length * (PRIMARY_H + 3)
+ 6
+ maxCompactRows * (COMPACT_H + 3)

View file

@ -390,14 +390,19 @@ function updateHabitatSuitability(
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
if (!isWater(tile)) 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
let stock = tile.fish_stock ?? 0
// Seed fish on water tiles that have none yet (spontaneous colonization)
if (stock <= 0) {
stock = Math.round(cap * 0.1 * tempMult) // start at 10% of capacity
if (stock <= 0) stock = 1
}
const growth = 0.05 * tempMult * stock * (1.0 - stock / cap)
tile.fish_stock = Math.max(0, Math.min(Math.round(stock + growth), cap))
}

View file

@ -264,6 +264,21 @@ class GenMap {
wonder_anchor_schools: [],
wonder_tier: 0,
river_source_type: gt?.river_source_type || undefined,
// Ecology fields (initialized to zero, populated by EcologyPhysics)
canopy_cover: 0.0,
undergrowth: 0.0,
fungi_network: 0.0,
drought_counter: 0,
succession_progress: 0,
regrowth_stage: -1,
regrowth_turns: 0,
habitat_suitability: 0.0,
habitat_low_turns: 0,
landmark_name: '',
substrate_id: '',
water_body_id: -1,
depth_from_coast: -1,
fish_stock: 0,
}
}
}

View file

@ -144,11 +144,13 @@ export function computeTurnStats(
let moistSum = 0
let albedoSum = 0
let solarSum = 0
let landFloraSum = 0
let landFaunaSum = 0
let landCanopySum = 0
let landUndergrowthSum = 0
let landFungiSum = 0
let landHabitatSum = 0
let landQualitySum = 0
let marineFloraSum = 0
let marineFaunaSum = 0
let waterReefSum = 0
let waterFishSum = 0
let waterQualitySum = 0
let waterCount = 0
let aerosolSum = 0
@ -175,14 +177,16 @@ export function computeTurnStats(
if (isWater) {
waterCount++
waterQualitySum += tile.quality ?? 1
marineFloraSum += tile.reef_health ?? 0.0
marineFaunaSum += tile.fish_stock ?? 0
waterReefSum += tile.reef_health ?? 0.0
waterFishSum += tile.fish_stock ?? 0
} else {
landCount++
tempSum += tile.temperature
moistSum += tile.moisture
landFloraSum += tile.canopy_cover ?? 0
landFaunaSum += tile.habitat_suitability ?? 0
landCanopySum += tile.canopy_cover ?? 0
landUndergrowthSum += tile.undergrowth ?? 0
landFungiSum += tile.fungi_network ?? 0
landHabitatSum += tile.habitat_suitability ?? 0
landQualitySum += tile.quality ?? 1
const et = (td as Record<string, number> | undefined)?.['evapotranspiration'] ?? 0
etSum += et
@ -202,10 +206,12 @@ export function computeTurnStats(
sea_level: grid.sea_level,
avg_albedo: albedoSum / n,
avg_solar: solarSum / n,
avg_land_flora: landCount > 0 ? landFloraSum / landCount : 0,
avg_land_fauna: landCount > 0 ? landFaunaSum / landCount : 0,
avg_marine_flora: waterCount > 0 ? marineFloraSum / waterCount : 1.0,
avg_marine_fauna: waterCount > 0 ? marineFaunaSum / waterCount : 0,
avg_land_canopy: landCount > 0 ? landCanopySum / landCount : 0,
avg_land_undergrowth: landCount > 0 ? landUndergrowthSum / landCount : 0,
avg_land_fungi: landCount > 0 ? landFungiSum / landCount : 0,
avg_land_habitat: landCount > 0 ? landHabitatSum / landCount : 0,
avg_water_reef: waterCount > 0 ? waterReefSum / waterCount : 0,
avg_water_fish: waterCount > 0 ? waterFishSum / waterCount : 0,
avg_land_quality: landCount > 0 ? landQualitySum / landCount : 1,
avg_water_quality: waterCount > 0 ? waterQualitySum / waterCount : 1,
avg_aerosol: aerosolSum / n,

View file

@ -67,10 +67,12 @@ export interface TurnStats {
sea_level: number // current sea level elevation
avg_albedo: number // global average albedo (0=absorbs all, 1=reflects all)
avg_solar: number // global average solar input after albedo
avg_land_flora: number // average canopy_cover across land tiles
avg_land_fauna: number // average habitat_suitability across land tiles
avg_marine_flora: number // average reef_health across coast tiles
avg_marine_fauna: number // average fish_stock across coast tiles
avg_land_canopy: number // average canopy_cover across land tiles
avg_land_undergrowth: number // average undergrowth across land tiles
avg_land_fungi: number // average fungi_network across land tiles
avg_land_habitat: number // average habitat_suitability across land tiles
avg_water_reef: number // average reef_health across water tiles
avg_water_fish: number // average fish_stock across water tiles
avg_land_quality: number // average quality across land tiles (1-5)
avg_water_quality: number // average quality across water tiles (1-5)
avg_aerosol: number // global average sulfate aerosol opacity

Binary file not shown.