tarinai/js/tarinai_update_policy.js
2026-07-01 22:59:49 +09:00

150 lines
8.1 KiB
JavaScript

"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;
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(helpers.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 radius = Math.max(150, (Number(tarinai.radius) || 22) + 190);
const source = worldRef.nearbyItems?.(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) && tarinai.target.type !== "nest_box") 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 count = (worldRef.tarinai || []).length;
const mobile = Boolean(global.TarinaiInputMode.snapshot().touchFirst || (global.innerWidth || 9999) <= 760);
const isVisible = visible(worldRef, tarinai, visibleRect);
const farRect = isVisible ? visibleRect : (nearVisibleRect || helpers.updateVisibleWorldRect(worldRef, 520));
const isNear = isVisible || Boolean(helpers.pointInRect(tarinai.x || 0, tarinai.y || 0, farRect));
const motionNeed = hasMotionNeed(tarinai);
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 ||
/fight|knockback|danger|hurt|ant_attack|cursor_enemy|explosion/i.test(id);
if (hardRealtimeUrgent) return { interval: 0, lane: "urgent", smoothMotion: false, visible: isVisible, urgent: true };
// 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 interval = isVisible ? (mobile ? 0.13 : 0.08) : (isNear ? (mobile ? 0.24 : 0.18) : (mobile ? 0.46 : 0.36));
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|seek_material|build/i.test(id) || /follow_parent|seek_bed|play_ball/.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 = count > 28 ? 0.16 : 0.12;
else interval = isNear ? 0.55 : 1.10;
} else if (isVisible) {
lane = "visible";
interval = count > 36 ? 0.11 : (count > 24 ? 0.08 : 0.05);
} else {
lane = isNear ? "near" : "far";
interval = isNear ? 0.30 : 0.90;
}
if (Number.isFinite(interval) && !/birth|eat|sleep/.test(lane)) {
if (count > 54) interval *= 1.26;
else if (count > 38) interval *= 1.14;
}
if (mobile && Number.isFinite(interval)) interval *= count > 24 ? 1.35 : 1.18;
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, urgent: false, sleepPhysical, eatPhysical, passivePhysicsNeed };
}
global.TarinaiCreatureUpdatePolicy = Object.freeze({
updateCadence: cadence,
behaviorId,
});
})(typeof window !== "undefined" ? window : globalThis);