diff --git a/games/age-of-dwarves/guide/src/simulation/simulation.worker.ts b/games/age-of-dwarves/guide/src/simulation/simulation.worker.ts index d5df167f..77933e4d 100644 --- a/games/age-of-dwarves/guide/src/simulation/simulation.worker.ts +++ b/games/age-of-dwarves/guide/src/simulation/simulation.worker.ts @@ -8,6 +8,8 @@ import type { WorkerResponse, FramePayload, EasterEggSeed, + EventCatalog, + CrossTriggers, } from '@magic-civ/engine-ts' import { SCENARIOS, @@ -18,13 +20,13 @@ import { STARTING_ATMOSPHERES, deriveSubstrates, classifyBiome, -} from '@magic-civ/engine-ts' -import { WasmClimatePhysics, WasmEcologyPhysics, WasmGrid, WasmMapGenerator, -} from '@magic-civ/physics-rs' + EventEvaluator, + DEFAULT_DT, +} from '@magic-civ/engine-ts' // --------------------------------------------------------------------------- // Worker-internal scenario state @@ -40,10 +42,12 @@ interface ScenarioState { wasmGrid: WasmGrid physics: WasmClimatePhysics ecology: WasmEcologyPhysics | null + eventEvaluator: EventEvaluator | null cursorScenarioTurn: number totalScenarioTurns: number worldSeed: number isVolcanicWinter: boolean + dt: number } const CHUNK_SIZE = 5 @@ -72,9 +76,10 @@ function wasmPhysicsStep( wasmGrid: WasmGrid, turn: number, seed: number, + dt: number, ): void { - physics.processStep(wasmGrid, turn, seed) - if (ecology) ecology.processStep(wasmGrid) + physics.processStep(wasmGrid, turn, seed, dt) + if (ecology) ecology.processStep(wasmGrid, dt) physics.stepAtmosphericChemistry(wasmGrid) } @@ -144,14 +149,29 @@ function stepOneTurn(state: ScenarioState, scenarioTurn: number, specJson: strin state.wasmGrid.free() state.wasmGrid = jsGridToWasm(state.grid) } - wasmPhysicsStep(state.physics, state.ecology, state.wasmGrid, absTurn, state.worldSeed) + wasmPhysicsStep(state.physics, state.ecology, state.wasmGrid, absTurn, state.worldSeed, state.dt) // Compute stats in Rust — avoids full 960-tile JSON deserialization every turn. const prevStats = state.stats.length > 0 ? state.stats[state.stats.length - 1] : undefined const stats: TurnStats = JSON.parse( state.wasmGrid.computeStatsJson(prevStats?.avg_temp ?? 0.5, prevStats?.avg_moisture ?? 0.5) ) state.cursorScenarioTurn = scenarioTurn + 1 - return { stats, events: [] } + + // Fire stochastic events — only deserialize JS grid when events would fire or + // there are active multi-turn effects already running. + let events: EcologicalEvent[] = [] + const evaluator = state.eventEvaluator + if (evaluator !== null && (evaluator.checkWillFire(absTurn, state.dt) || evaluator.hasActiveEffects())) { + state.grid = wasmGridToJs(state.wasmGrid) + events = evaluator.evaluateTurn(state.grid, absTurn, state.dt) + if (events.length > 0) { + // Sync any event-driven grid mutations back to WASM + state.wasmGrid.free() + state.wasmGrid = jsGridToWasm(state.grid) + } + } + + return { stats, events } } function addCheckpoint(state: ScenarioState, turn: number): void { @@ -187,7 +207,7 @@ function seekToTurn(state: ScenarioState, targetSt: number): void { state.wasmGrid.free() state.wasmGrid = jsGridToWasm(state.grid) } - wasmPhysicsStep(state.physics, state.ecology, state.wasmGrid, absTurn, state.worldSeed) + wasmPhysicsStep(state.physics, state.ecology, state.wasmGrid, absTurn, state.worldSeed, state.dt) } state.grid = wasmGridToJs(state.wasmGrid) state.cursorScenarioTurn = targetSt @@ -219,7 +239,7 @@ function seekToTurn(state: ScenarioState, targetSt: number): void { state.wasmGrid.free() state.wasmGrid = jsGridToWasm(state.grid) } - wasmPhysicsStep(state.physics, state.ecology, state.wasmGrid, absTurn, state.worldSeed) + wasmPhysicsStep(state.physics, state.ecology, state.wasmGrid, absTurn, state.worldSeed, state.dt) } state.grid = wasmGridToJs(state.wasmGrid) state.cursorScenarioTurn = targetSt @@ -263,7 +283,7 @@ async function prebufferFrames(state: ScenarioState, scenarioId: string, frameCo tmpWasmGrid.free() tmpWasmGrid = jsGridToWasm(tmpGrid) } - wasmPhysicsStep(tmpPhysics, tmpEcology, tmpWasmGrid, absTurn, state.worldSeed) + wasmPhysicsStep(tmpPhysics, tmpEcology, tmpWasmGrid, absTurn, state.worldSeed, state.dt) } // Use writeFrameBuffers for the texture data (hot path) @@ -370,6 +390,7 @@ async function handleRun(scenarioId: string, turns: number, prebufferFrameCount: const worldAge = config.worldAge const totalAbsTurns = worldAge + turns const isVolcanicWinter = config.id === 'volcanic_winter' + const dt = config.dt ?? DEFAULT_DT // Planet-specific params override earth defaults when available const planet = config.planet ?? 'earth' @@ -436,7 +457,7 @@ async function handleRun(scenarioId: string, turns: number, prebufferFrameCount: if (geoEcology) geoEcology.free() return } - wasmPhysicsStep(geoPhysics, geoEcology, geoWasmGrid, turn, seed) + wasmPhysicsStep(geoPhysics, geoEcology, geoWasmGrid, turn, seed, dt) if (turn % CHUNK_SIZE === 0) { post({ type: 'progress', scenarioId, turn, total: totalAbsTurns, phase: 'geology' }) @@ -468,15 +489,29 @@ async function handleRun(scenarioId: string, turns: number, prebufferFrameCount: const physics = new WasmClimatePhysics(effectiveParamsJson, terrainJson, baseSpecJson) const ecology = config.abioticWorld ? null : new WasmEcologyPhysics() const wasmGrid = jsGridToWasm(grid) + + // Build event evaluator from ecological_events catalog loaded at init + const ecologicalEvents = baseSpec.ecological_events as Record | undefined + let eventEvaluator: EventEvaluator | null = null + if (ecologicalEvents) { + const { cross_triggers: crossTriggersRaw, ...eventCategories } = ecologicalEvents + eventEvaluator = new EventEvaluator( + eventCategories as EventCatalog, + (crossTriggersRaw ?? {}) as CrossTriggers, + seed, + ) + } + const state: ScenarioState = { config, grid, wasmGrid, physics, - ecology, + ecology, eventEvaluator, stats: [], events: [], timings: [], checkpoints: new Map(), cursorScenarioTurn: 0, totalScenarioTurns: turns, worldSeed: seed, isVolcanicWinter, + dt, } addCheckpoint(state, 0) @@ -592,7 +627,7 @@ function handleFrame(scenarioId: string, turn: number, lookahead: number): void tmpWasmGrid.free() tmpWasmGrid = jsGridToWasm(tmpGrid) } - wasmPhysicsStep(tmpPhysics, tmpEcology, tmpWasmGrid, absTurn, state.worldSeed) + wasmPhysicsStep(tmpPhysics, tmpEcology, tmpWasmGrid, absTurn, state.worldSeed, state.dt) } // Use writeFrameBuffers for texture data