tarinai/js/impact_response_system.js
2026-07-03 00:45:22 +09:00

117 lines
7.3 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;
function itemDropImpact(worldRef, it) {
if (!worldRef) return undefined;
if (!it || it.dead || it.type === "water") return;
const isStone = it.type === "stone";
const isGenkotsu = it.type === "genkotsu";
const dropFallHeight = isGenkotsu ? Math.max(360, (it.r || 88) * 5.8) : 170;
const dropDuration = Math.max(0.08, Number(it.dropMax || 0.42) || 0.42);
const impactSpeed = dropFallHeight / dropDuration;
const radius = Math.max(32, Math.min(220, (it.r || 14) * 1.35 + impactSpeed * 0.055));
const damage = physicalDamageFromImpactSpeed(impactSpeed, { min: 1.2, max: isGenkotsu ? 58 : 34, scale: 8.6 });
const push = clamp((impactSpeed - 120) * 0.62, 42, isGenkotsu ? 1040 : 260);
const reason = isGenkotsu ? "\u3052\u3093\u3053\u3064" : (isStone ? "\u77f3\u306b\u6f70\u3055\u308c\u305f" : (impactSpeed >= PHYSICAL_DAMAGE_SPEED_THRESHOLD ? "\u9ad8\u901f\u3067\u843d\u3061\u3066\u304d\u305f\u9053\u5177\u306b\u885d\u7a81" : "\u843d\u3061\u3066\u304d\u305f\u9053\u5177\u306b\u5f53\u305f\u3063\u305f"));
if (isGenkotsu) { worldRef.lastGenkotsuAt = worldRef.time || 0; it.impactFlash = 1; it.lingerTimer = Math.max(it.lingerDuration || 2, 2); worldRef.triggerGroundShake?.(2.0, 10); }
let hit = 0;
for (const t of worldRef.nearbyTarinai(it.x, it.y, radius + 54)) {
if (!t || t.dead || worldRef.isTarinaiHiddenInNestBox(t)) continue;
const d = Math.max(1, distXY(t.x, t.y, it.x, it.y));
const p = clamp(1 - d / (radius + t.radius), 0, 1);
if (p <= 0) continue;
let nx = (t.x - it.x) / d;
let ny = (t.y - it.y) / d;
if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) {
const a = deterministicAngle(worldRef, "drop-impact-overlap-normal", it, t);
nx = Math.cos(a);
ny = Math.sin(a);
}
t.vx += nx * push * (0.35 + p);
t.vy += ny * push * (0.35 + p) - Math.min(210, push * 0.22) * p;
const dealtDamage = damage * (0.35 + p * 0.65);
if (dealtDamage > 0) t.damage(dealtDamage, reason);
if (isGenkotsu) {
t.fearTimer = Math.max(t.fearTimer || 0, 4.0 + p * 2.2);
if (typeof applyNeedShock === "function") applyNeedShock(t, { safety: 18 + p * 30, health: 8 + p * 15 });
t.adjustPersonality?.("openness", -(0.020 + p * 0.030), "after being struck by genkotsu.");
t.thought = "\u3052\u3093\u3053\u3064\u304c\u3053\u308f\u3044";
} else {
t.thought = isStone ? "\u77f3\u304c\u843d\u3061\u3066\u304d\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b" : "\u843d\u3061\u3066\u304d\u305f\u9053\u5177\u306b\u5f53\u305f\u3063\u3066\u6df7\u4e71\u3057\u3066\u3044\u308b";
}
t.hurtTimer = Math.max(t.hurtTimer, isGenkotsu ? 3.4 : (isStone ? 2.7 : 1.25));
t.fallTimer = Math.max(t.fallTimer, isGenkotsu ? 0.86 : (isStone ? 0.62 : 0.26));
t.fallMax = Math.max(t.fallMax || t.fallTimer, t.fallTimer);
t.fallDir = nx >= 0 ? 1 : -1;
hit += 1;
}
let ballHit = 0;
if (isGenkotsu && (worldRef.itemCounts?.ball || worldRef.itemCounts?.balloon)) {
const ballRange = radius + Math.max(72, (it.r || 88) * 0.78);
const balls = worldRef.nearbyItems?.(it.x, it.y, ballRange + 42, true) || worldRef.items || [];
for (const ball of balls) {
if (!ball || ball.dead || (ball.type !== "ball" && ball.type !== "balloon")) continue;
const d = Math.max(1, distXY(ball.x, ball.y, it.x, it.y));
const p = clamp(1 - d / (ballRange + (ball.r || 18)), 0, 1);
if (p <= 0) continue;
if (ball.type === "balloon") {
globalThis.TarinaiItemDynamicBallSystem?.popBalloon?.(ball, worldRef, it);
ballHit += 1;
continue;
}
let nx = (ball.x - it.x) / d;
let ny = (ball.y - it.y) / d;
if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) {
const a = deterministicAngle(worldRef, "genkotsu-ball-impact-normal", it, ball);
nx = Math.cos(a);
ny = Math.sin(a);
}
const ballPush = clamp(push * (0.44 + p * 0.86), 160, 1080);
ball.prevX = Number.isFinite(ball.x) ? ball.x : (ball.prevX || 0);
ball.prevY = Number.isFinite(ball.y) ? ball.y : (ball.prevY || 0);
ball.vx = clamp((ball.vx || 0) + nx * ballPush, -1180, 1180);
ball.vy = clamp((ball.vy || 0) + ny * ballPush - Math.min(180, ballPush * 0.12) * p, -1180, 1180);
ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx >= 0 ? 1 : -1) * (10 + p * 12), -48, 48);
ball.lastPokedAt = worldRef.time || 0;
ballHit += 1;
worldRef.effects?.push(new Effect("ring", ball.x, ball.y, { size: Math.max(18, (ball.r || 18) * 1.1), life: 0.20, color: "rgba(255,255,255,0.52)" }));
}
if (ballHit > 0) worldRef.markSpatialDirty?.("genkotsu-ball-impact");
}
const antHit = damage > 0 ? worldRef.damageAntsInRadius(it.x, it.y, radius + 54, p => damage * (0.35 + p * 0.65), reason, {
push: p => push * (0.62 + p * 1.25),
}) : 0;
if (isGenkotsu) {
worldRef.blastZunchiFrom?.(it.x, it.y, radius + 92, 2.75, { limit: 96, effectLimit: 6 });
worldRef.blastLoosePinsFrom?.(it.x, it.y, radius + 92, it, 2.75, "genkotsu-impact");
const existingEffects = (worldRef.effects || []).length;
const ringCount = existingEffects > 180 ? 2 : 3;
const burstCount = existingEffects > 180 ? 4 : 7;
for (let r = 0; r < ringCount; r++) worldRef.effects.push(new Effect("ring", it.x, it.y, { size: 32 + r * 42, life: 0.32 + r * 0.07, color: r % 2 ? "rgba(255,203,54,0.68)" : "rgba(74,57,42,0.38)" }));
for (let i = 0; i < burstCount; i++) worldRef.effects.push(new Effect("fight", it.x + deterministicRange(worldRef, "genkotsu-impact-effect-x", -38, 38, it, i), it.y + deterministicRange(worldRef, "genkotsu-impact-effect-y", -22, 28, it, i), { size: deterministicRange(worldRef, "genkotsu-impact-effect-size", 12, 22, it, i), life: deterministicRange(worldRef, "genkotsu-impact-effect-life", 0.22, 0.42, it, i), color: "rgba(255,205,46,0.78)" }));
} else {
worldRef.effects.push(new Effect("ring", it.x, it.y, { size: isStone ? 32 : 18, life: 0.34, color: isStone ? "rgba(128,96,58,0.76)" : "rgba(174,128,70,0.52)" }));
}
if (hit > 0 || antHit > 0 || isGenkotsu) worldRef.spawnFallEffect(it.x, it.y + (it.r || 12) * 0.6, isGenkotsu ? 1.32 : (isStone ? 0.9 : 0.45));
if (isGenkotsu) audio.genkotsuImpact?.();
else if (isStone) audio.stoneImpact?.();
else if (hit > 0 || antHit > 0) audio.poke?.();
else audio.drop?.();
if (hit > 0 || antHit > 0) {
const label = isGenkotsu ? "\u3052\u3093\u3053\u3064" : (isStone ? "\u77f3" : "\u9053\u5177");
const victims = [];
if (hit > 0) victims.push(`${hit}\u5339\u306e\u305f\u308a\u306a\u3044`);
if (antHit > 0) victims.push(`${antHit}\u5339\u306e\u30a2\u30ea`);
worldRef.log(`${label}\u304c\u843d\u3061\u3001${victims.join("\u3068")}\u304c\u5dfb\u304d\u8fbc\u307e\u308c\u305f\u3002`, "accident");
}
}
global.TarinaiImpactResponseSystem = Object.freeze({
itemDropImpact,
});
})(typeof window !== "undefined" ? window : globalThis);