tarinai/js/system_order.js
2026-07-20 14:36:59 +09:00

77 lines
4.1 KiB
JavaScript

"use strict";
// Layer: simulation/system-order
// Names the update order in one place. Prefer concrete simulation systems,
// with the phase facade kept as a stable named entry point.
(function (global) {
function phase(name) {
return global.TarinaiSimulationSystems.phases?.[name] || null;
}
const SYSTEM_ORDER = Object.freeze([
{ id: "clock", run(worldRef, ctx) { phase("updateClock")?.(worldRef, ctx.dt); } },
{ id: "frame_signals", run(worldRef, ctx) { phase("updateFrameSignals")?.(worldRef, ctx.dt); } },
{ id: "environment", run(worldRef, ctx) { phase("updateEnvironment")?.(worldRef, ctx.dt); } },
{ id: "spatial_prepare", run(worldRef) { phase("prepareSpatialFrame")?.(worldRef); } },
{ id: "items_and_ants", run(worldRef, ctx) { ctx.mobile = phase("updateItemsAndAnts")?.(worldRef, ctx.dt) || null; } },
{ id: "effects", run(worldRef, ctx) { phase("updateEffects")?.(worldRef, ctx.dt); } },
{ id: "creatures", run(worldRef, ctx) { phase("updateCreatures")?.(worldRef, ctx.dt); } },
{ id: "trap_holds", run(worldRef) { global.TarinaiItemEnvironmentHazardSystem?.enforceTrapHolds?.(worldRef); } },
{ id: "seesaws", run(worldRef, ctx) { global.TarinaiSeesawSystem?.updateWorld?.(worldRef, ctx.dt); } },
{ id: "maintenance", run(worldRef, ctx) { phase("runMaintenance")?.(worldRef, ctx.dt, ctx.mobile?.itemCountBefore ?? (worldRef.items || []).length); } },
{ id: "phase_events", run(worldRef) { phase("emitPhaseChange")?.(worldRef); } },
{ id: "ambient_tarinai", run(worldRef, ctx) { phase("spawnAmbientTarinai")?.(worldRef, ctx.dt); } },
{ id: "ambient_grass", run(worldRef, ctx) { phase("spawnAmbientGrass")?.(worldRef, ctx.dt); } },
{ id: "rain_water", run(worldRef, ctx) { phase("spawnRainWater")?.(worldRef, ctx.dt); } },
{ id: "diagnostics", run(worldRef) { phase("finalizeDiagnostics")?.(worldRef); } },
]);
function runSimulationStep(worldRef, stepDt) {
const ctx = { dt: stepDt, mobile: null };
const profiler = global.TarinaiPerf;
for (const system of SYSTEM_ORDER) {
const end = profiler.begin(`update.phase.${system.id}`) || null;
try {
system.run(worldRef, ctx);
} finally {
if (end) end();
}
}
}
const FIXED_SIMULATION_STEP = 0.025;
const MAX_FIXED_STEPS_PER_FRAME = 40;
function update(worldRef, dt) {
const normalize = phase("normalizeDelta");
if (!normalize) throw new Error("Tarinai simulation normalize phase is not available");
const scaledDt = normalize(worldRef, dt);
if (scaledDt === null) return;
// Always advance the world with one fixed simulation quantum. Previously
// slow speeds used many tiny render-frame-sized updates while fast speeds
// used fewer 0.05-second chunks. Any per-update random choice, cooldown
// edge, collision pass, or AI decision could therefore change simply by
// changing the speed control. A fixed accumulator keeps the number and
// size of simulation updates identical for the same amount of game time.
let accumulator = Math.max(0, Number(worldRef._fixedSimulationAccumulator || 0) || 0);
accumulator += Math.max(0, Number(scaledDt) || 0);
// Main-loop dt is capped and the public speed control tops out at x16, so
// this guard still covers the worst supported frame without dropping time.
let steps = 0;
while (accumulator + 1e-10 >= FIXED_SIMULATION_STEP && steps < MAX_FIXED_STEPS_PER_FRAME) {
runSimulationStep(worldRef, FIXED_SIMULATION_STEP);
accumulator -= FIXED_SIMULATION_STEP;
steps += 1;
}
// Keep a bounded remainder if an external caller supplies an unsupported
// delta. Normal gameplay never reaches this branch, but avoiding an
// unbounded backlog is safer than a permanent catch-up spiral.
if (steps >= MAX_FIXED_STEPS_PER_FRAME && accumulator >= FIXED_SIMULATION_STEP) {
accumulator = Math.min(accumulator, FIXED_SIMULATION_STEP * 0.999999);
}
worldRef._fixedSimulationAccumulator = Math.max(0, accumulator);
}
global.TarinaiSystemOrder = Object.freeze({ update });
})(typeof window !== "undefined" ? window : globalThis);