tarinai/js/collision_response_system.js
2026-06-30 10:57:06 +09:00

386 lines
22 KiB
JavaScript

"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 resolveBallBallCollisions(worldRef, dt) {
if (!worldRef) return undefined;
if ((worldRef.itemCounts?.ball || 0) < 2) return;
const now = worldRef.time || 0;
if (!worldRef.ballCollisionMemo) worldRef.ballCollisionMemo = new Map();
const balls = worldRef.itemsOfType?.("ball") || (worldRef.items || []).filter(it => it && !it.dead && it.type === "ball");
for (const a of balls) {
if (!a || a.dead || a.type !== "ball") 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 || b.type !== "ball") 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);
const restitution = 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)" }));
}
}
}
function resolveBallInteractions(worldRef, dt) {
if (!worldRef) return undefined;
if (!worldRef.itemCounts?.ball) return;
for (const ball of (worldRef.itemsOfType ? worldRef.itemsOfType("ball") : worldRef.items)) {
if (!ball || ball.dead || ball.type !== "ball") 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 || t.state === "sleep" || worldRef.isTarinaiHiddenInNestBox(t)) continue;
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 < 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 = globalThis.TarinaiItemDynamicPinSystem.transferBallPinsToTarinai(ball, t, worldRef) || 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}にボールから鋲が移った。`, "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 isDangerous = 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 });
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;
}
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 impulse = clamp(48 + relativeSpeed * 0.72 + (playIntent ? 52 : 0) + (playful ? 24 : 0), 48, isDangerous ? 360 : 260);
ball.vx = (ball.vx || 0) * (isDangerous ? 0.78 : 0.92) + nx * impulse + (t.vx || 0) * 0.52;
ball.vy = (ball.vy || 0) * (isDangerous ? 0.78 : 0.92) + ny * impulse + (t.vy || 0) * 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 = 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\u30dc\u30fc\u30eb\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: "rgba(170,210,95,0.50)" }));
}
}
}
function resolveTarinaiHighSpeedCollisions(worldRef, dt) {
if (!worldRef) return undefined;
const threshold = PHYSICAL_DAMAGE_SPEED_THRESHOLD;
const now = worldRef.time || 0;
if (!worldRef.tarinaiCollisionMemo) worldRef.tarinaiCollisionMemo = new Map();
const live = Array.from(worldRef.tarinai || [])
.filter(t => t && !t.dead && !worldRef.isTarinaiHiddenInNestBox(t))
.sort((a, b) => String(a.id || "").localeCompare(String(b.id || "")));
const candidates = [];
const maxCandidates = Math.max(24, Math.min(320, live.length * 6));
for (let i = 0; i < live.length; i += 1) {
const a = live[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;
for (let j = 0; j < live.length; j += 1) {
const b = live[j];
if (!b || b === a) continue;
const key = a.id < b.id ? `${a.id}:${b.id}` : `${b.id}:${a.id}`;
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(8, Math.min(64, Number(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({
resolveBallBallCollisions,
resolveBallInteractions,
resolveTarinaiHighSpeedCollisions,
});
})(typeof window !== "undefined" ? window : globalThis);