diff --git a/guide/engine/src/components/climate-sim/StatsDashboard.tsx b/guide/engine/src/components/climate-sim/StatsDashboard.tsx index 87c9797a..17f54744 100644 --- a/guide/engine/src/components/climate-sim/StatsDashboard.tsx +++ b/guide/engine/src/components/climate-sim/StatsDashboard.tsx @@ -52,6 +52,39 @@ interface MetricDef { phaseBands?: PhaseBand[] } +// ── Formatters ─────────────────────────────────────────────────────────────── + +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) + +// ── Metric catalog (single source of truth) ───────────────────────────────── + +const METRIC_CATALOG: Record = { + // -- 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: 'Flora', 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: 'Fauna', tooltip: 'Average fauna 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: 'Flora', 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: 'Fauna', 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] + +// ── Layout selections ──────────────────────────────────────────────────────── + 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', @@ -69,44 +102,14 @@ const PRIMARY_METRICS: MetricDef[] = [ m('sea_level'), ] -// ── 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 = { - // -- 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) +// Life mode: LAND column — Fauna, Flora, Quality, then details const LIFE_LEFT: MetricDef[] = [ - m('land_habitat'), m('land_quality'), m('land_canopy'), m('land_under'), m('land_fungi'), + m('land_habitat'), m('land_canopy'), m('land_quality'), m('land_under'), m('land_fungi'), ] -// Life mode: WATER column (fauna quality first, then flora quality, then details) +// Life mode: WATER column — Fauna, Flora, Quality const LIFE_RIGHT: MetricDef[] = [ - m('water_fish'), m('water_quality'), m('water_reef'), + m('water_fish'), m('water_reef'), m('water_quality'), ] // Environment mode: left column (energy budget) @@ -359,7 +362,9 @@ 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(LIFE_LEFT.length, LIFE_RIGHT.length) + 1 + const lifeRows = Math.max(LIFE_LEFT.length, LIFE_RIGHT.length) + 1 // +1 for header + const envRows = Math.max(ENV_LEFT.length, ENV_RIGHT.length) + const maxCompactRows = Math.max(lifeRows, envRows) const chartH = PRIMARY_METRICS.length * (PRIMARY_H + 3) + 6 + maxCompactRows * (COMPACT_H + 3) diff --git a/m2_life_dashboard_working.png b/m2_life_dashboard_working.png new file mode 100644 index 00000000..ebab6412 Binary files /dev/null and b/m2_life_dashboard_working.png differ diff --git a/tools/sprite-generation/server.py b/tools/sprite-generation/server.py index 7e43d19f..f6196fde 100644 --- a/tools/sprite-generation/server.py +++ b/tools/sprite-generation/server.py @@ -189,22 +189,31 @@ def create_app( @app.get("/api/stream/variants") async def stream_variants() -> StreamingResponse: async def event_generator(): - # Start from the highest known variant ID current = registry.get_recent_variants(limit=1) last_id = current[0]["variant_id"] if current else 0 + last_rating_snapshot = "" + poll_count = 0 while True: await asyncio.sleep(3) new_variants = registry.get_recent_variants(limit=10, since_id=last_id) if new_variants: last_id = max(v["variant_id"] for v in new_variants) yield f"data: {json.dumps(new_variants)}\n\n" - else: - # Also check for rating updates on recent variants + continue + + # Check for rating updates every ~15s (5 cycles) + poll_count += 1 + if poll_count % 5 == 0: updated = registry.get_recent_variants(limit=30) - if updated: + snapshot = "|".join( + f"{v['variant_id']}:{v['rating']}" for v in updated + ) + if snapshot != last_rating_snapshot: + last_rating_snapshot = snapshot yield f"data: {json.dumps(updated)}\n\n" - else: - yield ": keepalive\n\n" + continue + + yield ": keepalive\n\n" return StreamingResponse( event_generator(), diff --git a/tools/sprite-generation/sprites.db b/tools/sprite-generation/sprites.db index 3bf8cc19..2914153a 100644 Binary files a/tools/sprite-generation/sprites.db and b/tools/sprite-generation/sprites.db differ diff --git a/tools/sprite-generation/sprites.db-shm b/tools/sprite-generation/sprites.db-shm deleted file mode 100644 index e22f21ef..00000000 Binary files a/tools/sprite-generation/sprites.db-shm and /dev/null differ diff --git a/tools/sprite-generation/sprites.db-wal b/tools/sprite-generation/sprites.db-wal deleted file mode 100644 index 15ccc841..00000000 Binary files a/tools/sprite-generation/sprites.db-wal and /dev/null differ