"use strict"; // Layer: entity-runtime/tarinai/policy // Owns Tarinai update cadence decisions. The cadence throttles expensive // thinking/environment work, while visible movement can still be integrated // every frame via the smooth-motion pass. (function (global) { const helpers = global.TarinaiSimulationRuntime.helpers; const Geometry = global.TarinaiGeometry; function behaviorOf(tarinai) { return currentTarinaiBehavior(tarinai); } function behaviorId(tarinai) { const behavior = behaviorOf(tarinai); return String(behavior?.actionId || tarinai?.state || "none"); } function visible(worldRef, tarinai, visibleRect) { return Boolean(Geometry.pointInRect(tarinai.x || 0, tarinai.y || 0, visibleRect)); } function hasMotionNeed(tarinai) { if (!tarinai || tarinai.dead) return false; if (tarinai.target && !tarinai.target.dead) return true; const speed = Math.hypot((tarinai.vx || 0) + (tarinai.impulseVx || 0), (tarinai.vy || 0) + (tarinai.impulseVy || 0)); if (speed > 1.8) return true; const id = behaviorId(tarinai); return /idle|wander|approach|follow|seek|panic|fight|birth_ritual|sunbath|zunchi_sick|fight_sick|cursor_/i.test(id); } function activeMechanicalObstacleNear(tarinai, worldRef = null) { if (!tarinai || !worldRef || worldRef.isTarinaiHiddenInNestBox?.(tarinai)) return false; const pressure = worldRef._mechanicalCrowdPressure; if (pressure?.active && pressure.ids) { return pressure.ids.has(tarinai.id || tarinai.familyKey || tarinai); } const radius = Math.max(150, (Number(tarinai.radius) || 22) + 190); const source = worldRef.nearbyNonGrassItems?.(tarinai.x || 0, tarinai.y || 0, radius, true) || worldRef.items || []; for (const it of source) { if (!it || it.dead || !global.TarinaiMechanicalSystem.isMechanicalType(it.type)) continue; const reach = global.TarinaiMechanicalSystem.reach(it) || Math.max(64, it.r || 64); if (Math.hypot((tarinai.x || 0) - (it.x || 0), (tarinai.y || 0) - (it.y || 0)) > reach + radius) continue; if (it.type === "rotator" || it.type === "reciprocator") return true; if (global.TarinaiMechanicalSystem.passiveItemAwake(it)) return true; const body = global.TarinaiPhysicsBodySystem.ensureBody(it, worldRef, { syncFromLegacy: false }); const vel = body?.velocity || {}; if (Math.hypot(Number(vel.x || it.vx || 0) || 0, Number(vel.y || it.vy || 0) || 0) + Math.abs(Number(vel.angular || it.spin || 0) || 0) * 64 > 0.8) return true; } return false; } function hasPassivePhysicsNeed(tarinai, worldRef = null, isVisible = false) { if (!tarinai || tarinai.dead) return false; const vx = Number.isFinite(tarinai.vx) ? tarinai.vx : 0; const vy = Number.isFinite(tarinai.vy) ? tarinai.vy : 0; const ivx = Number.isFinite(tarinai.impulseVx) ? tarinai.impulseVx : 0; const ivy = Number.isFinite(tarinai.impulseVy) ? tarinai.impulseVy : 0; if (Math.hypot(vx + ivx, vy + ivy) > 0.35) return true; const id = behaviorId(tarinai); const state = String(tarinai.state || id || ""); const sleeping = state === "sleep" || tarinai.sleeping; const lockedPassive = sleeping || state === "eat" || /eat|sunbath|birth_ritual|breed|repro/i.test(id) || /sunbath|birth_ritual|breed|repro/.test(state) || (tarinai.birthRitualTimer || 0) > 0.04 || (tarinai.eatTimer || 0) > 0.04 || (tarinai.sunbathTimer || 0) > 0.04; if (sleeping && isVisible && !worldRef?.isTarinaiHiddenInNestBox?.(tarinai)) return true; if (lockedPassive && isVisible && activeMechanicalObstacleNear(tarinai, worldRef)) return true; if (sleeping && tarinai.target && !tarinai.target.dead && tarinai.isSleepFurniture?.(tarinai.target) && !globalThis.TarinaiToolRuntime?.isNestContainerItem?.(tarinai.target, { alive: false })) return true; return false; } function cadence(worldRef, tarinai, visibleRect, nearVisibleRect = null) { if (!tarinai || tarinai.dead) return { interval: Infinity, lane: "dead", smoothMotion: false, visible: false, urgent: false }; const isSelected = worldRef.selected === tarinai; if (isSelected) return { interval: 0, lane: "selected", smoothMotion: false, visible: true, urgent: true }; const id = behaviorId(tarinai); const state = String(tarinai.state || id || ""); const isVisible = visible(worldRef, tarinai, visibleRect); const farRect = isVisible ? visibleRect : (nearVisibleRect || helpers.updateVisibleWorldRect(worldRef, 520)); const isNear = isVisible || Boolean(Geometry.pointInRect(tarinai.x || 0, tarinai.y || 0, farRect)); const motionNeed = hasMotionNeed(tarinai); const perfProfile = global.TarinaiPerf?.performanceProfile?.(); const nestEntryActive = Boolean(tarinai.insideNestBoxId && (typeof tarinai.nestBoxEntryActive === "function" ? tarinai.nestBoxEntryActive() : ((worldRef.time || 0) < (tarinai._nestBoxEntrySmoothUntil || -Infinity) || ((tarinai.nestFade || 0) > 0.01 && (tarinai.nestFade || 0) < 0.985)))); if (nestEntryActive) return { interval: 0, lane: "nest-entry", smoothMotion: false, visible: true, urgent: true }; const hardRealtimeUrgent = (tarinai.hurtTimer || 0) > 0.04 || (tarinai.pokeFlashTimer || 0) > 0.03 || (tarinai.entryTimer || 0) > 0.01 || (tarinai.fallTimer || 0) > 0.01 || /knockback|danger|hurt|ant_attack|cursor_enemy|explosion/i.test(id); if (hardRealtimeUrgent) return { interval: 0, lane: "urgent", smoothMotion: false, visible: isVisible, urgent: true }; // Fighting used to run the complete AI/environment/health pipeline for every // participant on every rendered frame. Dense conflict presets then formed a // positive feedback loop of fights, effects and collision work. Keep motion // smooth, but quantize the expensive full pipeline to a bounded cadence. const fightUrgent = state === "fight" || (tarinai.fightTimer || 0) > 0.04 || /fight/i.test(id); if (fightUrgent) { // Even the full-quality profile must not execute the entire dense-fight // pipeline once per rendered frame. It gets a tighter cadence than the // throttled profiles, while visible motion remains interpolated. const highQuality = perfProfile?.simulationThrottled === false; const baseInterval = highQuality ? (isVisible ? 0.035 : (isNear ? 0.07 : 0.14)) : (isVisible ? 0.06 : (isNear ? 0.12 : 0.24)); const interval = baseInterval * Math.max(1, Number(perfProfile?.creatureCadenceScale || 1)); return { interval, lane: highQuality ? "fight-quality-high" : "fight", smoothMotion: Boolean(isVisible && !worldRef.isTarinaiHiddenInNestBox?.(tarinai)), visible: isVisible, near: isNear, urgent: true, }; } if (perfProfile?.simulationThrottled === false) { return { interval: 0, lane: "quality-high", smoothMotion: false, visible: isVisible, near: isNear, urgent: false }; } // Panic movement must stay visually continuous, but the full creature // pipeline does not need to run every rendered frame. Treat panic as // a high-priority motion lane: AI/timers are quantized, motion-only still // runs on visible skipped frames. const panicUrgent = (tarinai.fearTimer || 0) > 0.20 || /panic/i.test(id); if (panicUrgent) { const baseInterval = isVisible ? 0.08 : (isNear ? 0.18 : 0.36); const interval = baseInterval * Math.max(1, Number(global.TarinaiPerf?.performanceProfile?.().creatureCadenceScale || 1)); return { interval, lane: "panic", smoothMotion: Boolean(isVisible && !worldRef.isTarinaiHiddenInNestBox?.(tarinai)), visible: isVisible, urgent: true }; } let interval; let lane = "calm"; if (/birth_ritual/i.test(id) || state === "birth_ritual" || (tarinai.birthRitualTimer || 0) > 0.04) { lane = "birth"; interval = isVisible ? 0.12 : 0.24; } else if (/approach_parent_or_child|follow_parent|approach_mate|seek_bed|play_ball|play_seesaw|seek_material|build/i.test(id) || /follow_parent|seek_bed|play_ball|play_seesaw/.test(state)) { lane = "moving"; interval = isVisible ? 0.10 : (isNear ? 0.22 : 0.55); } else if (/eat_food|eat/i.test(id) || state === "eat" || (tarinai.eatTimer || 0) > 0.04) { lane = "eat"; interval = isVisible ? 0.22 : 0.48; } else if (/sleep|sleep_in_bed/.test(id) || state === "sleep" || tarinai.sleeping) { lane = "sleep"; interval = isVisible ? 0.42 : (isNear ? 0.70 : 1.10); } else if (/idle|none/.test(id) || state === "idle") { lane = "idle"; if (isVisible) interval = 0.12; else interval = isNear ? 0.55 : 1.10; } else if (isVisible) { lane = "visible"; interval = 0.05; } else { lane = isNear ? "near" : "far"; interval = isNear ? 0.30 : 0.90; } if (Number.isFinite(interval)) interval *= Math.max(1, Number(perfProfile?.creatureCadenceScale || 1)); const passivePhysicsNeed = hasPassivePhysicsNeed(tarinai, worldRef, isVisible); const sleepPhysical = lane === "sleep" && passivePhysicsNeed; const eatPhysical = lane === "eat" && passivePhysicsNeed; const smoothAllowed = lane !== "eat" && lane !== "sleep"; const smoothMotion = Boolean(isVisible && Number.isFinite(interval) && interval > 0 && interval <= 0.75 && !worldRef.isTarinaiHiddenInNestBox?.(tarinai) && ((smoothAllowed && motionNeed) || sleepPhysical || eatPhysical)); return { interval, lane, smoothMotion, visible: isVisible, near: isNear, urgent: false, sleepPhysical, eatPhysical, passivePhysicsNeed }; } global.TarinaiCreatureUpdatePolicy = Object.freeze({ updateCadence: cadence, behaviorId, }); })(typeof window !== "undefined" ? window : globalThis);