409 lines
18 KiB
JavaScript
409 lines
18 KiB
JavaScript
"use strict";
|
|
|
|
// Layer: entity-runtime/items/dynamic-system
|
|
// Owns dynamic tool behavior for rotator, reciprocator, firecracker, genkotsu,
|
|
// links, and static gate-fence ticking. Item.prototype methods delegate here for
|
|
// direct Item.prototype calls; lifecycle dispatch also calls this boundary directly.
|
|
(function (global) {
|
|
|
|
function updateFirecracker(item, dt, worldRef) {
|
|
item.fuseTimer = Math.max(0, (item.fuseTimer ?? 5) - dt);
|
|
if (item.fuseTimer <= 0) {
|
|
if (worldRef?.explodeFirecracker) worldRef.explodeFirecracker(item);
|
|
else item.amount = 0;
|
|
}
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
|
|
|
|
function updateFlexibleLink(item, dt, worldRef) {
|
|
return Boolean(global.TarinaiConstraintSystem.updateFlexibleLink(item, dt, worldRef));
|
|
}
|
|
|
|
function updateRigidLink(item, dt, worldRef) {
|
|
return Boolean(global.TarinaiConstraintSystem.updateRigidLink(item, dt, worldRef));
|
|
}
|
|
|
|
|
|
|
|
const ROBOT_DEFAULT_MASK = 0b000011; // zunchi + corpses and stains
|
|
const ROBOT_MOVE_SPEED = 60;
|
|
const ROBOT_HIGH_SPEED = 120;
|
|
const ROBOT_RESCAN_INTERVAL = 2.0;
|
|
const ROBOT_TARGET_BITS = Object.freeze({
|
|
zunchi: 1 << 0,
|
|
ant_corpse: 1 << 1,
|
|
tarinai: 1 << 2,
|
|
grass: 1 << 3,
|
|
grass_bed: 1 << 4,
|
|
water: 1 << 5,
|
|
});
|
|
const ROBOT_TARGET_ORDER = Object.freeze(["zunchi", "ant_corpse", "tarinai", "grass", "grass_bed", "water"]);
|
|
|
|
function robotCleanerDefaultMask() {
|
|
return ROBOT_DEFAULT_MASK;
|
|
}
|
|
|
|
function robotCleanerMask(item) {
|
|
const raw = Number(item?.robotCleanerMask);
|
|
return Number.isFinite(raw) ? (raw | 0) : ROBOT_DEFAULT_MASK;
|
|
}
|
|
|
|
function setRobotCleanerMask(item, mask = ROBOT_DEFAULT_MASK) {
|
|
if (!item || item.type !== "robot_cleaner") return false;
|
|
item.robotCleanerMask = Math.max(0, Math.min(0x3f, Number(mask) | 0));
|
|
return true;
|
|
}
|
|
|
|
function robotCleanerHighSpeed(item) {
|
|
return Boolean(item?.robotCleanerHighSpeed);
|
|
}
|
|
|
|
function setRobotCleanerHighSpeed(item, enabled = false) {
|
|
if (!item || item.type !== "robot_cleaner") return false;
|
|
item.robotCleanerHighSpeed = Boolean(enabled);
|
|
item.robotCleanerSpeed = robotCleanerEffectiveSpeed(item);
|
|
return true;
|
|
}
|
|
|
|
function robotCleanerEffectiveSpeed(item) {
|
|
return robotCleanerHighSpeed(item) ? ROBOT_HIGH_SPEED : ROBOT_MOVE_SPEED;
|
|
}
|
|
|
|
function robotCleanerHasTarget(item, key = "") {
|
|
const bit = ROBOT_TARGET_BITS[String(key || "")];
|
|
return Boolean(bit && (robotCleanerMask(item) & bit));
|
|
}
|
|
|
|
function robotTargetLabel(key = "") {
|
|
switch (String(key || "")) {
|
|
case "zunchi": return "\u305a\u3093\u3061";
|
|
case "ant_corpse": return "\u6b7b\u9ab8";
|
|
case "trace": return "\u6b7b\u9ab8";
|
|
case "splat": return "\u8840\u75d5";
|
|
case "tarinai": return "\u305f\u308a\u306a\u3044";
|
|
case "grass": return "\u8349";
|
|
case "grass_bed": return "\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9";
|
|
case "water": return "\u6c34\u6ef4";
|
|
default: return "\u5bfe\u8c61";
|
|
}
|
|
}
|
|
|
|
function normalizeRobotCleanerState(item) {
|
|
if (!item || item.type !== "robot_cleaner") return null;
|
|
if (!Number.isFinite(Number(item.robotCleanerMask))) item.robotCleanerMask = ROBOT_DEFAULT_MASK;
|
|
item.robotCleanerMask = Math.max(0, Math.min(0x3f, Number(item.robotCleanerMask) | 0));
|
|
item.amount = Math.max(Number(item.amount || 0) || 0, 999);
|
|
if (!Number.isFinite(Number(item.robotCleanerScanTimer))) item.robotCleanerScanTimer = 0;
|
|
if (!Number.isFinite(Number(item.robotCleanerCleanFlash))) item.robotCleanerCleanFlash = 0;
|
|
if (!Number.isFinite(Number(item.robotCleanerAngle))) item.robotCleanerAngle = Number(item.angle || 0) || 0;
|
|
if (!Number.isFinite(Number(item.robotCleanerWanderAngle))) item.robotCleanerWanderAngle = Number(item.robotCleanerAngle || 0) || 0;
|
|
item.robotCleanerHighSpeed = Boolean(item.robotCleanerHighSpeed);
|
|
item.robotCleanerSpeed = robotCleanerEffectiveSpeed(item);
|
|
return item;
|
|
}
|
|
|
|
function robotTargetAlive(target) {
|
|
if (!target) return false;
|
|
return !target.dead && Number.isFinite(Number(target.x)) && Number.isFinite(Number(target.y));
|
|
}
|
|
|
|
function robotTargetBlocked(item, target, worldRef) {
|
|
if (!item || !target || !worldRef) return false;
|
|
return Boolean(worldRef.targetBlockedByObstacle?.(item, target, Math.max(18, (item.r || 22) * 0.65)));
|
|
}
|
|
|
|
function currentRobotTarget(item, worldRef) {
|
|
const key = String(item?.robotCleanerTargetType || "");
|
|
const id = String(item?.robotCleanerTargetId || "");
|
|
if (!key || !id || !worldRef) return null;
|
|
if (key === "tarinai") {
|
|
const t = (worldRef.tarinai || []).find(t => t && t.id === id && !t.dead);
|
|
return robotTargetAlive(t) && !robotTargetBlocked(item, t, worldRef) ? t : null;
|
|
}
|
|
const found = (worldRef.items || []).find(it => it && it.id === id && !it.dead && (
|
|
it.type === key || (key === "ant_corpse" && (it.type === "trace" || it.type === "splat"))
|
|
));
|
|
return robotTargetAlive(found) && !robotTargetBlocked(item, found, worldRef) ? found : null;
|
|
}
|
|
|
|
function robotTargetCandidates(item, worldRef) {
|
|
const out = [];
|
|
if (!worldRef || !item) return out;
|
|
const mask = robotCleanerMask(item);
|
|
worldRef.ensureItemBuckets?.("robot-cleaner-scan");
|
|
for (const key of ROBOT_TARGET_ORDER) {
|
|
const bit = ROBOT_TARGET_BITS[key];
|
|
if (!(mask & bit)) continue;
|
|
if (key === "tarinai") {
|
|
for (const t of worldRef.tarinai || []) {
|
|
if (!t || t.dead) continue;
|
|
if (worldRef.isTarinaiHiddenInNestBox?.(t)) continue;
|
|
if (robotTargetBlocked(item, t, worldRef)) continue;
|
|
out.push({ target: t, type: key });
|
|
}
|
|
continue;
|
|
}
|
|
const buckets = key === "ant_corpse"
|
|
? ["ant_corpse", "trace", "splat"].flatMap(type => worldRef.itemsOfType?.(type) || (worldRef.items || []).filter(it => it && it.type === type))
|
|
: (worldRef.itemsOfType?.(key) || (worldRef.items || []).filter(it => it && it.type === key));
|
|
for (const it of buckets || []) {
|
|
if (!it || it.dead || it === item) continue;
|
|
if (robotTargetBlocked(item, it, worldRef)) continue;
|
|
out.push({ target: it, type: key });
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function retargetRobotCleaner(item, worldRef, force = false) {
|
|
const current = currentRobotTarget(item, worldRef);
|
|
if (current && !force) return current;
|
|
item.robotCleanerTargetId = "";
|
|
item.robotCleanerTargetType = "";
|
|
let best = null;
|
|
let bestType = "";
|
|
let bestD = Infinity;
|
|
for (const rec of robotTargetCandidates(item, worldRef)) {
|
|
const target = rec.target;
|
|
if (!robotTargetAlive(target)) continue;
|
|
const d = distXY(item.x, item.y, target.x, target.y);
|
|
if (d < bestD) { best = target; bestType = rec.type; bestD = d; }
|
|
}
|
|
if (best) {
|
|
item.robotCleanerTargetId = best.id || "";
|
|
item.robotCleanerTargetType = bestType || best.type || "";
|
|
}
|
|
return best;
|
|
}
|
|
|
|
function triggerRobotPanic(item, target, worldRef, force = false) {
|
|
if (!item || !target || target.dead) return false;
|
|
const now = Number(worldRef?.time || target.world?.time || 0) || 0;
|
|
if (!force && now < Number(target.robotCleanerPanicCooldownUntil || 0)) return false;
|
|
target.robotCleanerPanicCooldownUntil = now + 0.65;
|
|
const reason = "\u30ed\u30dc\u6383\u9664\u6a5f\u304b\u3089\u9003\u3052\u3066\u3044\u308b";
|
|
if (typeof target.enterPanic === "function") {
|
|
target.enterPanic({ threat: item, reason, fear: 0.86, wake: true, cause: "robot_cleaner", surpriseTimer: 0.18, awakeLockTimer: 0.45 });
|
|
} else {
|
|
target.fearTimer = Math.max(target.fearTimer || 0, 0.86);
|
|
target.setActionState?.("panic", { target: target.panicDestination ? target.panicDestination(item, true) : null, reason, wake: true });
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function isRobotTarinaiTarget(target) {
|
|
return Boolean(target && (target.constructor?.name === "Tarinai" || (target.thought !== undefined && target.name && target.die)));
|
|
}
|
|
|
|
function robotTargetTypeForContact(item, target) {
|
|
if (!item || !target || target.dead) return "";
|
|
if (isRobotTarinaiTarget(target)) return robotCleanerHasTarget(item, "tarinai") ? "tarinai" : "";
|
|
const type = String(target.type || "");
|
|
if ((type === "ant_corpse" || type === "trace" || type === "splat") && robotCleanerHasTarget(item, "ant_corpse")) return type;
|
|
return robotCleanerHasTarget(item, type) ? type : "";
|
|
}
|
|
|
|
function robotCleanerCaptureRadius(item, target) {
|
|
return Math.max(18, (item?.r || 22) * 0.72 + (target?.r || target?.radius || 10) * 0.72);
|
|
}
|
|
|
|
function clearRobotTrackedTargetIfMatched(item, target) {
|
|
if (!item || !target) return false;
|
|
const id = String(target.id || "");
|
|
if (!id || String(item.robotCleanerTargetId || "") !== id) return false;
|
|
item.robotCleanerTargetId = "";
|
|
item.robotCleanerTargetType = "";
|
|
item.robotCleanerScanTimer = 0;
|
|
return true;
|
|
}
|
|
|
|
function cleanRobotTarget(item, target, worldRef, typeOverride = "") {
|
|
if (!item || !target || !worldRef || target.dead) return false;
|
|
const type = String(typeOverride || item.robotCleanerTargetType || target.type || "");
|
|
const label = robotTargetLabel(type);
|
|
let removed = false;
|
|
const cleanReason = "\u30ed\u30dc\u6383\u9664\u6a5f\u306b\u5438\u3044\u8fbc\u307e\u308c\u305f";
|
|
if (type === "tarinai" || isRobotTarinaiTarget(target)) {
|
|
triggerRobotPanic(item, target, worldRef, true);
|
|
if (typeof target.die === "function") {
|
|
target.die(cleanReason, { lowHealth: true });
|
|
removed = true;
|
|
} else {
|
|
target.dead = true;
|
|
target.deathReason = cleanReason;
|
|
worldRef.markDead?.(target, cleanReason);
|
|
removed = true;
|
|
}
|
|
} else {
|
|
removed = Boolean(global.TarinaiStructureLifecycle?.deleteItem?.(worldRef, target, {
|
|
reason: "robot-cleaner",
|
|
userReason: type === "grass_bed" ? "\u30ed\u30dc\u6383\u9664\u6a5f\u306b\u7247\u4ed8\u3051\u3089\u308c\u305f" : cleanReason,
|
|
wake: true,
|
|
breaker: item,
|
|
}));
|
|
if (!removed) {
|
|
target.amount = 0;
|
|
target.hp = 0;
|
|
target.dead = true;
|
|
target.removedReason = "robot-cleaner";
|
|
removed = true;
|
|
}
|
|
}
|
|
if (!removed) return false;
|
|
clearRobotTrackedTargetIfMatched(item, target);
|
|
item.robotCleanerScanTimer = 0;
|
|
item.robotCleanerCleanFlash = 0.42;
|
|
item.robotCleanerCleanCount = (Number(item.robotCleanerCleanCount || 0) || 0) + 1;
|
|
worldRef.effects?.push(new Effect("ring", target.x, target.y, { size: Math.max(16, (target.r || target.radius || 14) * 1.8), life: 0.24, color: "rgba(146,206,220,0.36)" }));
|
|
worldRef.markItemBucketsDirty?.("robot-cleaner");
|
|
worldRef.markSpatialDirty?.("robot-cleaner");
|
|
if (target.type === "trace" || target.type === "splat") worldRef.markTerrainDirtyAt?.(target.x, target.y, Math.max(target.r || target.radius || 24, 36), "robot-cleaner");
|
|
worldRef.updateItemCounts?.();
|
|
worldRef.drawListDirty = true;
|
|
if ((worldRef.time || 0) - Number(item.robotCleanerLastLogAt || -999) > 1.2) {
|
|
worldRef.log?.(`\u30ed\u30dc\u6383\u9664\u6a5f\u304c${label}\u3092\u7247\u4ed8\u3051\u305f\u3002`, "observe");
|
|
item.robotCleanerLastLogAt = worldRef.time || 0;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function cleanRobotContacts(item, worldRef) {
|
|
if (!item || !worldRef) return false;
|
|
const seen = new Set();
|
|
let changed = false;
|
|
for (const rec of robotTargetCandidates(item, worldRef)) {
|
|
const target = rec?.target;
|
|
if (!robotTargetAlive(target)) continue;
|
|
const key = String(target.id || `${rec.type}:${target.x}:${target.y}`);
|
|
if (seen.has(key)) continue;
|
|
seen.add(key);
|
|
const type = robotTargetTypeForContact(item, target) || rec.type || "";
|
|
if (!type) continue;
|
|
const capture = robotCleanerCaptureRadius(item, target);
|
|
if (distXY(item.x, item.y, target.x, target.y) <= capture) {
|
|
if (cleanRobotTarget(item, target, worldRef, type)) changed = true;
|
|
}
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
function moveRobotCleaner(item, dt, target, worldRef) {
|
|
const r = Math.max(14, Number(item.r || 22) || 22);
|
|
const speed = robotCleanerEffectiveSpeed(item);
|
|
let desiredAngle = Number(item.robotCleanerWanderAngle || 0) || 0;
|
|
let desiredSpeed = speed;
|
|
if (target && robotTargetAlive(target)) {
|
|
desiredAngle = Math.atan2((target.y || 0) - (item.y || 0), (target.x || 0) - (item.x || 0));
|
|
desiredSpeed = speed;
|
|
} else if ((item.robotCleanerWanderTimer || 0) <= 0) {
|
|
desiredAngle = global.deterministicAngle ? global.deterministicAngle(worldRef, "robot-cleaner-wander", item, Math.floor((worldRef.time || 0) * 2)) : Math.random() * Math.PI * 2;
|
|
item.robotCleanerWanderTimer = global.deterministicRange ? global.deterministicRange(worldRef, "robot-cleaner-wander-timer", 1.2, 3.2, item) : (1.2 + Math.random() * 2.0);
|
|
}
|
|
item.robotCleanerWanderTimer = Math.max(0, Number(item.robotCleanerWanderTimer || 0) - dt);
|
|
item.robotCleanerWanderAngle = desiredAngle;
|
|
const vx = Math.cos(desiredAngle) * desiredSpeed;
|
|
const vy = Math.sin(desiredAngle) * desiredSpeed;
|
|
const blend = Math.min(1, dt * 6.2);
|
|
item.vx = (Number(item.vx || 0) || 0) + (vx - (Number(item.vx || 0) || 0)) * blend;
|
|
item.vy = (Number(item.vy || 0) || 0) + (vy - (Number(item.vy || 0) || 0)) * blend;
|
|
item.prevX = item.x;
|
|
item.prevY = item.y;
|
|
item.x += item.vx * dt;
|
|
item.y += item.vy * dt;
|
|
const minX = CONFIG.worldPadding + r * 0.5;
|
|
const minY = CONFIG.worldPadding + r * 0.5;
|
|
const maxX = Math.max(minX, (worldRef.w || 0) - CONFIG.worldPadding - r * 0.5);
|
|
const maxY = Math.max(minY, (worldRef.h || 0) - CONFIG.worldPadding - r * 0.5);
|
|
let bounced = false;
|
|
if (item.x < minX) { item.x = minX; item.vx = Math.abs(item.vx) * 0.35; bounced = true; }
|
|
if (item.x > maxX) { item.x = maxX; item.vx = -Math.abs(item.vx) * 0.35; bounced = true; }
|
|
if (item.y < minY) { item.y = minY; item.vy = Math.abs(item.vy) * 0.35; bounced = true; }
|
|
if (item.y > maxY) { item.y = maxY; item.vy = -Math.abs(item.vy) * 0.35; bounced = true; }
|
|
if (bounced) {
|
|
item.robotCleanerWanderAngle = Math.atan2(item.vy || 0, item.vx || 0) + (global.deterministicRange ? global.deterministicRange(worldRef, "robot-cleaner-bounce", -0.8, 0.8, item) : (Math.random() - 0.5) * 1.6);
|
|
item.robotCleanerWanderTimer = 0.4;
|
|
}
|
|
if (Math.hypot(item.vx || 0, item.vy || 0) > 2) item.robotCleanerAngle = Math.atan2(item.vy || 0, item.vx || 0);
|
|
worldRef.markSpatialDirty?.("robot-cleaner-move");
|
|
worldRef.drawListDirty = true;
|
|
}
|
|
|
|
function updateRobotCleaner(item, dt, worldRef) {
|
|
if (!item || item.dead || !worldRef) return false;
|
|
normalizeRobotCleanerState(item);
|
|
dt = Math.max(0, Math.min(0.05, Number(dt || 0) || 0));
|
|
item.robotCleanerCleanFlash = Math.max(0, Number(item.robotCleanerCleanFlash || 0) - dt * 2.8);
|
|
item.robotCleanerScanTimer = Math.max(0, Number(item.robotCleanerScanTimer || 0) - dt);
|
|
let target = currentRobotTarget(item, worldRef);
|
|
if (!target || item.robotCleanerScanTimer <= 0) {
|
|
target = retargetRobotCleaner(item, worldRef, true);
|
|
item.robotCleanerScanTimer = ROBOT_RESCAN_INTERVAL;
|
|
}
|
|
moveRobotCleaner(item, dt, target, worldRef);
|
|
if (target && robotTargetAlive(target)) {
|
|
const isTarinaiTarget = String(item.robotCleanerTargetType || "") === "tarinai" || isRobotTarinaiTarget(target);
|
|
const d = distXY(item.x, item.y, target.x, target.y);
|
|
if (isTarinaiTarget && d <= Math.max(96, (item.r || 22) + (target.radius || target.r || 12) + 70)) triggerRobotPanic(item, target, worldRef);
|
|
}
|
|
cleanRobotContacts(item, worldRef);
|
|
return true;
|
|
}
|
|
|
|
function updateGateFence(item) {
|
|
if (item) item.amount = 999;
|
|
return Boolean(item);
|
|
}
|
|
|
|
function update(item, dt, worldRef) {
|
|
if (!item || item.dead) return false;
|
|
if (item.type === "firecracker") return updateFirecracker(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 === "gate_fence") return updateGateFence(item, dt, worldRef);
|
|
if (item.type === "robot_cleaner") return updateRobotCleaner(item, dt, worldRef);
|
|
if (item.type === "rope") return updateFlexibleLink(item, dt, worldRef);
|
|
if (item.type === "rod") return updateRigidLink(item, dt, worldRef);
|
|
return false;
|
|
}
|
|
|
|
global.TarinaiItemDynamicToolSystem = Object.freeze({
|
|
update,
|
|
robotCleanerDefaultMask,
|
|
robotCleanerMask,
|
|
setRobotCleanerMask,
|
|
robotCleanerHasTarget,
|
|
robotCleanerHighSpeed,
|
|
setRobotCleanerHighSpeed,
|
|
robotCleanerEffectiveSpeed,
|
|
robotTargetLabel,
|
|
robotCleanerTargetBits: ROBOT_TARGET_BITS,
|
|
robotCleanerTargetOrder: ROBOT_TARGET_ORDER,
|
|
robotCleanerMoveSpeed: ROBOT_MOVE_SPEED,
|
|
robotCleanerHighMoveSpeed: ROBOT_HIGH_SPEED,
|
|
});
|
|
})(typeof window !== "undefined" ? window : globalThis);
|