"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);