From e1880d53b14a7efadd12656fee2591acefbdd386 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Thu, 26 Mar 2026 01:12:44 -0700 Subject: [PATCH] =?UTF-8?q?feat(engine-core):=20=E2=9C=A8=20Update=20clima?= =?UTF-8?q?te=20stats=20dashboard=20and=20core=20simulation=20engine=20log?= =?UTF-8?q?ic,=20including=20physics,=20map=20generation,=20and=20sprite?= =?UTF-8?q?=20asset=20management?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../components/climate-sim/StatsDashboard.tsx | 167 +++++++----------- .../engine-ts/src/EcologyPhysics.generated.ts | 9 +- .../engine-ts/src/MapGenerator.generated.ts | 15 ++ packages/engine-ts/src/runner.ts | 30 ++-- packages/engine-ts/src/types.ts | 10 +- tools/sprite-generation/sprites.db | Bin 81920 -> 90112 bytes tools/sprite-generation/sprites.db-shm | Bin 32768 -> 32768 bytes tools/sprite-generation/sprites.db-wal | Bin 45352 -> 12392 bytes 8 files changed, 106 insertions(+), 125 deletions(-) diff --git a/guide/engine/src/components/climate-sim/StatsDashboard.tsx b/guide/engine/src/components/climate-sim/StatsDashboard.tsx index 2419508b..87c9797a 100644 --- a/guide/engine/src/components/climate-sim/StatsDashboard.tsx +++ b/guide/engine/src/components/climate-sim/StatsDashboard.tsx @@ -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 = { + // -- 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 Land - {COMPACT_LEFT.map((def) => { + {LIFE_LEFT.map((def) => { const val = currentStats ? def.getValue(currentStats) : 0 return ( @@ -249,7 +202,7 @@ export function StatsDashboard({ stats, currentTurn, onScrub, category = 'enviro Water - {COMPACT_RIGHT.map((def) => { + {LIFE_RIGHT.map((def) => { const val = currentStats ? def.getValue(currentStats) : 0 return ( @@ -265,7 +218,7 @@ export function StatsDashboard({ stats, currentTurn, onScrub, category = 'enviro /* Environment mode: atmosphere + sea level metrics */ - {ATMOSPHERE_METRICS.map((def) => { + {ENV_LEFT.map((def) => { const val = currentStats ? def.getValue(currentStats) : 0 return ( @@ -277,7 +230,7 @@ export function StatsDashboard({ stats, currentTurn, onScrub, category = 'enviro })} - {ENV_RIGHT_METRICS.map((def) => { + {ENV_RIGHT.map((def) => { const val = currentStats ? def.getValue(currentStats) : 0 return ( @@ -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) diff --git a/packages/engine-ts/src/EcologyPhysics.generated.ts b/packages/engine-ts/src/EcologyPhysics.generated.ts index a5057ad7..8d70e245 100644 --- a/packages/engine-ts/src/EcologyPhysics.generated.ts +++ b/packages/engine-ts/src/EcologyPhysics.generated.ts @@ -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)) } diff --git a/packages/engine-ts/src/MapGenerator.generated.ts b/packages/engine-ts/src/MapGenerator.generated.ts index 9c5bda93..79dd9f6a 100644 --- a/packages/engine-ts/src/MapGenerator.generated.ts +++ b/packages/engine-ts/src/MapGenerator.generated.ts @@ -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, } } } diff --git a/packages/engine-ts/src/runner.ts b/packages/engine-ts/src/runner.ts index d75495d9..0d031146 100644 --- a/packages/engine-ts/src/runner.ts +++ b/packages/engine-ts/src/runner.ts @@ -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 | 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, diff --git a/packages/engine-ts/src/types.ts b/packages/engine-ts/src/types.ts index 5e1f75b5..071719f3 100644 --- a/packages/engine-ts/src/types.ts +++ b/packages/engine-ts/src/types.ts @@ -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 diff --git a/tools/sprite-generation/sprites.db b/tools/sprite-generation/sprites.db index c1d9a2d6e04e8c2e877f62f40842bfb071579dcc..3bf8cc198c8d00878a98e0fd55d4948b70e4ac58 100644 GIT binary patch delta 2715 zcmbW2Z)h8J7{`CV+$Fhdn_iPN({9xsg^IYXz1+Y2p=_&jl-h+M2otxM-7eQGU7EyP zG8BnxgG}hFW(OAgY7x=R4J7M}zN`*mx;Oe};LG9*Mc?QvK_=qwcWFILe;2WVz~%1A zeV*t0e4ppHyzE{6!n=0FO9225HTjc}YF}!d3viDC#oY(o&)j|P;bw5?WA0+uLmh=V zqi9sLYQ9v=RBA=jGeA+#z-wA1uNA9i2)$z%egE}|Kxzt_UYLxD$M|u*v{Wvc zdE5*8rV>Xt&D!Nf6qwO;y;jlmw>#nJLAg@O*3cVJE23W@KAS8=opUy9eQof1#?odOq{&GVs#!r(k;TZa_NW6rAOKh1ddYao$w zz`776=#_(9w?-F{1k-*;f;7XfoUu}i?Th=#B=MuJVE5XQ^nYQl>T^oQt)I1~uHJcr zP%^3F1A&#g_5D>uj(8n1Zu*?X7Tza`EtAlaTg|Z^H6!#|0ijRO4mxf7?aRXFB(D*@ zE*;40$ZO>##Ew%=qp`>Etlc58ZA|x^scvPf*cdK8myyX{F7h-0^e1QqeME;EPq9G| zwVj>@9r0mq*!uQLjnn`P?M_6*O(D8Hil4Y-f?W4fo7gJ{?DTYi8#D{nXTmUIJ-U30yp4|fRf;>Ei9pHi8^FmwS zAbA1rpYCK_mzC_E3$o&T7FqG+pkqb*Y~Hov&FltA3|w6|-jg0_&zg9$7Zc><$>7bMZJRxKo88~{zW2?WxA>7?T;!K} zc@sjYtKvBVrs~C&F^BfW;Vp!J;jegW!{vO3RXfM^!NPPQH=Um^y4@yjz&fku)xz|A z?pi)mxREb*12+W4>xJCBn#ty_s`;WuiU*jP8w7--9dnK)`)grW*l!!QW-V*&U(CPF z0)M9M3RgBggKZEhl^?Fsq}z#x5RdvxDRka~5Pctq7j!ED9#<*oGMIz0zX>SQ`Ve&Y zDFG7l4-gV2q?C0!k&N0jiy4&CWQayOp_}fE!&ACF0UmcLjhTWqnL%d_#Xu64C5cGN zAn^@K$~Xyzi4^uL-k>N3{K2Muy2G$%37htFQ?N|4DP}+HFcd6jQ$a4kEy~B(O)lA; zNYn#l^oaCP3f|DwG-GjrF&00ghoj%qutw_{#-?mWoj#pF3|`VmoUwqlRhL5gbav0d zeOe!5Y{J4O$2^k_&yM4;O1nlF%e1$qnrvi=jzCrXZw$oe4ap=Zi_I~_%!Z{N>rOX>lkMbc3_vjrvxaiwaz^+oN%@D9BqTfd075#aV6~{Ou4|^QC=agpbIn7wY zWH7yH@+hp+%Mr!`U@*PuDt*Lu&$Q$yDej+!bvz-K7MYyUh_bT3~-kv1ZKe(=~ z`)9nV{aeR28`lE`bs?wTeE4az*W5U6>}<+v^Gy2zdTNIcLWHAB$H2_XL{Gl%5q$xv_GB1lxqmN!)As-X diff --git a/tools/sprite-generation/sprites.db-shm b/tools/sprite-generation/sprites.db-shm index 2f60f931c8cb26b2f65010a8dc52b996e0b1c292..e22f21ef5ac9c351db57da53be796cb3dc280435 100644 GIT binary patch delta 169 zcmZo@U}|V!s+V}A%K!pQK+MR%AixZy#eg`At2U`>BBz{_22KVp2JVd$?eBIJvmAn~qYV28m#lq$>DA!P!MWf#@f& zqb}m&ClH+79348jcqK};b#T++e-Ls!JbAeLZTZ`+>G}p1$SpSNzNQ z<^5W@BTCt?^~_!Ce6e$MQBWR zv}lvXGx?bp%pAV@!#7V?1yPg~n<&@Hns{MB00Izz00bZa0SG_<0uX=z1R&58n3Zy* zE`F(HnR3RhD$}dnT4Ii;X`ZI5+%O!QRT%RalWQ5#TPIYeeZ9=}G_LA~ZgDN`9h}8g z)74Fzf6S8QI!&k1foYn@4OQcoWBoXd8J@{i%dt(zO{bNL*&%VIZ@IwgaelL$J3B~o r0V~Y~tlq`3g8&2|009U<00Izz00bZa0SG{#lEwn`H?hF~&IR59JpzEz literal 45352 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 X1vt*_*`SF9W{U_Q0*Jt80D=DlLsWnf