230 lines
12 KiB
JavaScript
230 lines
12 KiB
JavaScript
"use strict";
|
|
|
|
// Layer: entity-runtime/items/dynamic-system
|
|
// Owns dynamic tool behavior for rotator, reciprocator, firecracker, genkotsu,
|
|
// links, and active tool ticking. Item.prototype methods delegate here for
|
|
// direct Item.prototype calls; lifecycle dispatch also calls this boundary directly.
|
|
(function (global) {
|
|
function fireApi() {
|
|
const api = global.TarinaiFireRuntimeSystem;
|
|
if (!api) throw new Error("TarinaiFireRuntimeSystem must be loaded before fire item behavior is used");
|
|
return api;
|
|
}
|
|
|
|
function robotApi() {
|
|
const api = global.TarinaiRobotCleanerSystem;
|
|
if (!api) throw new Error("TarinaiRobotCleanerSystem must be loaded before robot cleaner behavior is used");
|
|
return api;
|
|
}
|
|
|
|
function updateGenkotsu(item, dt) {
|
|
item.impactFlash = Math.max(0, (item.impactFlash || 0) - dt);
|
|
if (item.dropImpactDone) {
|
|
item.lingerTimer = Math.max(0, (item.lingerTimer || 0) - dt);
|
|
if ((item.lingerTimer || 0) <= 0) item.amount = 0;
|
|
} else {
|
|
item.amount = Math.max(item.amount || 0, 100);
|
|
}
|
|
}
|
|
|
|
function updateRotator(item, dt, worldRef) {
|
|
return Boolean(global.TarinaiMechanicalSystem.updateRotator(item, dt, worldRef));
|
|
}
|
|
|
|
function updateReciprocator(item, dt, worldRef) {
|
|
return Boolean(global.TarinaiMechanicalSystem.updateReciprocator(item, dt, worldRef));
|
|
}
|
|
|
|
function updatePoisonBlock(item, dt, worldRef) {
|
|
return Boolean(global.TarinaiMechanicalSystem.updatePoisonBlock(item, dt, worldRef));
|
|
}
|
|
|
|
|
|
const STICKY_BOMB_PANIC_TEXT = "\u7c98\u7740\u30dc\u30e0\u304c\u4ed8\u3044\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b\u3002";
|
|
const STICKY_BOMB_FUSE_SECONDS = 12.0;
|
|
const STICKY_BOMB_BLAST_SCALE = 0.68; // identical to TOOL_SIZE_SCALES.small
|
|
|
|
function tarinaiById(worldRef, id = "") {
|
|
return (worldRef?.tarinai || []).find(t => t && !t.dead && t.id === id) || null;
|
|
}
|
|
|
|
function panicStickyBombCarrier(t, item, worldRef, force = false) {
|
|
if (!t || t.dead) return false;
|
|
const now = Number(worldRef?.time || t.world?.time || 0) || 0;
|
|
if (!force && now < Number(item.stickyBombPanicCooldown || 0)) return false;
|
|
item.stickyBombPanicCooldown = now + 0.55;
|
|
t.thought = STICKY_BOMB_PANIC_TEXT;
|
|
t.lastPanicCause = "sticky_bomb";
|
|
t.lastPanicDetail = STICKY_BOMB_PANIC_TEXT;
|
|
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.38);
|
|
t.fearTimer = Math.max(t.fearTimer || 0, 1.15);
|
|
if (typeof t.enterPanic === "function") {
|
|
t.enterPanic({ threat: item, target: item, reason: STICKY_BOMB_PANIC_TEXT, fear: 1.45, wake: true, cause: "sticky_bomb", surpriseTimer: 0.45, awakeLockTimer: 0.28, bubble: "!!", bubbleCooldown: 0.55 });
|
|
} else {
|
|
t.setActionState?.("panic", { target: item, reason: STICKY_BOMB_PANIC_TEXT, wake: true, sleeping: false });
|
|
}
|
|
if (t.addStress) t.addStress(1.8, { threshold: 5 });
|
|
return true;
|
|
}
|
|
|
|
function attachStickyBomb(item, carrier, worldRef, reason = "attach") {
|
|
if (!item || !carrier || carrier.dead) return false;
|
|
if (item.stickyBombCarrierId === carrier.id) return false;
|
|
const old = tarinaiById(worldRef, item.stickyBombCarrierId);
|
|
item.stickyBombCarrierId = carrier.id;
|
|
item.stickyBombTransferCooldown = 0.42;
|
|
item.vx = 0;
|
|
item.vy = 0;
|
|
item.x = carrier.x;
|
|
item.y = carrier.y - Math.max(10, (carrier.radius || 20) * 0.55);
|
|
item.prevX = item.x;
|
|
item.prevY = item.y;
|
|
item.noStickUntil = 0;
|
|
panicStickyBombCarrier(carrier, item, worldRef, true);
|
|
if (old && old !== carrier) {
|
|
old.surpriseTimer = Math.max(old.surpriseTimer || 0, 0.18);
|
|
old.thought = "\u7c98\u7740\u30dc\u30e0\u304c\u79fb\u3063\u305f";
|
|
if (reason === "transfer") global.TarinaiAchievements?.recordStickyBombPass?.(item, { world: worldRef, from: old, to: carrier });
|
|
}
|
|
if (worldRef?.effects && typeof Effect !== "undefined") {
|
|
worldRef.effects.push(new Effect("ring", item.x, item.y, { size: Math.max(16, (item.r || 16) * 1.45), life: 0.18, color: reason === "transfer" ? "rgba(255,210,72,0.52)" : "rgba(255,128,78,0.48)" }));
|
|
}
|
|
worldRef?.markItemBucketsDirty?.("sticky-bomb-attach");
|
|
worldRef?.markSpatialDirty?.("sticky-bomb-attach");
|
|
return true;
|
|
}
|
|
|
|
function explodeStickyBomb(item, worldRef) {
|
|
if (!item || item.dead) return false;
|
|
item.stickyBombCarrierId = "";
|
|
item.stickyBombTransferCooldown = 0;
|
|
item.blastScale = STICKY_BOMB_BLAST_SCALE;
|
|
if (worldRef?.explodeFirecracker) {
|
|
return Boolean(worldRef.explodeFirecracker(item));
|
|
}
|
|
item.amount = 0;
|
|
item.hp = 0;
|
|
audio.explode?.();
|
|
worldRef?.log?.("\u7c98\u7740\u30dc\u30e0\u304c\u7206\u767a\u3057\u305f\u3002", "accident");
|
|
return true;
|
|
}
|
|
|
|
function updateStickyBomb(item, dt, worldRef) {
|
|
if (!item || item.dead || item.type !== "sticky_bomb") return false;
|
|
const step = Math.max(0.016, Number(dt || 0.016) || 0.016);
|
|
item.fuseTimer = Math.max(0, Number(item.fuseTimer ?? STICKY_BOMB_FUSE_SECONDS) - step);
|
|
item.stickyBombTransferCooldown = Math.max(0, Number(item.stickyBombTransferCooldown || 0) - step);
|
|
item.spin = (Number(item.spin || 0) || 0) + (Number(item.spinVelocity || 0) || 0) * step;
|
|
if (item.fuseTimer <= 0) return explodeStickyBomb(item, worldRef);
|
|
|
|
const carrier = tarinaiById(worldRef, item.stickyBombCarrierId);
|
|
if (carrier) {
|
|
const side = Number(carrier.facing || carrier.faceDir || 1) || 1;
|
|
item.x = carrier.x + side * Math.max(6, (carrier.radius || 20) * 0.32);
|
|
item.y = carrier.y - Math.max(10, (carrier.radius || 20) * 0.62);
|
|
item.prevX = item.x;
|
|
item.prevY = item.y;
|
|
item.vx = 0;
|
|
item.vy = 0;
|
|
panicStickyBombCarrier(carrier, item, worldRef, false);
|
|
if (item.stickyBombTransferCooldown <= 0) {
|
|
let best = null;
|
|
let bestD = Infinity;
|
|
for (const other of worldRef?.nearbyTarinai?.(carrier.x, carrier.y, Math.max(72, (carrier.radius || 20) * 3.0), true) || []) {
|
|
if (!other || other.dead || other === carrier || other.id === carrier.id) continue;
|
|
const d = distXY(carrier.x, carrier.y, other.x, other.y);
|
|
if (d <= Math.max(30, (carrier.radius || 18) + (other.radius || 18) + 10) && d < bestD) { best = other; bestD = d; }
|
|
}
|
|
if (best) attachStickyBomb(item, best, worldRef, "transfer");
|
|
}
|
|
} else {
|
|
item.stickyBombCarrierId = "";
|
|
item.prevX = item.x;
|
|
item.prevY = item.y;
|
|
item.x += (Number(item.vx || 0) || 0) * step;
|
|
item.y += (Number(item.vy || 0) || 0) * step;
|
|
item.vx *= Math.pow(0.65, step);
|
|
item.vy *= Math.pow(0.65, step);
|
|
const p = Math.max(28, CONFIG.worldPadding || 30);
|
|
item.x = clamp(item.x, p, (worldRef?.w || 1) - p);
|
|
item.y = clamp(item.y, p, (worldRef?.h || 1) - p);
|
|
let best = null;
|
|
let bestD = Infinity;
|
|
for (const t of worldRef?.nearbyTarinai?.(item.x, item.y, Math.max(72, (item.r || 16) + 56), true) || []) {
|
|
if (!t || t.dead) continue;
|
|
const d = distXY(item.x, item.y, t.x, t.y);
|
|
if (d <= Math.max(34, (item.r || 16) + (t.radius || 18) + 8) && d < bestD) { best = t; bestD = d; }
|
|
}
|
|
if (best) attachStickyBomb(item, best, worldRef, "attach");
|
|
}
|
|
if (worldRef?.effects && typeof Effect !== "undefined" && item.fuseTimer < Math.max(1.2, (item.fuseMax || STICKY_BOMB_FUSE_SECONDS) * 0.35) && deterministicChance(worldRef, "sticky-bomb-spark", step * 5.8, item, Math.floor((worldRef.time || 0) * 20))) {
|
|
worldRef.effects.push(new Effect("fight", item.x, item.y - (item.r || 16) * 0.55, { size: 4 + Math.random() * 4, life: 0.16, color: "rgba(255,224,92,0.78)", vy: -20 }));
|
|
}
|
|
worldRef && (worldRef.drawListDirty = true);
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
function updateFlexibleLink(item, dt, worldRef) {
|
|
global.TarinaiPhysicsBodySystem?.applyConstraintState?.(item, item?.physicsConstraint);
|
|
return Boolean(global.TarinaiConstraintSystem.updateFlexibleLink(item, dt, worldRef));
|
|
}
|
|
|
|
function updateRigidLink(item, dt, worldRef) {
|
|
global.TarinaiPhysicsBodySystem?.applyConstraintState?.(item, item?.physicsConstraint);
|
|
return Boolean(global.TarinaiConstraintSystem.updateRigidLink(item, dt, worldRef));
|
|
}
|
|
|
|
function update(item, dt, worldRef) {
|
|
if (!item || item.dead) return false;
|
|
if (item.type === "firecracker") return fireApi().updateFirecracker(item, dt, worldRef);
|
|
if (item.type === "flame_firecracker") return fireApi().updateFlameFirecracker(item, dt, worldRef);
|
|
if (item.type === "sticky_bomb") return updateStickyBomb(item, dt, worldRef);
|
|
if (item.type === "fire") return fireApi().updateFire(item, dt, worldRef);
|
|
if (item.type === "nest_box") return fireApi().updateNestBoxFire(item, dt, worldRef);
|
|
if (item.type === "genkotsu") return updateGenkotsu(item, dt, worldRef);
|
|
if (item.type === "rotator") return updateRotator(item, dt, worldRef);
|
|
if (item.type === "poison_block") return updatePoisonBlock(item, dt, worldRef);
|
|
if (item.type === "reciprocator") return updateReciprocator(item, dt, worldRef);
|
|
if (item.type === "robot_cleaner") return robotApi().updateRobotCleaner(item, dt, worldRef);
|
|
if (item.type === "rope") return updateFlexibleLink(item, dt, worldRef);
|
|
if (item.type === "rod") return updateRigidLink(item, dt, worldRef);
|
|
if (item.type === "spring") {
|
|
global.TarinaiPhysicsBodySystem?.applyConstraintState?.(item, item?.physicsConstraint);
|
|
return global.TarinaiConstraintSystem?.updateSpringLink?.(item, dt, worldRef) ?? false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
global.TarinaiItemDynamicToolSystem = Object.freeze({
|
|
update,
|
|
robotCleanerDefaultMask: (...args) => robotApi().robotCleanerDefaultMask(...args),
|
|
robotCleanerMask: (...args) => robotApi().robotCleanerMask(...args),
|
|
setRobotCleanerMask: (...args) => robotApi().setRobotCleanerMask(...args),
|
|
robotCleanerHasTarget: (...args) => robotApi().robotCleanerHasTarget(...args),
|
|
robotCleanerHighSpeed: (...args) => robotApi().robotCleanerHighSpeed(...args),
|
|
setRobotCleanerHighSpeed: (...args) => robotApi().setRobotCleanerHighSpeed(...args),
|
|
robotCleanerEffectiveSpeed: (...args) => robotApi().robotCleanerEffectiveSpeed(...args),
|
|
robotTargetLabel: (...args) => robotApi().robotTargetLabel(...args),
|
|
get robotCleanerTargetBits() { return robotApi().robotCleanerTargetBits; },
|
|
get robotCleanerTargetOrder() { return robotApi().robotCleanerTargetOrder; },
|
|
get robotCleanerMoveSpeed() { return robotApi().robotCleanerMoveSpeed; },
|
|
get robotCleanerHighMoveSpeed() { return robotApi().robotCleanerHighMoveSpeed; },
|
|
get fireBurningText() { return fireApi().fireBurningText; },
|
|
get fireSeenText() { return fireApi().fireSeenText; },
|
|
fireTempMultiplier: (...args) => fireApi().fireTempMultiplier(...args),
|
|
contactWaterDrop: (...args) => fireApi().contactWaterDrop(...args),
|
|
consumeWaterForFire: (...args) => fireApi().consumeWaterForFire(...args),
|
|
panicTarinaiFromFire: (...args) => fireApi().panicTarinaiFromFire(...args),
|
|
igniteAntByFire: (...args) => fireApi().igniteAntByFire(...args),
|
|
igniteGrassByFire: (...args) => fireApi().igniteGrassByFire(...args),
|
|
igniteNestBoxByFire: (...args) => fireApi().igniteNestBoxByFire(...args),
|
|
extinguishNestBoxFire: (...args) => fireApi().extinguishNestBoxFire(...args),
|
|
updateNestBoxFire: (...args) => fireApi().updateNestBoxFire(...args),
|
|
nestBoxFireActive: (...args) => fireApi().nestBoxFireActive(...args),
|
|
spawnFireAt: (...args) => fireApi().spawnFireAt(...args),
|
|
scatterFireFrom: (...args) => fireApi().scatterFireFrom(...args),
|
|
compactWorldFires: (...args) => fireApi().compactWorldFires(...args),
|
|
});
|
|
})(typeof window !== "undefined" ? window : globalThis);
|