feat(climate-sim): Update biome/terrain rendering, add async worker hook, and refresh sprite generation database

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-29 04:57:43 -07:00
parent 344614c7f0
commit 5a559140a1
7 changed files with 41 additions and 21 deletions

View file

@ -5,7 +5,6 @@ import { SCENARIOS, DEFAULT_SCENARIO_TURNS } from '@magic-civ/engine-ts'
import { getCache, putCache, pruneStaleEntries } from '@/simulation/simCache'
import type { CachedScenario } from '@/simulation/simCache'
import SimulationWorker from '../simulation/simulation.worker.ts?worker'
// ---------------------------------------------------------------------------
// Types
@ -134,7 +133,7 @@ export function useSimulationWorker(): UseSimulationWorkerResult {
// Defined before useEffect so HMR partial re-evaluation keeps references stable.
const speculateSingle = (scenarioId: string, seed: number, payload: NonNullable<typeof initPayloadRef.current>): void => {
const bgWorker = new SimulationWorker()
const bgWorker = new Worker(new URL('../simulation/simulation.worker.ts', import.meta.url), { type: 'module' })
bgWorkersRef.current.push(bgWorker)
bgWorker.onmessage = (e: MessageEvent<WorkerResponse>): void => {
@ -175,7 +174,7 @@ export function useSimulationWorker(): UseSimulationWorkerResult {
.then((cached) => {
if (cached && cached.turns >= DEFAULT_SCENARIO_TURNS) return
const bgWorker = new SimulationWorker()
const bgWorker = new Worker(new URL('../simulation/simulation.worker.ts', import.meta.url), { type: 'module' })
bgWorkersRef.current.push(bgWorker)
bgWorker.onmessage = (e: MessageEvent<WorkerResponse>): void => {
@ -204,7 +203,7 @@ export function useSimulationWorker(): UseSimulationWorkerResult {
// ── Initialize worker ──────────────────────────────────────────────────
useEffect(() => {
const worker = new SimulationWorker()
const worker = new Worker(new URL('../simulation/simulation.worker.ts', import.meta.url), { type: 'module' })
workerRef.current = worker
worker.onmessage = (e: MessageEvent<WorkerResponse>): void => {

View file

@ -128,14 +128,23 @@ function fmtVal(v: number): string {
interface BiomeReferenceProps {
layerMask: number
presentBiomes?: ReadonlySet<string>
}
export function BiomeReference({ layerMask }: BiomeReferenceProps): ReactElement | null {
export function BiomeReference({ layerMask, presentBiomes }: BiomeReferenceProps): ReactElement | null {
const [hoveredBiome, setHoveredBiome] = useState<string | null>(null)
// Only relevant when Terrain (bit 5) or Biomes (bit 0) layer is active
if ((layerMask & ((1 << 5) | (1 << 0))) === 0) return null
const visibleFamilies = BIOME_FAMILIES.flatMap((family) => {
const visibleBiomes = presentBiomes
? family.biomes.filter((b) => presentBiomes.has(b.id))
: family.biomes
if (visibleBiomes.length === 0) return []
return [{ ...family, biomes: visibleBiomes }]
})
return (
<MapOverlayPanel title="Biome Reference">
<Intro>
@ -143,7 +152,7 @@ export function BiomeReference({ layerMask }: BiomeReferenceProps): ReactElement
the decision path and example input values that produce it.
</Intro>
{BIOME_FAMILIES.map((family) => (
{visibleFamilies.map((family) => (
<FamilySection key={family.label}>
<FamilyHeader>
<PanelSectionLabel style={{ margin: 0 }}>{family.label}</PanelSectionLabel>

View file

@ -324,6 +324,11 @@ export function ClimateSimDisplay({ easterEggs }: ClimateSimDisplayProps): React
const timings = scenarioData?.timings ?? []
const currentStats = stats[currentTurn]
const presentBiomes = useMemo(
() => new Set(Object.keys(currentStats?.terrain_counts ?? {})),
[currentStats],
)
const snapshot = useMemo((): GridSnapshot | null => {
if (!currentFrame) return null
return frameAsSnapshot(currentFrame, currentStats)
@ -541,8 +546,8 @@ export function ClimateSimDisplay({ easterEggs }: ClimateSimDisplayProps): React
</OverlayTopRight>
<OverlayTopLeft>
<TerrainLegend layerMask={layerMask} />
<BiomeReference layerMask={layerMask} />
<TerrainLegend layerMask={layerMask} presentBiomes={presentBiomes} />
<BiomeReference layerMask={layerMask} presentBiomes={presentBiomes} />
</OverlayTopLeft>
<OverlayBottomLeft>

View file

@ -84,9 +84,10 @@ const has = (mask: number, b: number): boolean => (mask & (1 << b)) !== 0
interface TerrainLegendProps {
layerMask: number
presentBiomes?: ReadonlySet<string>
}
export function TerrainLegend({ layerMask }: TerrainLegendProps): ReactElement | null {
export function TerrainLegend({ layerMask, presentBiomes }: TerrainLegendProps): ReactElement | null {
const showTerrain = has(layerMask, 5) || has(layerMask, 0)
const showTemp = has(layerMask, 1)
const showMoisture = has(layerMask, 2)
@ -112,18 +113,24 @@ export function TerrainLegend({ layerMask }: TerrainLegendProps): ReactElement |
{showTerrain && (
<Section>
<BiomeList>
{TERRAIN_SECTIONS.map(({ label, biomes }, si) => (
<BiomeGroup key={label}>
{si > 0 && <GroupDivider />}
<GroupLabel>{label}</GroupLabel>
{biomes.map(({ id, label: biomeLabel, rgb }) => (
<SwatchRow key={id}>
<Swatch style={{ background: rgbToCSS(rgb) }} />
<SwatchLabel>{biomeLabel}</SwatchLabel>
</SwatchRow>
))}
</BiomeGroup>
))}
{TERRAIN_SECTIONS.flatMap(({ label, biomes }, si) => {
const visibleBiomes = presentBiomes
? biomes.filter(({ id }) => presentBiomes.has(id))
: biomes
if (visibleBiomes.length === 0) return []
return [(
<BiomeGroup key={label}>
{si > 0 && <GroupDivider />}
<GroupLabel>{label}</GroupLabel>
{visibleBiomes.map(({ id, label: biomeLabel, rgb }) => (
<SwatchRow key={id}>
<Swatch style={{ background: rgbToCSS(rgb) }} />
<SwatchLabel>{biomeLabel}</SwatchLabel>
</SwatchRow>
))}
</BiomeGroup>
)]
})}
</BiomeList>
{showTerrain && (
<GradientRow>

BIN
legend-filtered.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB