"use strict"; (function (global) { const ImpactCore = global.TarinaiImpactCoreSystem; const PHYSICAL_DAMAGE_SPEED_THRESHOLD = ImpactCore.PHYSICAL_DAMAGE_SPEED_THRESHOLD || 165; const physicalDamageFromImpactSpeed = ImpactCore.physicalDamageFromImpactSpeed; const itemHasAttachedLink = ImpactCore.itemHasAttachedLink; function isBallLikeType(type = "") { return type === "ball" || type === "balloon"; } function ballLikeCount(worldRef) { return (worldRef?.itemCounts?.ball || 0) + (worldRef?.itemCounts?.balloon || 0); } function ballLikeItems(worldRef) { if (!worldRef) return []; if (worldRef.itemsOfType) { const balls = worldRef.itemsOfType("ball") || []; const balloons = worldRef.itemsOfType("balloon") || []; return balls.concat(balloons); } return (worldRef.items || []).filter(it => it && !it.dead && isBallLikeType(it.type)); } function ballLikeLabel(item) { return item?.type === "balloon" ? "\u6c34\u98a8\u8239" : "\u30dc\u30fc\u30eb"; } function resolveBallBallCollisions(worldRef, dt) { if (!worldRef) return undefined; if (ballLikeCount(worldRef) < 2) return; const now = worldRef.time || 0; if (!worldRef.ballCollisionMemo) worldRef.ballCollisionMemo = new Map(); const balls = ballLikeItems(worldRef); let solved = 0; for (const a of balls) { if (!a || a.dead || !isBallLikeType(a.type)) continue; const ar = a.r || 18; const avx = a.vx || 0; const avy = a.vy || 0; const aSpeed = Math.hypot(avx, avy); const range = ar * 2 + aSpeed * Math.max(0.016, dt || 0.016) + 52; for (const b of worldRef.nearbyItems(a.x, a.y, range) || []) { if (!b || b === a || b.dead || !isBallLikeType(b.type)) continue; const key = a.id < b.id ? `${a.id}:${b.id}` : `${b.id}:${a.id}`; if (now - (worldRef.ballCollisionMemo.get(key) || -999) < 0.055) continue; const br = b.r || 18; const hitRadius = ar + br; let dx = b.x - a.x; let dy = b.y - a.y; let d = Math.hypot(dx, dy); let hitX = a.x; let hitY = a.y; if (d > hitRadius) { const ax0 = Number.isFinite(a.prevX) ? a.prevX : a.x; const ay0 = Number.isFinite(a.prevY) ? a.prevY : a.y; const bx0 = Number.isFinite(b.prevX) ? b.prevX : b.x; const by0 = Number.isFinite(b.prevY) ? b.prevY : b.y; const rvx = (a.x - ax0) - (b.x - bx0); const rvy = (a.y - ay0) - (b.y - by0); const rx = ax0 - bx0; const ry = ay0 - by0; const aa = rvx * rvx + rvy * rvy; if (aa <= 0.0001) continue; const bb = 2 * (rx * rvx + ry * rvy); const cc = rx * rx + ry * ry - hitRadius * hitRadius; const disc = bb * bb - 4 * aa * cc; if (disc < 0) continue; const u = clamp((-bb - Math.sqrt(disc)) / (2 * aa), 0, 1); const axHit = ax0 + (a.x - ax0) * u; const ayHit = ay0 + (a.y - ay0) * u; const bxHit = bx0 + (b.x - bx0) * u; const byHit = by0 + (b.y - by0) * u; dx = bxHit - axHit; dy = byHit - ayHit; d = Math.hypot(dx, dy); hitX = (axHit + bxHit) * 0.5; hitY = (ayHit + byHit) * 0.5; if (d > hitRadius + 0.5) continue; } else { hitX = (a.x + b.x) * 0.5; hitY = (a.y + b.y) * 0.5; } if (d < 0.001) { const rvx = (b.vx || 0) - (a.vx || 0); const rvy = (b.vy || 0) - (a.vy || 0); const rs = Math.hypot(rvx, rvy); if (rs > 0.001) { dx = rvx / rs; dy = rvy / rs; d = 1; } else { const ang = deterministicAngle(worldRef, "ball-ball-overlap-normal", a, b); dx = Math.cos(ang); dy = Math.sin(ang); d = 1; } } const nx = dx / d; const ny = dy / d; const rvx = (b.vx || 0) - (a.vx || 0); const rvy = (b.vy || 0) - (a.vy || 0); const relNormal = rvx * nx + rvy * ny; if (relNormal > 0 && d >= hitRadius - 0.5) continue; worldRef.ballCollisionMemo.set(key, now); solved += 1; const restitution = (a.type === "balloon" || b.type === "balloon") ? 0.58 : 0.86; const impulse = -(1 + restitution) * relNormal / 2; if (Number.isFinite(impulse)) { a.vx = (a.vx || 0) - impulse * nx; a.vy = (a.vy || 0) - impulse * ny; b.vx = (b.vx || 0) + impulse * nx; b.vy = (b.vy || 0) + impulse * ny; } const overlap = Math.max(0, hitRadius - d + 0.6); if (overlap > 0) { a.x -= nx * overlap * 0.5; a.y -= ny * overlap * 0.5; b.x += nx * overlap * 0.5; b.y += ny * overlap * 0.5; } const tangent = nx * ((a.vy || 0) - (b.vy || 0)) - ny * ((a.vx || 0) - (b.vx || 0)); a.spinVelocity = clamp((a.spinVelocity || 0) - tangent / Math.max(14, ar) * 1.8, -38, 38); b.spinVelocity = clamp((b.spinVelocity || 0) + tangent / Math.max(14, br) * 1.8, -38, 38); worldRef.effects.push(new Effect("ring", hitX, hitY, { size: Math.max(13, hitRadius * 0.38), life: 0.16, color: "rgba(255,255,255,0.44)" })); } } return solved; } function resolveBallInteractions(worldRef, dt) { if (!worldRef) return undefined; if (ballLikeCount(worldRef) < 1) return; for (const ball of ballLikeItems(worldRef)) { if (!ball || ball.dead || !isBallLikeType(ball.type)) continue; const ballSpeed = Math.hypot(ball.vx || 0, ball.vy || 0); const sweep = ballSpeed * Math.max(0.016, dt || 0.016) + 84; const range = (ball.r || 18) + 72 + sweep; const px = Number.isFinite(ball.prevX) ? ball.prevX : ball.x; const py = Number.isFinite(ball.prevY) ? ball.prevY : ball.y; const sx = ball.x - px; const sy = ball.y - py; const segLenSq = sx * sx + sy * sy; for (const t of worldRef.nearbyTarinai(ball.x, ball.y, range)) { if (!t || t.dead || worldRef.isTarinaiHiddenInNestBox(t)) continue; const sleepingContact = t.state === "sleep" || t.sleeping; let hitX = ball.x; let hitY = ball.y; if (segLenSq > 1) { const u = clamp(((t.x - px) * sx + (t.y - py) * sy) / segLenSq, 0, 1); hitX = px + sx * u; hitY = py + sy * u; } let d = Math.max(0.001, distXY(hitX, hitY, t.x, t.y)); const hitDistance = (ball.r || 18) + t.radius * 0.74; if (d > hitDistance) continue; const now = worldRef.time || 0; const repeat = ball.lastKickerId === t.id && now - (ball.lastKickedAt || -999) < 0.18; if (repeat && ballSpeed < (ball.type === "balloon" ? 180 : 300)) continue; let nx = (hitX - t.x) / d; let ny = (hitY - t.y) / d; if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) { if (ballSpeed > 0.01) { nx = (ball.vx || 0) / ballSpeed; ny = (ball.vy || 0) / ballSpeed; } else { nx = t.facingDir ? t.facingDir() : 1; ny = deterministicRange(worldRef, "ball-interaction-fallback-ny", -0.22, 0.22, ball, t); } } const transferredPins = ball.type === "ball" ? (globalThis.TarinaiItemDynamicPinSystem.transferBallPinsToTarinai(ball, t, worldRef) || 0) : 0; if (transferredPins > 0) { ball.vx = (ball.vx || 0) * 0.72 - nx * Math.min(78, Math.max(18, ballSpeed * 0.06)); ball.vy = (ball.vy || 0) * 0.72 - ny * Math.min(78, Math.max(18, ballSpeed * 0.06)); ball.spinVelocity = clamp((ball.spinVelocity || 0) * 0.72 + (nx >= 0 ? -1 : 1) * 5.2, -38, 38); ball.x = clamp(t.x + nx * (hitDistance + 2), CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding); ball.y = clamp(t.y + ny * (hitDistance + 2), CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding); ball.lastKickedAt = now; ball.lastKickerId = t.id; worldRef.effects.push(new Effect("ring", hitX, hitY, { size: Math.max(18, ball.r * 1.05), life: 0.20, color: "rgba(196,65,72,0.46)" })); if (worldRef.relationNotice(t.id, ball.id || "ball", "ball-pin-transfer", 1.2)) worldRef.log(`${t.name}\u306B\u30DC\u30FC\u30EB\u304B\u3089\u92F2\u304C\u79FB\u3063\u305F\u3002`, "accident", { participants: [t] }); continue; } const constrainedBall = itemHasAttachedLink(worldRef, ball); const frameDt = Math.max(0.016, Number(dt || 0.016) || 0.016); const moveVx = sx / frameDt; const moveVy = sy / frameDt; const effectiveVx = Math.abs(ball.vx || 0) + Math.abs(ball.vy || 0) > 0.1 ? (ball.vx || 0) : moveVx; const effectiveVy = Math.abs(ball.vx || 0) + Math.abs(ball.vy || 0) > 0.1 ? (ball.vy || 0) : moveVy; const incomingSpeed = -(effectiveVx * nx + effectiveVy * ny); const overlapPush = Math.max(0, hitDistance - d); const playIntentForPush = t.state === "play_ball" || t.target === ball; if (constrainedBall && !playIntentForPush && (incomingSpeed > 8 || overlapPush > 1.5)) { const pushImpulse = clamp(20 + incomingSpeed * 0.48 + overlapPush * 2.6, 20, 150); t.vx = clamp((t.vx || 0) - nx * pushImpulse + effectiveVx * 0.08, -360, 360); t.vy = clamp((t.vy || 0) - ny * pushImpulse + effectiveVy * 0.08, -360, 360); if (overlapPush > 0.4) { const sep = Math.min(14, overlapPush * 0.58 + 0.8); t.x = clamp((t.x || 0) - nx * sep, CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding); t.y = clamp((t.y || 0) - ny * sep, CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding); ball.x = clamp((ball.x || 0) + nx * sep * 0.28, CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding); ball.y = clamp((ball.y || 0) + ny * sep * 0.28, CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding); } ball.vx = clamp((ball.vx || effectiveVx) * 0.62 + nx * Math.min(42, pushImpulse * 0.22), -720, 720); ball.vy = clamp((ball.vy || effectiveVy) * 0.62 + ny * Math.min(42, pushImpulse * 0.22), -720, 720); ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 4.6, -38, 38); ball.lastKickedAt = now; ball.lastKickerId = t.id; t.target = null; t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.16); worldRef.markSpatialDirty?.("constrained-ball-push"); continue; } const isBalloon = ball.type === "balloon"; const objectLabel = ballLikeLabel(ball); const isDangerous = !isBalloon && ballSpeed >= 285; if (isDangerous && now - (t.lastBallDamageAt || -999) > 0.46) { const bvx = ballSpeed > 0.01 ? (ball.vx || 0) / ballSpeed : nx; const damage = physicalDamageFromImpactSpeed(ballSpeed, { min: 3.5, max: 38, scale: 18 }); t.lastBallDamageAt = now; worldRef.applyImpulse(t, (ball.vx || 0) * 0.20, (ball.vy || 0) * 0.20, { panic: true, target: { x: ball.x, y: ball.y }, fearTimer: 0.95, thought: "\u9ad8\u901f\u306e\u30dc\u30fc\u30eb\u306b\u3076\u3064\u304b\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", }); worldRef.applyImpactDamage(t, damage, "\u885d\u7a81", { source: ball }); if (t.dead && ball.type === "ball") { globalThis.TarinaiAchievements?.recordSoccerBallDeath?.({ world: worldRef, tarinai: t, ball, speed: ballSpeed, damage }); } const sensitiveHit = t.shouldApplyPersonalityBehavior?.("neuroticism", 1) ? 1.35 : (t.shouldApplyPersonalityBehavior?.("neuroticism", -1) ? 0.82 : 1.0); const stressGain = clamp(damage * 0.55 * sensitiveHit, 3, sensitiveHit > 1 ? 24 : 18); if (t.addStress) t.addStress(stressGain, { threshold: 8 }); if (t.enterPanic) t.enterPanic({ target: { x: ball.x, y: ball.y }, reason: "\u9ad8\u901f\u306e\u30dc\u30fc\u30eb\u306b\u3076\u3064\u304b\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", fear: 0.95, stress: 0, wake: true, cause: "fast_ball_hit" }); else t.setActionState?.("panic", { target: { x: ball.x, y: ball.y }, reason: "\u9ad8\u901f\u306e\u30dc\u30fc\u30eb\u306b\u3076\u3064\u304b\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", wake: true }); t.hurtTimer = Math.max(t.hurtTimer, damage > 16 ? 2.4 : 1.35); t.fallTimer = Math.max(t.fallTimer, damage > 14 ? 0.98 : 0.46); t.fallMax = Math.max(t.fallMax || 0.98, t.fallTimer); t.fallDir = bvx >= 0 ? 1 : -1; t.fearTimer = Math.max(t.fearTimer, 0.95); t.blastSpinTimer = Math.max(t.blastSpinTimer || 0, damage > 14 ? 1.0 : 0.44); t.blastSpinMax = Math.max(t.blastSpinMax || 0, t.blastSpinTimer); worldRef.spawnFallEffect(t.x, t.y + t.radius * 0.55, clamp(damage / 18, 0.55, 1.8)); worldRef.effects.push(new Effect("ring", hitX, hitY, { size: Math.max(20, ball.r * 1.25), life: 0.22, color: "rgba(210,75,65,0.50)" })); if (worldRef.relationNotice(t.id, ball.id || "ball", "fast-ball-hit", 2.8)) worldRef.log(`${t.name}\u306f\u9ad8\u901f\u306e\u30dc\u30fc\u30eb\u306b\u885d\u7a81\u3057\u3066\u5f3e\u304d\u98db\u3070\u3055\u308c\u305f\u3002`, "accident", { participants: [t] }); ball.vx = (ball.vx || 0) * 0.58 - nx * Math.min(110, ballSpeed * 0.10); ball.vy = (ball.vy || 0) * 0.58 - ny * Math.min(110, ballSpeed * 0.10); ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 8.5, -38, 38); continue; } if (sleepingContact) { const toward = (ball.vx || 0) * nx + (ball.vy || 0) * ny; const bodyImpulse = clamp(10 + Math.max(0, incomingSpeed) * 0.34 + overlapPush * 1.8, 10, 96); t.vx = clamp((t.vx || 0) - nx * bodyImpulse, -220, 220); t.vy = clamp((t.vy || 0) - ny * bodyImpulse, -220, 220); if (toward < 0) { const restitution = isBalloon ? 0.34 : 0.56; ball.vx = (ball.vx || 0) - (1 + restitution) * toward * nx; ball.vy = (ball.vy || 0) - (1 + restitution) * toward * ny; } else { ball.vx = (ball.vx || 0) + nx * Math.max(8, bodyImpulse * 0.28); ball.vy = (ball.vy || 0) + ny * Math.max(8, bodyImpulse * 0.28); } const separation = Math.max(0, overlapPush + 1.5); if (separation > 0) { ball.x = clamp((ball.x || 0) + nx * separation * 0.72, CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding); ball.y = clamp((ball.y || 0) + ny * separation * 0.72, CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding); t.x = clamp((t.x || 0) - nx * separation * 0.28, CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding); t.y = clamp((t.y || 0) - ny * separation * 0.28, CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding); } ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 4.2, -38, 38); ball.lastKickedAt = now; ball.lastKickerId = t.id; global.TarinaiMovementUpdateStep?.markSleepExternalMotion?.(t, frameDt, "ball-contact"); worldRef.markSpatialDirty?.("sleeping-ball-contact"); worldRef.effects.push(new Effect("ring", hitX, hitY, { size: Math.max(12, ball.r * 0.82), life: 0.18, color: isBalloon ? "rgba(248,122,166,0.38)" : "rgba(180,205,120,0.44)" })); continue; } const relVx = (t.vx || 0) - (ball.vx || 0); const relVy = (t.vy || 0) - (ball.vy || 0); const relativeSpeed = Math.hypot(relVx, relVy); const playIntent = t.state === "play_ball" || t.target === ball; const playful = t.shouldApplyPersonalityBehavior?.("openness", 1); const balloonPlayLikeBall = isBalloon && (playIntent || playful); const impulseBase = isBalloon && !balloonPlayLikeBall ? 34 : 48; const impulse = clamp( impulseBase + relativeSpeed * (isBalloon && !balloonPlayLikeBall ? 0.48 : 0.72) + (playIntent ? 52 : 0) + (playful ? 24 : 0), impulseBase, isDangerous ? 360 : (isBalloon && !balloonPlayLikeBall ? 165 : 260) ); ball.vx = (ball.vx || 0) * (isDangerous ? 0.78 : (isBalloon && !balloonPlayLikeBall ? 0.72 : 0.92)) + nx * impulse + (t.vx || 0) * (isBalloon && !balloonPlayLikeBall ? 0.32 : 0.52); ball.vy = (ball.vy || 0) * (isDangerous ? 0.78 : (isBalloon && !balloonPlayLikeBall ? 0.72 : 0.92)) + ny * impulse + (t.vy || 0) * (isBalloon && !balloonPlayLikeBall ? 0.32 : 0.52); ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx * (t.vy || 0) - ny * (t.vx || 0)) / 16 + impulse / Math.max(12, ball.r || 18) * (nx >= 0 ? 1 : -1), -38, 38); ball.x = clamp(t.x + nx * (hitDistance + 2), CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding); ball.y = clamp(t.y + ny * (hitDistance + 2), CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding); ball.lastKickedAt = now; ball.lastKickerId = t.id; t.vx -= nx * Math.min(22, impulse * 0.10); t.vy -= ny * Math.min(22, impulse * 0.10); t.energy = clamp(t.energy - (playIntent ? 0.20 : 0.08), 0, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(t) : (Number(t.maxEnergy) || 100)); const playRelief = playful && playIntent ? 2.8 : playful ? 1.5 : playIntent ? 0.75 : 0.20; const sensitiveBump = !isBalloon && t.shouldApplyPersonalityBehavior?.("neuroticism", 1) && !playIntent ? clamp(ballSpeed / 230, 0.6, 2.6) : 0; if (typeof applyNeedShock === "function" && sensitiveBump > playRelief) applyNeedShock(t, { safety: (sensitiveBump - playRelief) * 4 }); else if (typeof applyNeedRelief === "function") applyNeedRelief(t, { fulfill: -playRelief * 6, social: -playRelief * 2 }); if (playful && playIntent) { t.loneliness = clamp(t.loneliness - 0.9, 0, 100); t.affection = clamp(t.affection + 0.18, 0, 100); } t.goodMode = playIntent || playful ? "smile" : t.goodMode; if (playIntent && now > (t.nextPlayBubbleAt || 0)) { t.nextPlayBubbleAt = now + deterministicRange(worldRef, "ball-play-bubble-delay", 2.0, 3.8, ball, t); worldRef.spawnBubble(t.x, t.y - t.radius * 1.15, pick(["!", "\u306f\u3046", "?" ]), "rgba(70,96,50,0.76)"); } if (playful && worldRef.relationNotice(t.id, ball.id || "ball", "ball-kick", 16)) worldRef.log(`${t.name}\u306f${objectLabel}\u3092\u8ffd\u3044\u304b\u3051\u3066\u5f3e\u3044\u305f\u3002`, "observe", { participants: [t] }); worldRef.effects.push(new Effect("ring", ball.x, ball.y, { size: Math.max(12, ball.r * 0.80), life: 0.20, color: isBalloon ? "rgba(248,122,166,0.42)" : "rgba(170,210,95,0.50)" })); } } } function highSpeedThreshold() { return PHYSICAL_DAMAGE_SPEED_THRESHOLD; } function resolveTarinaiHighSpeedCollisions(worldRef, dt, opts = {}) { if (!worldRef) return undefined; const threshold = Math.max(1, Number(opts.threshold || PHYSICAL_DAMAGE_SPEED_THRESHOLD) || PHYSICAL_DAMAGE_SPEED_THRESHOLD); const now = worldRef.time || 0; if (!worldRef.tarinaiCollisionMemo) worldRef.tarinaiCollisionMemo = new Map(); const providedFastSources = Array.isArray(opts.fastSources); const liveSource = providedFastSources ? opts.fastSources : (worldRef.tarinai || []); const maxSources = Math.max(1, Math.floor(Number(opts.maxSources || Infinity) || Infinity)); const fastSources = []; const sourceLen = liveSource?.length || 0; const cursor = sourceLen > maxSources ? (Math.max(0, Math.floor(worldRef._tarinaiHighSpeedCollisionCursor || 0)) % sourceLen) : 0; for (let n = 0; n < sourceLen; n += 1) { if (fastSources.length >= maxSources) break; const t = liveSource[(cursor + n) % sourceLen]; if (!t || t.dead || worldRef.isTarinaiHiddenInNestBox(t)) continue; if (Math.hypot(t.vx || 0, t.vy || 0) >= threshold) fastSources.push(t); } if (sourceLen > maxSources) worldRef._tarinaiHighSpeedCollisionCursor = (cursor + Math.max(1, maxSources)) % sourceLen; if (!fastSources.length) return; const live = providedFastSources ? null : []; if (!providedFastSources) { for (const t of worldRef.tarinai || []) { if (t && !t.dead && !worldRef.isTarinaiHiddenInNestBox(t)) live.push(t); } if (live.length < 2) return; live.sort((a, b) => String(a.id || "").localeCompare(String(b.id || ""))); } const candidates = []; const maxCandidates = Math.max(24, Math.min(320, fastSources.length * 12)); const seenPairs = new Set(); for (let i = 0; i < fastSources.length; i += 1) { const a = fastSources[i]; const avx = a.vx || 0; const avy = a.vy || 0; const aSpeed = Math.hypot(avx, avy); if (aSpeed < threshold) continue; const ax0 = Number.isFinite(a.prevX) ? a.prevX : a.x; const ay0 = Number.isFinite(a.prevY) ? a.prevY : a.y; const ax1 = Number.isFinite(a.x) ? a.x : ax0; const ay1 = Number.isFinite(a.y) ? a.y : ay0; const asx = ax1 - ax0; const asy = ay1 - ay0; const moveDist = Math.hypot(asx, asy); const queryX = ax0 + asx * 0.5; const queryY = ay0 + asy * 0.5; const queryRadius = Math.max(140, (a.radius || 22) * 1.6 + moveDist + threshold * Math.max(0.016, Number(dt || 0.016) || 0.016) + 96); const nearby = typeof worldRef.nearbyTarinai === "function" ? (worldRef.nearbyTarinai(queryX, queryY, queryRadius, true) || []) : (live || worldRef.tarinai || []); for (const b of nearby) { if (!b || b === a || b.dead || worldRef.isTarinaiHiddenInNestBox(b)) continue; const key = a.id < b.id ? `${a.id}:${b.id}` : `${b.id}:${a.id}`; if (seenPairs.has(key)) continue; seenPairs.add(key); if (now - (worldRef.tarinaiCollisionMemo.get(key) || -999) < 0.42) continue; const bvx = b.vx || 0; const bvy = b.vy || 0; const relVx = avx - bvx; const relVy = avy - bvy; const relSpeed = Math.hypot(relVx, relVy); if (relSpeed < threshold) continue; const bx0 = Number.isFinite(b.prevX) ? b.prevX : b.x; const by0 = Number.isFinite(b.prevY) ? b.prevY : b.y; const bx1 = Number.isFinite(b.x) ? b.x : bx0; const by1 = Number.isFinite(b.y) ? b.y : by0; const bsx = bx1 - bx0; const bsy = by1 - by0; const rx0 = ax0 - bx0; const ry0 = ay0 - by0; const rvx = asx - bsx; const rvy = asy - bsy; const relMoveSq = rvx * rvx + rvy * rvy; const u = relMoveSq > 0.001 ? clamp(-(rx0 * rvx + ry0 * rvy) / relMoveSq, 0, 1) : 1; const cx = rx0 + rvx * u; const cy = ry0 + rvy * u; const d = Math.max(0.001, Math.hypot(cx, cy)); const hitDistance = (a.radius || 22) * 0.74 + (b.radius || 22) * 0.74; if (d > hitDistance) continue; const score = (1 - u) * 4 + Math.max(0, hitDistance - d) * 0.12 + Math.min(5, relSpeed / 120); candidates.push({ a, b, key, relSpeed, u, cx, cy, d, hitDistance, score }); if (candidates.length >= maxCandidates) break; } if (candidates.length >= maxCandidates) break; } candidates.sort((p, q) => (p.u - q.u) || (q.score - p.score) || String(p.key).localeCompare(String(q.key))); const maxImpacts = Math.max(1, Math.min(64, Number(opts.maxImpacts || worldRef.maxTarinaiCollisionImpacts || 32) || 32)); let solved = 0; for (const c of candidates) { if (solved >= maxImpacts) break; const { a, b, key, relSpeed, hitDistance } = c; if (!a || !b || a.dead || b.dead) continue; if (now - (worldRef.tarinaiCollisionMemo.get(key) || -999) < 0.42) continue; worldRef.tarinaiCollisionMemo.set(key, now); solved += 1; const as = Math.hypot(a.vx || 0, a.vy || 0); const bs = Math.hypot(b.vx || 0, b.vy || 0); const impactor = as >= bs ? a : b; const target = impactor === a ? b : a; const ivx = impactor.vx || 0; const ivy = impactor.vy || 0; const impactSpeed = Math.hypot(ivx, ivy) || relSpeed || 1; let nx = impactSpeed > 0.001 ? ivx / impactSpeed : 0; let ny = impactSpeed > 0.001 ? ivy / impactSpeed : 0; if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) { const dx = target.x - impactor.x; const dy = target.y - impactor.y; const dd = Math.hypot(dx, dy); if (dd > 0.001) { nx = dx / dd; ny = dy / dd; } else { const angle = (stableUnit(key, "tarinai-collision-normal")) * Math.PI * 2; nx = Math.cos(angle); ny = Math.sin(angle); } } const overlap = Math.max(0, hitDistance - Math.hypot((a.x || 0) - (b.x || 0), (a.y || 0) - (b.y || 0))); if (overlap > 0.5) { const sep = Math.min(18, overlap * 0.45 + 0.8); impactor.x = clamp((impactor.x || 0) - nx * sep * 0.45, CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding); impactor.y = clamp((impactor.y || 0) - ny * sep * 0.45, CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding); target.x = clamp((target.x || 0) + nx * sep * 0.55, CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding); target.y = clamp((target.y || 0) + ny * sep * 0.55, CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding); worldRef.markSpatialDirty?.("tarinai-highspeed-separation"); } const damage = physicalDamageFromImpactSpeed(relSpeed, { min: 1.2, max: 22, scale: 26 }); worldRef.applyImpulse(target, ivx * 0.20 + nx * 28, ivy * 0.20 + ny * 28, { panic: true, target: { x: impactor.x, y: impactor.y }, fearTimer: 0.55, thought: "\u9ad8\u901f\u3067\u3076\u3064\u304b\u3063\u3066\u6df7\u4e71\u3057\u3066\u3044\u308b", }); impactor.vx = ivx * 0.72 - nx * Math.min(56, relSpeed * 0.10); impactor.vy = ivy * 0.72 - ny * Math.min(56, relSpeed * 0.10); worldRef.applyImpactDamage(target, damage, "\u885d\u7a81", { source: impactor }); if (!impactor.dead) worldRef.applyImpactDamage(impactor, damage * 0.35, "\u885d\u7a81", { source: target }); for (const t of [target, impactor]) { if (!t || t.dead) continue; if (t.enterPanic) t.enterPanic({ target: { x: impactor.x, y: impactor.y }, reason: "\u9ad8\u901f\u3067\u3076\u3064\u304b\u3063\u3066\u6df7\u4e71\u3057\u3066\u3044\u308b", fear: 0.75, wake: true, cause: "tarinai_highspeed_collision" }); else t.setActionState?.("panic", { target: { x: impactor.x, y: impactor.y }, reason: "\u9ad8\u901f\u3067\u3076\u3064\u304b\u3063\u3066\u6df7\u4e71\u3057\u3066\u3044\u308b", wake: true }); t.hurtTimer = Math.max(t.hurtTimer || 0, 0.9 + damage * 0.035); t.fallTimer = Math.max(t.fallTimer || 0, 0.36 + damage * 0.025); t.fallMax = Math.max(t.fallMax || 0, t.fallTimer); t.fallDir = nx >= 0 ? 1 : -1; t.fearTimer = Math.max(t.fearTimer || 0, 0.55); } worldRef.effects.push(new Effect("ring", (a.x + b.x) * 0.5, (a.y + b.y) * 0.5, { size: Math.max(16, hitDistance * 0.45), life: 0.20, color: "rgba(210,75,65,0.40)" })); if (worldRef.relationNotice(target.id, impactor.id, "tarinai-highspeed-collision", 2.6)) worldRef.log(`${target.name}\u306f\u9ad8\u901f\u306e${impactor.name}\u306b\u885d\u7a81\u3057\u3066\u5f3e\u304d\u98db\u3070\u3055\u308c\u305f\u3002`, "accident", { participants: [target, impactor] }); } if (solved && worldRef.spatialDirty) worldRef.rebuildSpatial?.(true, "post-tarinai-highspeed-collision"); } global.TarinaiCollisionResponseSystem = Object.freeze({ highSpeedThreshold, resolveBallBallCollisions, resolveBallInteractions, resolveTarinaiHighSpeedCollisions, }); })(typeof window !== "undefined" ? window : globalThis);