tarinai/js/item_dynamic_zunchi_system.js
2026-07-20 14:36:59 +09:00

199 lines
11 KiB
JavaScript

"use strict";
// Layer: entity-runtime/items/dynamic-system
// Owns zunchi item motion, high-speed collision, and lifecycle stage ticking.
// Prototype methods delegate here.
(function (global) {
function burstOwnerHitRadius(item, tarinai) {
return (tarinai?.radius || 20) * 0.72 + (item?.r || 14);
}
function updateBurstOwnerClearance(item, worldRef) {
if (!item?.achievementBurstOwnerId || item.achievementBurstClearedOwner === true || !worldRef) return false;
const owner = worldRef.liveTarinaiById?.(String(item.achievementBurstOwnerId))
|| (worldRef.tarinai || []).find(candidate => candidate && !candidate.dead && String(candidate.id || "") === String(item.achievementBurstOwnerId))
|| null;
if (!owner) return false;
const clearance = burstOwnerHitRadius(item, owner) + 3;
if (distXY(item.x, item.y, owner.x, owner.y) <= clearance) return false;
item.achievementBurstClearedOwner = true;
return true;
}
function updateMotion(item, dt, worldRef) {
if (!item || item.dead || item.type !== "zunchi") return false;
if (item.playerHeld || item._heldByPlayer) {
if (item.achievementBurstOwnerId) item.achievementBurstNeverStopped = false;
item.prevX = item.x;
item.prevY = item.y;
return true;
}
const speed = Math.hypot(item.vx || 0, item.vy || 0);
if (speed < 0.08) {
if (item.achievementBurstOwnerId) item.achievementBurstNeverStopped = false;
item.vx = 0; item.vy = 0;
global.TarinaiItemDynamicBallSystem.resolveObstacleCollisions(item, dt, worldRef);
return true;
}
if (global.TarinaiPhysics.applyKinematicItemMotion) {
global.TarinaiPhysics.applyKinematicItemMotion(item, worldRef, dt, { bounce: 0.34, frictionBase: 0.68, frictionRate: 2.4, spin: false, stopSpeed: 0.08 });
} else {
item.prevX = item.x;
item.prevY = item.y;
item.x += (item.vx || 0) * dt;
item.y += (item.vy || 0) * dt;
const p = Math.max(28, CONFIG.worldPadding || 30);
if (item.x < p) { item.x = p; item.vx = Math.abs(item.vx || 0) * 0.34; }
if (item.x > worldRef.w - p) { item.x = worldRef.w - p; item.vx = -Math.abs(item.vx || 0) * 0.34; }
if (item.y < p) { item.y = p; item.vy = Math.abs(item.vy || 0) * 0.34; }
if (item.y > worldRef.h - p) { item.y = worldRef.h - p; item.vy = -Math.abs(item.vy || 0) * 0.34; }
const slow = Math.pow(0.68, dt * 2.4);
item.vx *= slow;
item.vy *= slow;
worldRef.drawListDirty = true;
}
global.TarinaiItemDynamicBallSystem.resolveObstacleCollisions(item, dt, worldRef);
updateBurstOwnerClearance(item, worldRef);
const collisionSpeed = Math.hypot(item.vx || 0, item.vy || 0);
if (collisionSpeed < 0.08 && item.achievementBurstOwnerId) item.achievementBurstNeverStopped = false;
resolveHighSpeedTarinaiCollision(item, dt, worldRef, collisionSpeed);
item.spin = (item.spin || 0) + collisionSpeed * dt / Math.max(8, item.r || 14);
return true;
}
function resolveHighSpeedTarinaiCollision(item, dt, worldRef, speed = 0) {
if (!worldRef || speed < 140 || (item.amount || 0) <= 0) return;
const search = Math.max(38, (item.r || 14) + speed * Math.max(dt || 0.016, 0.016) + 18);
for (const t of worldRef.nearbyTarinai?.(item.x, item.y, search, true) || []) {
if (!t || t.dead || worldRef.isTarinaiHiddenInNestBox?.(t)) continue;
const d = distXY(item.x, item.y, t.x, t.y);
if (d > burstOwnerHitRadius(item, t)) continue;
const hitTarinaiIdBeforeDamage = String(t.id || t.releasedLiveId || "");
const isBurstOwner = Boolean(item.achievementBurstOwnerId && String(item.achievementBurstOwnerId) === hitTarinaiIdBeforeDamage);
// Burst zunchi spawn inside their owner. Ignore that initial overlap
// completely; only a projectile that has first cleared the owner can
// return and become a valid self-collision.
if (isBurstOwner && item.achievementBurstClearedOwner !== true) continue;
const now = worldRef.time || 0;
if ((item.lastHitTarinaiAt || {})[t.id] && item.lastHitTarinaiAt[t.id] + 0.55 > now) continue;
item.lastHitTarinaiAt = item.lastHitTarinaiAt || {};
item.lastHitTarinaiAt[t.id] = now;
const nx = speed > 0 ? (item.vx || 1) / speed : rand(-1, 1);
const ny = speed > 0 ? (item.vy || 0) / speed : rand(-1, 1);
const impulse = clamp(speed * 1.15, 150, 620);
t.vx = (t.vx || 0) + nx * impulse + rand(-20, 20);
t.vy = (t.vy || 0) + ny * impulse * 0.72 + rand(-18, 10);
if (speed >= 185) {
const damage = Math.min(18, Math.max(1, (speed - 185) / 22));
// A fatal damage call releases the tarinai live ID before control returns here.
// Capture ownership eligibility first so a valid self-zunchi death is not lost.
const eligibleSelfZunchiDeath = Boolean(
isBurstOwner
&& item.achievementBurstClearedOwner === true
&& item.achievementBurstNeverStopped !== false
);
worldRef.applyImpactDamage?.(t, damage, "\u885d\u7a81", { source: item }) || t.damage?.(damage, "\u885d\u7a81");
if (t.dead) {
globalThis.TarinaiAchievements?.recordZunchiCollisionDeath?.({ world: worldRef, tarinai: t, zunchi: item, speed });
}
if (t.dead && eligibleSelfZunchiDeath) {
globalThis.TarinaiAchievements?.recordSelfZunchiDeath?.({ world: worldRef, tarinai: t, zunchi: item, speed });
}
}
t.fallTimer = Math.max(t.fallTimer || 0, 0.85);
t.fallMax = Math.max(t.fallMax || 0.85, t.fallTimer);
t.fallDir = nx < 0 ? -1 : 1;
if (t.enterPanic) t.enterPanic({ threat: item, reason: "\u9ad8\u901f\u305a\u3093\u3061\u306b\u3076\u3064\u304b\u3063\u3066\u5439\u304d\u98db\u3093\u3067\u3044\u308b", fear: 0.55, stress: 7, stressDuration: 3.2, surpriseTimer: 0.5, cause: "fast_zunchi_hit" });
else {
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.5);
t.fearTimer = Math.max(t.fearTimer || 0, 0.55);
t.setActionState?.("panic", { target: t.panicDestination ? t.panicDestination(item) : null, reason: "\u9ad8\u901f\u305a\u3093\u3061\u306b\u3076\u3064\u304b\u3063\u3066\u5439\u304d\u98db\u3093\u3067\u3044\u308b", wake: true });
if (t.addStress) t.addStress(7, { threshold: 8, duration: 3.2 });
}
worldRef.spawnFallEffect?.(t.x, t.y + (t.radius || 20) * 0.45, 0.65);
worldRef.queueEffect?.("zunchi_miasma", item.x, item.y - 4, { size: 14, life: 0.38, color: "rgba(77,92,42,0.36)" });
item.vx *= -0.18;
item.vy *= -0.18;
break;
}
}
function updateBurningLifecycle(item, dt, worldRef) {
if (!item || item.dead || item.type !== "zunchi" || !(item.burning || (item.burnTimer || 0) > 0.02)) return false;
item.burning = true;
item.burnTimer = Math.max(0, Number(item.burnTimer || 0) - dt);
item.spawnGrace = 0;
item.freshness = clamp((item.freshness ?? 1) - dt * 0.62, 0, 1);
item.amount = Math.max(0, Number(item.amount || 0) - dt * 30);
item.stageTimer = Math.max(0, Number(item.stageTimer || 0) + dt * 1.8);
if (item.stage === "fresh" && item.freshness < 0.45) { item.stage = "dry"; item.stageTimer = 0; }
if (item.stage === "dry" && item.freshness < 0.18) { item.stage = "decomposing"; item.stageTimer = 0; item.fertility = Math.max(item.fertility || 0, 0.18); }
if (worldRef?.effects && typeof Effect === "function" && global.deterministicChance(worldRef, "zunchi-fire-effect", dt * 1.15, item)) {
worldRef.spawnEffect("flame", item.x + deterministicRange(worldRef, "zunchi-fire-x", -5, 5, item), item.y - Math.max(4, (item.r || 14) * 0.25), {
vx: deterministicRange(worldRef, "zunchi-fire-vx", -3, 3, item),
vy: deterministicRange(worldRef, "zunchi-fire-vy", -10, -4, item),
size: deterministicRange(worldRef, "zunchi-fire-size", 6, 12, item),
life: deterministicRange(worldRef, "zunchi-fire-life", 0.18, 0.34, item),
color: "rgba(236,46,24,0.72)",
});
}
if (item.burnTimer <= 0.01 || (item.amount || 0) <= 0.5) {
item.burning = false;
item.burnTimer = 0;
item.amount = 0;
worldRef?.queueEffect?.("zunchi_miasma", item.x, item.y - 4, { size: Math.max(13, (item.r || 15) * 1.25), life: 0.42, color: "rgba(64,44,34,0.44)" });
}
worldRef?.markTerrainDirtyAt?.(item.x, item.y, Math.max(item.r || 15, 28), "zunchi-burning");
worldRef?.markItemBucketsDirty?.("zunchi-burning");
worldRef?.markSpatialDirty?.("zunchi-burning");
worldRef.drawListDirty = true;
return true;
}
function updateLifecycle(item, dt, worldRef) {
if (!item || item.dead || item.type !== "zunchi") return false;
updateBurningLifecycle(item, dt, worldRef);
const rainy = worldRef.weather === "light_rain";
const rainBoost = rainy ? 1.28 : 1;
const groundId = worldRef?.groundType || "soil";
const stageMul = Math.max(0.1, Number(global.TarinaiGround.zunchiStageMultiplier(groundId) || 1) || 1);
const groundDecayMul = Math.max(0.1, Number(global.TarinaiGround.zunchiDecayMultiplier(groundId) || 1) || 1);
const decayBoost = (rainy ? 1.35 : 1) * groundDecayMul;
let waterBoost = 0;
const nearby = worldRef.nearbyItems ? worldRef.nearbyItems(item.x, item.y, 84) : (worldRef.items || []);
for (const it of nearby) {
if (it.type !== "water") continue;
waterBoost += clamp(1 - distXY(item.x, item.y, it.x, it.y) / 80, 0, 1);
}
item.stageTimer += dt * 0.70 * stageMul * (rainBoost + waterBoost * 0.65);
const decayDt = ((item.spawnGrace || 0) > 0 && groundDecayMul <= 1.01) ? 0 : dt;
if (item.stage === "fresh") {
item.freshness = clamp(1 - item.stageTimer / 110, 0, 1);
item.amount -= decayDt * 0.018 * decayBoost;
if (item.stageTimer > 110) { item.stage = "dry"; item.stageTimer = 0; }
} else if (item.stage === "dry") {
item.freshness = clamp(0.55 - item.stageTimer / 220, 0.20, 0.55);
item.amount -= decayDt * 0.032 * decayBoost;
if (item.stageTimer > 150) { item.stage = "decomposing"; item.stageTimer = 0; item.fertility = 0.35; }
} else if (item.stage === "decomposing") {
item.fertility = clamp(item.fertility + dt * 0.015, 0, 1);
item.amount -= decayDt * 0.046 * decayBoost;
if (item.stageTimer > 190) { item.stage = "fertile_soil"; item.stageTimer = 0; item.amount = Math.min(item.amount, 140); }
} else if (item.stage === "fertile_soil") {
item.fertility = clamp(1 - item.stageTimer / 360, 0, 1);
item.amount -= decayDt * 0.090 * decayBoost;
}
if ((item.amount || 0) <= 0.5) {
item.amount = 0;
worldRef?.queueEffect?.("zunchi_miasma", item.x, item.y - 4, { size: Math.max(12, (item.r || 15) * 1.15), life: 0.34, color: "rgba(64,44,34,0.34)" });
worldRef?.markTerrainDirtyAt?.(item.x, item.y, Math.max(item.r || 15, 28), "zunchi-decay-out");
worldRef?.markItemBucketsDirty?.("zunchi-decay-out");
worldRef?.markSpatialDirty?.("zunchi-decay-out");
if (worldRef) worldRef.drawListDirty = true;
}
return true;
}
global.TarinaiItemDynamicZunchiSystem = Object.freeze({ updateMotion, updateLifecycle });
})(typeof window !== "undefined" ? window : globalThis);