tarinai/js/simulation_creature_system.js
2026-07-10 16:40:06 +09:00

167 lines
7.3 KiB
JavaScript

"use strict";
// Layer: simulation/system-creatures
// Owns Tarinai update cadence, Tarinai movement invalidation, and creature
// collision/interactions scheduled after individual updates.
(function (global) {
const helpers = global.TarinaiSimulationRuntime.helpers;
const updatePolicy = global.TarinaiCreatureUpdatePolicy;
function inc(map, key, n = 1) {
const k = String(key || "none");
map[k] = (map[k] || 0) + n;
}
function backgroundFullBudgetFor() {
return Math.max(1, Number(global.TarinaiPerf?.performanceProfile?.().creatureBackgroundBudget || 36));
}
function consumesBackgroundBudget(cadence) {
if (!cadence || cadence.urgent || cadence.visible) return false;
return true;
}
function updateCreatures(worldRef, dt) {
const h = helpers;
const end = global.TarinaiPerf.begin("update.tarinai.individual") || null;
const visibleRect = h.updateVisibleWorldRect(worldRef, 160);
const nearVisibleRect = h.updateVisibleWorldRect(worldRef, 520);
const now = worldRef.time || 0;
const stats = {
total: 0,
full: 0,
realtime: 0,
smooth: 0,
skipped: 0,
selected: 0,
urgent: 0,
visible: 0,
near: 0,
far: 0,
lanes: {},
states: {},
smoothStates: {},
skippedStates: {},
sleepPhysical: 0,
passivePhysical: 0,
fullBudget: backgroundFullBudgetFor(worldRef),
fullBudgetUsed: 0,
fullBudgetSkips: 0,
offscreenSkipped: 0,
};
const prevSpatialDeferMode = worldRef.deferSpatialRebuildMode || "";
worldRef.deferSpatialRebuildDepth = (worldRef.deferSpatialRebuildDepth || 0) + 1;
// Smooth-motion Tarinai pass may read obstacle/item grids after Tarinai and
// constraint movement have dirtied mobile buckets. Use the last coherent
// grid during this phase and collapse mobile invalidations into one rebuild
// after all visible bodies have been advanced.
worldRef.deferSpatialRebuildMode = "mobile";
try {
for (const t of worldRef.tarinai) {
if (!t || t.dead) continue;
stats.total += 1;
const behaviorId = updatePolicy.behaviorId(t);
inc(stats.states, behaviorId);
const cadence = updatePolicy.updateCadence(worldRef, t, visibleRect, nearVisibleRect);
const interval = Number.isFinite(cadence.interval) ? cadence.interval : Infinity;
inc(stats.lanes, cadence.lane || "unknown");
if (worldRef.selected === t) stats.selected += 1;
if (cadence.urgent) stats.urgent += 1;
if (cadence.sleepPhysical) stats.sleepPhysical += 1;
if (cadence.passivePhysicsNeed) stats.passivePhysical += 1;
if (cadence.visible) stats.visible += 1;
else if (interval < 0.75) stats.near += 1;
else stats.far += 1;
if (!Number.isFinite(interval)) continue;
const usesBackgroundBudget = consumesBackgroundBudget(cadence);
const context = { cadence, motionDt: dt };
if (interval <= 0) {
const runDt = Math.min(1.4, (t._updateAccum || 0) + dt);
t._updateAccum = 0;
stats.full += 1;
stats.realtime += 1;
global.TarinaiCreatureRuntime.updateOne(t, runDt, { context });
continue;
}
t._updateAccum = Math.min(2.4, (t._updateAccum || 0) + dt);
if (!Number.isFinite(t._nextLowFreqUpdateAt)) {
const jitter = stableUnit(t.id || t.familyKey || Math.random(), "creature-cadence");
t._nextLowFreqUpdateAt = now + interval * (0.65 + jitter * 0.70);
}
if (now >= t._nextLowFreqUpdateAt) {
if (usesBackgroundBudget && stats.fullBudgetUsed >= stats.fullBudget) {
const jitter = stableUnit(t.id || t.familyKey || Math.random(), `creature-budget:${Math.floor(now * 4)}`);
t._nextLowFreqUpdateAt = now + Math.max(dt, interval * (0.18 + jitter * 0.18));
stats.fullBudgetSkips += 1;
stats.offscreenSkipped += cadence.visible ? 0 : 1;
stats.skipped += 1;
inc(stats.skippedStates, behaviorId);
continue;
}
const runDt = Math.max(dt, t._updateAccum || dt);
const jitter = stableUnit(t.id || t.familyKey || Math.random(), `creature-cadence:${Math.floor(now * 2)}`);
t._updateAccum = 0;
t._nextLowFreqUpdateAt = now + interval * (0.92 + jitter * 0.22);
stats.full += 1;
if (usesBackgroundBudget) stats.fullBudgetUsed += 1;
global.TarinaiCreatureRuntime.updateOne(t, runDt, { context });
} else if (cadence.smoothMotion) {
if (global.TarinaiCreatureRuntime.updateMotionOnly(t, dt, { context })) {
stats.smooth += 1;
inc(stats.smoothStates, behaviorId);
}
} else {
stats.skipped += 1;
inc(stats.skippedStates, behaviorId);
}
}
} finally {
worldRef.deferSpatialRebuildDepth = Math.max(0, (worldRef.deferSpatialRebuildDepth || 1) - 1);
worldRef.deferSpatialRebuildMode = prevSpatialDeferMode;
worldRef._creatureUpdateStats = stats;
if (end) end();
}
const bedConflictInterval = 3.2;
if (now >= (worldRef.nextBedConflictCheckAt || 0)) {
worldRef.nextBedConflictCheckAt = now + bedConflictInterval;
worldRef.workStats && (worldRef.workStats.bedConflictRuns = (worldRef.workStats.bedConflictRuns || 0) + 1);
worldRef.resolveOwnedBedSleepConflicts?.();
} else {
worldRef.workStats && (worldRef.workStats.bedConflictSkips = (worldRef.workStats.bedConflictSkips || 0) + 1);
}
const endSpatial = global.TarinaiPerf.begin("update.tarinai.spatial") || null;
const tarinaiMoved = h.markSpatialDirtyIfMoved(worldRef, worldRef.tarinai, "tarinai-moved", 0.25);
if (tarinaiMoved || worldRef.spatialDirty) {
if (worldRef.rebuildSpatial) worldRef.rebuildSpatial(true, worldRef.deferredSpatialDirtyReason || "post-tarinai-update");
else worldRef.ensureSpatial?.("post-tarinai-update");
worldRef.deferredSpatialDirtyReason = "";
}
if (endSpatial) endSpatial();
const endBallInteractions = global.TarinaiPerf.begin("update.ballInteractions") || null;
worldRef.limitBallChasers();
worldRef.resolveBallInteractions(dt);
if (endBallInteractions) endBallInteractions();
let fastTarinai = null;
const collisionSpeedThreshold = global.TarinaiCollisionResponseSystem?.highSpeedThreshold?.() || 165;
for (const t of worldRef.tarinai || []) {
if (!t || t.dead || worldRef.isTarinaiHiddenInNestBox?.(t)) continue;
if (Math.hypot(t.vx || 0, t.vy || 0) < collisionSpeedThreshold) continue;
if (!fastTarinai) fastTarinai = [];
fastTarinai.push(t);
}
if (fastTarinai?.length) {
worldRef.workStats && (worldRef.workStats.collisionRuns = (worldRef.workStats.collisionRuns || 0) + 1);
const endCollision = global.TarinaiPerf.begin("update.tarinai.collisions") || null;
try { worldRef.resolveTarinaiHighSpeedCollisions(dt, { fastSources: fastTarinai, threshold: collisionSpeedThreshold }); }
finally { if (endCollision) endCollision(); }
} else {
worldRef.workStats && (worldRef.workStats.collisionSkips = (worldRef.workStats.collisionSkips || 0) + 1);
}
}
const phases = Object.freeze({ updateCreatures });
global.TarinaiCreatureSimulationSystem = Object.freeze({ phases });
})(typeof window !== "undefined" ? window : globalThis);