311 lines
15 KiB
JavaScript
311 lines
15 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) {
|
|
if (!map) return;
|
|
const k = String(key || "none");
|
|
map[k] = (map[k] || 0) + n;
|
|
}
|
|
|
|
function backgroundFullBudgetFor() {
|
|
return Math.max(1, Number(global.TarinaiPerf?.performanceProfile?.().creatureBackgroundBudget || 36));
|
|
}
|
|
|
|
function profileNumber(name, fallback) {
|
|
const value = Number(global.TarinaiPerf?.performanceProfile?.()[name]);
|
|
return Number.isFinite(value) ? value : fallback;
|
|
}
|
|
|
|
function resetMechanicalCrowdPressure(worldRef) {
|
|
const pressure = worldRef._mechanicalCrowdPressure || (worldRef._mechanicalCrowdPressure = { ids: new Set(), sources: [], active: false, count: 0, mechanical: 0, frame: 0 });
|
|
pressure.ids.clear();
|
|
pressure.sources.length = 0;
|
|
pressure.active = false;
|
|
pressure.count = 0;
|
|
pressure.mechanical = 0;
|
|
pressure.detailedContacts = 0;
|
|
pressure.degradedContacts = 0;
|
|
pressure.skippedContacts = 0;
|
|
pressure.contactBudget = 0;
|
|
pressure.frame = (pressure.frame || 0) + 1;
|
|
pressure.profile = global.TarinaiPerf?.simulationOptimizationTier?.() || "balanced";
|
|
return pressure;
|
|
}
|
|
|
|
function hasMechanicalPressureSources(worldRef) {
|
|
const counts = worldRef?.itemCounts || {};
|
|
return Boolean((counts.rotator || 0) + (counts.reciprocator || 0) + (counts.poison_block || 0));
|
|
}
|
|
|
|
function mechanicalItemActive(item, worldRef) {
|
|
if (!item || item.dead) return false;
|
|
const pb = global.TarinaiPhysicsBodySystem;
|
|
const body = pb?.ensureBody?.(item, worldRef, { syncFromLegacy: false }) || item.physicsBody || null;
|
|
const velocity = body?.velocity || {};
|
|
const motor = body?.motor || {};
|
|
const mech = global.TarinaiMechanicalSystem;
|
|
if (item.type === "rotator") return Number(mech?.motionLevel?.(item) || 0) > 0.55;
|
|
if (item.type === "reciprocator") {
|
|
const drive = (motor.powered !== false ? Math.max(0, Number(motor.speed || item.railMotorSpeed || 0) || 0) : 0) + Math.abs(Number(velocity.linear || item.slideSpeed || 0) || 0);
|
|
return drive > 0.08;
|
|
}
|
|
if (item.type === "poison_block") return Boolean(global.TarinaiMechanicalSystem?.passiveItemAwake?.(item));
|
|
return false;
|
|
}
|
|
|
|
function prepareMechanicalCrowdPressure(worldRef, visibleRect) {
|
|
const pressure = resetMechanicalCrowdPressure(worldRef);
|
|
if (!hasMechanicalPressureSources(worldRef)) return pressure;
|
|
pressure.contactBudget = Math.max(1, profileNumber("pressureObstacleContactBudget", 10));
|
|
if (!worldRef?.itemsOfType || !worldRef?.nearbyTarinai) return pressure;
|
|
const mech = global.TarinaiMechanicalSystem;
|
|
if (!mech?.reach) return pressure;
|
|
const types = ["rotator", "reciprocator", "poison_block"];
|
|
let checked = 0;
|
|
for (const type of types) {
|
|
for (const item of worldRef.itemsOfType(type) || []) {
|
|
if (!item || item.dead || !mechanicalItemActive(item, worldRef)) continue;
|
|
checked += 1;
|
|
pressure.mechanical += 1;
|
|
pressure.sources.push(item);
|
|
const radius = Math.max(120, (mech.reach(item) || item.r || 80) + 140);
|
|
const candidates = worldRef.nearbyTarinai(item.x || 0, item.y || 0, radius, true) || [];
|
|
for (const t of candidates) {
|
|
if (!t || t.dead || worldRef.isTarinaiHiddenInNestBox?.(t)) continue;
|
|
if (visibleRect && !global.TarinaiGeometry?.pointInRect?.(t.x || 0, t.y || 0, visibleRect)) continue;
|
|
pressure.ids.add(t.id || t.familyKey || t);
|
|
}
|
|
if (checked >= 8) break;
|
|
}
|
|
if (checked >= 8) break;
|
|
}
|
|
pressure.count = pressure.ids.size;
|
|
pressure.active = pressure.count >= 10;
|
|
pressure.intensity = Math.min(1, pressure.count / 44);
|
|
return pressure;
|
|
}
|
|
|
|
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 pressure = prepareMechanicalCrowdPressure(worldRef, visibleRect);
|
|
const pressureActive = Boolean(pressure?.active);
|
|
const visibleFullBudget = pressureActive ? Math.max(1, profileNumber("pressureCreatureFullBudget", 24)) : 1000000000;
|
|
const visibleMotionBudget = pressureActive ? Math.max(1, profileNumber("pressureCreatureMotionBudget", 36)) : 1000000000;
|
|
const collectDiagnostics = global.TarinaiPerf?.diagnosticsEnabled?.() === true;
|
|
const stats = {
|
|
total: 0,
|
|
full: 0,
|
|
realtime: 0,
|
|
smooth: 0,
|
|
skipped: 0,
|
|
selected: 0,
|
|
urgent: 0,
|
|
visible: 0,
|
|
near: 0,
|
|
far: 0,
|
|
lanes: collectDiagnostics ? {} : null,
|
|
states: collectDiagnostics ? {} : null,
|
|
smoothStates: collectDiagnostics ? {} : null,
|
|
skippedStates: collectDiagnostics ? {} : null,
|
|
sleepPhysical: 0,
|
|
passivePhysical: 0,
|
|
fullBudget: backgroundFullBudgetFor(worldRef),
|
|
fullBudgetUsed: 0,
|
|
fullBudgetSkips: 0,
|
|
visibleFullBudget,
|
|
visibleFullUsed: 0,
|
|
visibleFullSkips: 0,
|
|
visibleMotionBudget,
|
|
visibleMotionUsed: 0,
|
|
visibleMotionSkips: 0,
|
|
mechanicalPressure: pressure?.count || 0,
|
|
mechanicalPressureActive: pressureActive,
|
|
mechanicalContactBudget: pressure?.contactBudget || 0,
|
|
mechanicalDetailedContacts: pressure?.detailedContacts || 0,
|
|
mechanicalDegradedContacts: pressure?.degradedContacts || 0,
|
|
mechanicalSkippedContacts: pressure?.skippedContacts || 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 {
|
|
const creatureList = worldRef.tarinai || [];
|
|
const creatureCount = creatureList.length;
|
|
const perfProfile = global.TarinaiPerf?.performanceProfile?.() || {};
|
|
// High-population AI/environment budgets are consumed in a rotating order.
|
|
// Every creature is still visited each frame, but the first candidates for
|
|
// expensive scheduled work change without a queue, sort, or per-frame hash.
|
|
const rotateScheduledWork = perfProfile.simulationThrottled !== false && creatureCount >= 80;
|
|
let creatureIndex = rotateScheduledWork
|
|
? (Math.max(0, Math.floor(worldRef._creatureScheduledWorkCursor || 0)) % Math.max(1, creatureCount))
|
|
: 0;
|
|
if (rotateScheduledWork) {
|
|
const cursorStep = Math.max(1, Math.min(creatureCount, Math.floor(Number(perfProfile.aiBudget || 10)) || 10));
|
|
worldRef._creatureScheduledWorkCursor = (creatureIndex + cursorStep) % creatureCount;
|
|
} else {
|
|
worldRef._creatureScheduledWorkCursor = 0;
|
|
}
|
|
for (let creatureVisited = 0; creatureVisited < creatureCount; creatureVisited += 1) {
|
|
if (creatureIndex >= creatureCount) creatureIndex = 0;
|
|
const t = creatureList[creatureIndex++];
|
|
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 (cadence.visible && stats.visibleFullUsed >= stats.visibleFullBudget) {
|
|
const jitter = stableUnit(t.id || t.familyKey || Math.random(), `visible-creature-budget:${Math.floor(now * 5)}`);
|
|
t._nextLowFreqUpdateAt = now + Math.max(dt, interval * (0.20 + jitter * 0.25));
|
|
stats.visibleFullSkips += 1;
|
|
stats.skipped += 1;
|
|
inc(stats.skippedStates, behaviorId);
|
|
continue;
|
|
}
|
|
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 (cadence.visible) stats.visibleFullUsed += 1;
|
|
if (usesBackgroundBudget) stats.fullBudgetUsed += 1;
|
|
global.TarinaiCreatureRuntime.updateOne(t, runDt, { context });
|
|
} else if (cadence.smoothMotion) {
|
|
if (cadence.visible && stats.visibleMotionUsed >= stats.visibleMotionBudget) {
|
|
stats.visibleMotionSkips += 1;
|
|
stats.skipped += 1;
|
|
inc(stats.skippedStates, behaviorId);
|
|
continue;
|
|
}
|
|
if (global.TarinaiCreatureRuntime.updateMotionOnly(t, dt, { context })) {
|
|
stats.smooth += 1;
|
|
if (cadence.visible) stats.visibleMotionUsed += 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;
|
|
stats.mechanicalDetailedContacts = pressure?.detailedContacts || 0;
|
|
stats.mechanicalDegradedContacts = pressure?.degradedContacts || 0;
|
|
stats.mechanicalSkippedContacts = pressure?.skippedContacts || 0;
|
|
worldRef._creatureUpdateStats = collectDiagnostics ? stats : null;
|
|
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;
|
|
// Maintain the Tarinai grid incrementally. The same O(N) pass that used to
|
|
// detect movement now moves only entities that crossed a cell boundary,
|
|
// avoiding a second O(N) clear-and-rebuild pass every active frame.
|
|
const tarinaiCellChanges = worldRef.spatial?.syncTarinai?.(worldRef.tarinai || []) || 0;
|
|
if (tarinaiCellChanges > 0) worldRef.spatialVersion = (worldRef.spatialVersion || 0) + 1;
|
|
if (worldRef.spatialDirty && (worldRef.spatialStaticItemsDirty || worldRef.spatialDynamicItemsDirty || worldRef.spatialAntDirty)) {
|
|
if (worldRef.rebuildSpatial) worldRef.rebuildSpatial(true, worldRef.deferredSpatialDirtyReason || "post-tarinai-update");
|
|
else worldRef.ensureSpatial?.("post-tarinai-update");
|
|
} else if (worldRef.spatialTarinaiDirty) {
|
|
worldRef.spatialTarinaiDirty = false;
|
|
worldRef.spatialDirty = Boolean(worldRef.spatialStaticItemsDirty || worldRef.spatialDynamicItemsDirty || worldRef.spatialAntDirty);
|
|
}
|
|
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 {
|
|
const profile = global.TarinaiPerf?.performanceProfile?.() || {};
|
|
worldRef.resolveTarinaiHighSpeedCollisions(dt, {
|
|
fastSources: fastTarinai,
|
|
threshold: collisionSpeedThreshold,
|
|
maxSources: profile.collisionSourceBudget,
|
|
maxImpacts: profile.collisionImpactBudget,
|
|
});
|
|
}
|
|
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);
|