"use strict"; // Layer: entity-runtime/tarinai/update-step // Owns sunbath/runtime movement, collisions, and nest-box occupancy refresh. (function (global) { const SHOCK_DAMAGE_DELTA_THRESHOLD = 205; const SHOCK_DAMAGE_COOLDOWN = 0.46; function combinedVelocity(t) { return { vx: (Number.isFinite(t?.vx) ? t.vx : 0) + (Number.isFinite(t?.impulseVx) ? t.impulseVx : 0), vy: (Number.isFinite(t?.vy) ? t.vy : 0) + (Number.isFinite(t?.impulseVy) ? t.impulseVy : 0), }; } function applyVelocityShockDamage(t, before, dt) { if (!t || t.dead) return false; const now = t.world?.time || 0; const after = combinedVelocity(t); const baseline = before || (Number.isFinite(t._lastPhysicalVelocityX) && Number.isFinite(t._lastPhysicalVelocityY) ? { vx: t._lastPhysicalVelocityX, vy: t._lastPhysicalVelocityY } : after); const delta = Math.hypot(after.vx - baseline.vx, after.vy - baseline.vy); t._lastPhysicalVelocityX = after.vx; t._lastPhysicalVelocityY = after.vy; // A bounce fence is designed to launch rather than injure. Its deliberate // velocity discontinuity must not be interpreted as generic collision // shock damage on the following movement update. if (now - Number(t.lastBounceFenceImpulseAt || -999) < 0.62) return false; if (delta < SHOCK_DAMAGE_DELTA_THRESHOLD) return false; if (now - (t.lastVelocityShockDamageAt || -999) < SHOCK_DAMAGE_COOLDOWN) return false; if (now - (t.lastPhysicalCollisionDamageAt || -999) < SHOCK_DAMAGE_COOLDOWN) return false; const amount = Math.min(42, Math.max(1.0, (delta - SHOCK_DAMAGE_DELTA_THRESHOLD) / 14)); const source = (now - Number(t.lastSolidObstacleCollisionAt || -999) < 0.75) ? (t.lastSolidObstacleCollisionSource || null) : null; t.lastVelocityShockDamageAt = now; t.world?.applyImpactDamage?.(t, amount, "\u6025\u6fc0\u306a\u901f\u5ea6\u5909\u5316", { cause: "\u885d\u7a81", source }) || t.damage?.(amount, "\u885d\u7a81"); t.hurtTimer = Math.max(t.hurtTimer || 0, amount > 15 ? 1.9 : 0.9); t.fallTimer = Math.max(t.fallTimer || 0, amount > 12 ? 0.72 : 0.28); t.fallMax = Math.max(t.fallMax || 0, t.fallTimer || 0); t.fearTimer = Math.max(t.fearTimer || 0, 0.45); if (t.world?.spawnFallEffect) t.world.spawnFallEffect(t.x, t.y + (t.radius || 22) * 0.55, Math.min(1.8, Math.max(0.5, amount / 18))); return true; } function numeric(v, fallback = 0) { return Number.isFinite(Number(v)) ? Number(v) : fallback; } function markSleepExternalMotion(t, dt, reason = "sleep-external-motion") { if (!t || !(t.state === "sleep" || t.sleeping)) return false; const world = t.world || null; const now = Number(world?.time || 0) || 0; const vx = numeric(t.vx); const vy = numeric(t.vy); const ivx = numeric(t.impulseVx); const ivy = numeric(t.impulseVy); const speed = Math.hypot(vx + ivx, vy + ivy); const recentlyHit = now - Number(t.lastSolidObstacleCollisionAt || t.lastPhysicalCollisionDamageAt || -999) < 0.35; if (speed > 0.55 || recentlyHit) { const hold = speed > 18 ? 1.15 : (speed > 3 ? 0.75 : 0.42); t._sleepExternalMotionUntil = Math.max(Number(t._sleepExternalMotionUntil || 0) || 0, now + hold); return true; } return now < Number(t._sleepExternalMotionUntil || 0); } function sleepExternalMotionActive(t) { const now = Number(t?.world?.time || 0) || 0; return Boolean(t && (t.state === "sleep" || t.sleeping) && now < Number(t._sleepExternalMotionUntil || 0)); } function wakeSleepingFromExternalMotion(t, reason = "\u5916\u529b\u3067\u76ee\u304c\u899a\u3081\u305f") { if (!t || t.dead || t.sleepDisease || !(t.state === "sleep" || t.sleeping)) return false; const world = t.world || null; if (world?.isTarinaiHiddenInNestBox?.(t)) return false; const now = Number(world?.time || 0) || 0; if (now < Number(t._sleepExternalWakeSuppressUntil || -999)) return false; t._sleepExternalWakeSuppressUntil = now + 0.45; t._sleepExternalMotionUntil = 0; t.consumePendingGrassBedOnWake?.("\u5916\u529b\u3067\u8d77\u304d\u305f"); t.sleeping = false; t.sleepSession = null; t.target = null; t.targetKey = ""; t.awakeLockTimer = Math.max(t.awakeLockTimer || 0, 5.2); t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.45); t.fearTimer = Math.max(t.fearTimer || 0, 0.18); t.setActionState?.("idle", { target: null, clearTarget: true, reason, wake: true, sleeping: false }); t.thought = reason; t.pinchThoughtTimer = Math.max(t.pinchThoughtTimer || 0, 1.0); if (t.bubble && now >= Number(t._sleepExternalWakeBubbleAt || -999) + 0.6) { t._sleepExternalWakeBubbleAt = now; t.bubble("!", 0.55, "rgba(88,82,118,0.78)"); } return true; } function passivePhysicalMotion(t, dt, context = {}) { if (!t || t.dead) return { done: true }; const world = t.world; const beforeX = numeric(t.x); const beforeY = numeric(t.y); const beforeVelocity = Number.isFinite(t._lastPhysicalVelocityX) && Number.isFinite(t._lastPhysicalVelocityY) ? { vx: t._lastPhysicalVelocityX, vy: t._lastPhysicalVelocityY } : combinedVelocity(t); // Nest boxes intentionally hide and pin sleepers. Visible beds, eating, and // other locked states still need a passive physics shell so external // impulses/mechanical contacts are not delayed until the next low-frequency // full update. if (world?.isTarinaiHiddenInNestBox?.(t)) { t.vx = 0; t.vy = 0; t.impulseVx = 0; t.impulseVy = 0; t.updateNestBoxPresence?.(dt); return { done: false, passive: true }; } let moveX = 0; let moveY = 0; const state = String(t.state || ""); const sleepExternal = markSleepExternalMotion(t, dt, "passive-physical"); if ((state === "sleep" || t.sleeping) && t.target && !t.target.dead && t.isSleepFurniture?.(t.target) && !globalThis.TarinaiToolRuntime?.isNestContainerItem?.(t.target, { alive: false })) { const comfort = world?.bedComfort ? world.bedComfort(t.target) : 0.72; const spot = t.sleepSpotFor ? t.sleepSpotFor(t.target) : t.target; const dx = numeric(spot?.x, beforeX) - beforeX; const dy = numeric(spot?.y, beforeY) - beforeY; const d = Math.hypot(dx, dy); const strongExternal = sleepExternal && Math.hypot(numeric(t.vx) + numeric(t.impulseVx), numeric(t.vy) + numeric(t.impulseVy)) > 1.2; // Beds should gently keep a sleeper nearby, but must not pin the body. // Under external force the restoring pull is deliberately weak so a // sleeping Tarinai behaves like a physical object instead of snapping back // to the sleep spot every smooth/full update. if (d > 1.2) { const restore = strongExternal ? (2.0 + comfort * 2.0) : (12 + comfort * 14); const maxRestore = strongExternal ? Math.min(d, 2.8) : d; const step = Math.min(maxRestore, dt * restore); moveX += dx / d * step; moveY += dy / d * step; } else if (!strongExternal && d > 0.05 && Math.hypot(t.vx || 0, t.vy || 0, t.impulseVx || 0, t.impulseVy || 0) < 0.6) { moveX += dx; moveY += dy; } } const vx = numeric(t.vx); const vy = numeric(t.vy); const impulseVx = numeric(t.impulseVx); const impulseVy = numeric(t.impulseVy); moveX += (vx + impulseVx) * dt; moveY += (vy + impulseVy) * dt; const movedDistance = Math.hypot(moveX, moveY); if (movedDistance > 0.001) { if (world?.moveTarinaiWithCollision) { world.moveTarinaiWithCollision(t, moveX, moveY, { dt, maxChecks: context.cadence?.lane === "sleep" ? 18 : 22, maxStep: 10, }); } else { t.x = beforeX + moveX; t.y = beforeY + moveY; } } const bodyDrag = state === "sleep" ? (sleepExternalMotionActive(t) ? 0.86 : 0.55) : (state === "eat" ? 0.42 : 0.66); t.vx = Math.abs(numeric(t.vx) * Math.pow(bodyDrag, dt * 60)) < 0.025 ? 0 : numeric(t.vx) * Math.pow(bodyDrag, dt * 60); t.vy = Math.abs(numeric(t.vy) * Math.pow(bodyDrag, dt * 60)) < 0.025 ? 0 : numeric(t.vy) * Math.pow(bodyDrag, dt * 60); const impulseDrag = Math.pow(0.58, dt * 3.2); t.impulseVx = Math.abs(impulseVx) < 0.35 ? 0 : impulseVx * impulseDrag; t.impulseVy = Math.abs(impulseVy) < 0.35 ? 0 : impulseVy * impulseDrag; applyVelocityShockDamage(t, beforeVelocity, dt); t.updateNestBoxPresence?.(dt); const dx = numeric(t.x) - beforeX; const dy = numeric(t.y) - beforeY; const lockedPassiveContact = /^(sleep|eat|sunbath|birth_ritual|breed|reproduction)$/.test(state); if (world?.resolveSolidObstacleCollision && !world.isTarinaiHiddenInNestBox?.(t) && (lockedPassiveContact || sleepExternalMotionActive(t))) { // A moving mechanical item can hit a sleeping Tarinai while the Tarinai's // own velocity is zero. In that case swept movement will never run, so // do a small contact pass every passive-physics tick. const beforeContactX = numeric(t.x); const beforeContactY = numeric(t.y); const pushed = world.resolveSolidObstacleCollision(t, { maxPasses: 2, searchRadius: Math.max(72, (numeric(t.radius, 22) || 22) + 120), maxChecks: context.cadence?.lane === "sleep" ? 18 : 22, slop: 0.28, }); if (pushed) { t.lastSolidObstacleCollisionAt = world.time || 0; markSleepExternalMotion(t, dt, "sleep-contact-push"); const cdx = numeric(t.x) - beforeContactX; const cdy = numeric(t.y) - beforeContactY; if (Math.hypot(cdx, cdy) > 0.05) { const pushCarry = state === "sleep" ? 0.085 : (state === "eat" ? 0.070 : 0.040); t.vx = clamp(numeric(t.vx) + cdx / Math.max(0.016, dt) * pushCarry, -170, 170); t.vy = clamp(numeric(t.vy) + cdy / Math.max(0.016, dt) * pushCarry, -170, 170); } } } else if (dx * dx + dy * dy <= 0.25 && world?.resolveFenceCollision && !world.isTarinaiHiddenInNestBox?.(t)) { const now = world.time || 0; if (now >= (t._nextPassiveSolidRelaxAt || 0)) { const jitter = stableUnit(t.id || t.familyKey || 0, "passive-solid-relax") * 0.55; t._nextPassiveSolidRelaxAt = now + 1.20 + jitter; world.resolveFenceCollision(t); } } if (state === "sleep" || t.sleeping) { const finalDx = numeric(t.x) - beforeX; const finalDy = numeric(t.y) - beforeY; const rawSpeed = Math.hypot(numeric(t.vx) + numeric(t.impulseVx), numeric(t.vy) + numeric(t.impulseVy)); const movedByExternal = Math.hypot(finalDx, finalDy) > 4.2 || rawSpeed > 26 || (sleepExternalMotionActive(t) && Math.hypot(finalDx, finalDy) > 2.2); if (movedByExternal) wakeSleepingFromExternalMotion(t, "\u52d5\u304b\u3055\u308c\u3066\u76ee\u304c\u899a\u3081\u305f"); } return { done: false, passive: true }; } function update(t, dt, context = {}) { if (!t || t.dead) return { done: true }; if (t.playerHeld || t._heldByPlayer) { t.vx = 0; t.vy = 0; t.impulseVx = 0; t.impulseVy = 0; t.target = null; t.targetKey = ""; t.updateNestBoxPresence?.(dt); t._lastPhysicalVelocityX = 0; t._lastPhysicalVelocityY = 0; return { done: false, held: true }; } const motionDt = Math.max(0.001, Math.min(0.05, Number(context.motionDt ?? dt) || 0.016)); if (context.motionOnly && /^(sleep|eat|sunbath|birth_ritual|breed|reproduction|intimidate|ant_intimidate)$/.test(String(t.state || ""))) { return passivePhysicalMotion(t, motionDt, context); } const beforeX = Number.isFinite(t.x) ? t.x : 0; const beforeY = Number.isFinite(t.y) ? t.y : 0; const current = combinedVelocity(t); const before = Number.isFinite(t._lastPhysicalVelocityX) && Number.isFinite(t._lastPhysicalVelocityY) ? { vx: t._lastPhysicalVelocityX, vy: t._lastPhysicalVelocityY } : current; global.TarinaiSunbathSystem.updateOne(t, motionDt); t.maintainTargetProgress(motionDt); const prevMotionOnlyFlag = t._motionOnlyPhysicsPass; if (context.motionOnly) t._motionOnlyPhysicsPass = true; try { t.move(motionDt); } finally { if (context.motionOnly) t._motionOnlyPhysicsPass = prevMotionOnlyFlag; } if (t.ammoCollisionKnockback) t.ammoCollisionKnockback(motionDt); applyVelocityShockDamage(t, before, motionDt); t.updateNestBoxPresence(motionDt); // t.move() calls world.moveTarinaiWithCollision(), which already performs // swept solid-obstacle resolution. Avoid a second full collision pass and // keep only a staggered low-frequency relax pass for near-stationary bodies // so old overlaps still self-heal. const world = t.world; if (world?.resolveFenceCollision) { const dx = (Number.isFinite(t.x) ? t.x : 0) - beforeX; const dy = (Number.isFinite(t.y) ? t.y : 0) - beforeY; const movedSq = dx * dx + dy * dy; if (movedSq <= 0.25 && !world.isTarinaiHiddenInNestBox?.(t)) { const now = world.time || 0; if (now >= (t._nextSolidRelaxAt || 0)) { const jitter = stableUnit(t.id || t.familyKey || 0, "solid-relax") * 0.45; t._nextSolidRelaxAt = now + 0.85 + jitter; world.resolveFenceCollision(t); } } } return { done: false }; } global.TarinaiMovementUpdateStep = Object.freeze({ update, markSleepExternalMotion, sleepExternalMotionActive, wakeSleepingFromExternalMotion }); })(typeof window !== "undefined" ? window : globalThis);