386 lines
22 KiB
JavaScript
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);
|