tarinai/js/structure_lifecycle.js
2026-07-18 13:06:02 +09:00

325 lines
13 KiB
JavaScript

"use strict";
(function (global) {
const CONNECTION_ITEM_TYPES = new Set(["rope", "rod", "spring", "wire", "insulated_wire"]);
function isLiveLooseItem(item) {
return Boolean(item && !item.dead && !(Number.isFinite(Number(item.amount)) && Number(item.amount) <= 0));
}
function markWorldAfterStructureChange(worldRef, reason = "structure-change", item = null, radius = 40) {
if (!worldRef) return;
worldRef.drawListDirty = true;
worldRef.markItemBucketsDirty?.(reason);
worldRef.markSpatialDirty?.(reason);
if (item && Number.isFinite(item.x) && Number.isFinite(item.y)) {
worldRef.markTerrainDirtyAt?.(item.x, item.y, Math.max(24, Number(radius || item.r || 40) || 40), reason);
} else {
worldRef.markTerrainDirty?.(reason);
}
}
function markLooseItemGone(item, reason = "item-removed") {
if (!item || item.dead) return false;
// Normal Item.dead is a getter, so all non-structure removals go through
// amount/hp zeroing instead of assigning to .dead.
item.amount = 0;
item.hp = 0;
item.foodServingsRemaining = 0;
if (item.type === "grass") {
item.growth = 0;
item.grassStage = -1;
item.seedTimer = Math.max(Number(item.seedTimer || 0), 2.5);
}
return true;
}
function markStructureGone(structure, reason = "structure-removed") {
if (!structure || structure.dead) return false;
structure.dead = true;
structure.hp = 0;
structure.amount = 0;
return true;
}
function ownedStructureDestroyedLabel(structure) {
if (structure?.type === "plushie") return "\u306C\u3044\u3050\u308B\u307F";
if (structure?.type === "grass_bed") return "\u304B\u3093\u305F\u3093\u30D9\u30C3\u30C9";
return structure?.label || "\u81EA\u5206\u306E\u3082\u306E";
}
function shouldPanicOwnerOnStructureGone(structure, reason = "") {
if (!structure || !structure.ownerId) return false;
if (!(structure.type === "plushie" || structure.type === "grass_bed")) return false;
if (String(reason || "") === "grass-bed-wake-used") return false;
return true;
}
function triggerOwnedStructureDestroyedPanic(worldRef, structure, breaker = null, reason = "structure-destroyed") {
if (!worldRef || !shouldPanicOwnerOnStructureGone(structure, reason)) return false;
const owner = worldRef.liveTarinaiById?.(structure.ownerId) || (worldRef.tarinai || []).find(t => t && !t.dead && t.id === structure.ownerId);
if (!owner) return false;
const label = ownedStructureDestroyedLabel(structure);
const detail = `\u81EA\u5206\u306E${label}\u304C\u58CA\u3055\u308C\u305F`;
const panicReason = `\u81EA\u5206\u306E${label}\u3092\u58CA\u3055\u308C\u3066\u30D1\u30CB\u30C3\u30AF\u306B\u306A\u3063\u3066\u3044\u308B`;
const threat = breaker || {
id: `${structure.id || "structure"}:destroyed`,
x: Number(structure.x) || owner.x,
y: Number(structure.y) || owner.y,
type: "owned_structure_destroyed",
dead: false,
};
owner.lastPanicDetail = detail;
owner.fearTimer = Math.max(owner.fearTimer || 0, structure.type === "plushie" ? 3.2 : 2.6);
owner.surpriseTimer = Math.max(owner.surpriseTimer || 0, 0.85);
owner.awakeLockTimer = Math.max(owner.awakeLockTimer || 0, 2.4);
if (owner.enterPanic) {
owner.enterPanic({
threat,
target: owner.panicDestination?.(threat, true) || null,
forceDestination: true,
reason: panicReason,
fear: structure.type === "plushie" ? 1.65 : 1.35,
stress: structure.type === "plushie" ? 20 : 15,
wake: true,
cause: `${structure.type}_destroyed`,
surpriseTimer: 0.85,
awakeLockTimer: 2.4,
});
} else {
owner.setActionState?.("panic", { target: owner.panicDestination?.(threat, true) || null, reason: panicReason, wake: true, sleeping: false });
}
if (typeof global.setBehaviorText === "function") {
global.setBehaviorText(owner, {
need: "safety",
actionId: "panic_escape",
actionLabel: "\u30D1\u30CB\u30C3\u30AF\u306B\u306A\u3063\u3066\u3044\u308B",
reasonText: panicReason,
causeText: detail,
target: threat,
phase: "perform",
source: "behavior",
});
}
owner.thought = panicReason;
owner.behaviorLockTimer = Math.max(owner.behaviorLockTimer || 0, 1.2);
worldRef.spawnBubble?.(owner.x, owner.y - (owner.radius || 20) * 1.20, "\uFF01\uFF1F", "rgba(188,66,76,0.88)");
worldRef.log?.(`${owner.name || "\u305F\u308A\u306A\u3044"}\u306F\u81EA\u5206\u306E${label}\u3092\u58CA\u3055\u308C\u3066\u30D1\u30CB\u30C3\u30AF\u306B\u306A\u3063\u305F\u3002`, "danger");
worldRef.drawListDirty = true;
return true;
}
function releaseStructureUsers(worldRef, structure, opts = {}) {
if (!worldRef || !structure) return 0;
const reason = opts.reason || "\u306a\u304f\u306a\u3063\u305f";
const wake = opts.wake !== false;
let released = 0;
for (const t of worldRef.tarinai || []) {
if (!t || t.dead) continue;
const targetMatch = t.target === structure;
const sleepTargetMatch = t.sleepSession?.targetId && t.sleepSession.targetId === structure.id;
const nestMatch = (structure.type === "nest_box" || structure.type === "pipe") && t.insideNestBoxId === structure.id;
if (!targetMatch && !sleepTargetMatch && !nestMatch) continue;
if (nestMatch) {
t.insideNestBoxId = null;
}
if (sleepTargetMatch && t.sleepSession) {
t.sleepSession.targetId = null;
t.sleepSession.consumesGrassBedOnWake = false;
}
if (targetMatch) t.target = null;
if (t.state === "sleep" || t.sleeping || t.state === "seek_bed" || nestMatch) {
t.setActionState?.("idle", { clearTarget: true, reason, wake, sleeping: false });
}
released += 1;
}
return released;
}
function endpointTarget(worldRef, endpoint) {
if (!worldRef || !endpoint) return null;
const cached = endpoint._ref;
if (endpoint.kind === "tarinai") {
const token = endpoint.liveToken ?? null;
const found = worldRef.liveTarinaiById?.(endpoint.id, token)
|| worldRef.tarinaiById?.get?.(endpoint.id)
|| (worldRef.tarinai || []).find(t => t && !t.dead && t.id === endpoint.id)
|| null;
if (!found || found.dead) return null;
if (token != null && Number(found.liveToken ?? token) !== Number(token)) return null;
return found;
}
if (endpoint.kind === "ant") {
const found = (worldRef.ants || []).find(ant => ant && !ant.dead && ant.id === endpoint.id) || null;
return found && !found.dead ? found : null;
}
let found = worldRef.itemById?.(endpoint.id) || null;
if (found) return isLiveLooseItem(found) ? found : null;
found = (worldRef.items || []).find(item => item && item.id === endpoint.id) || null;
if (found) return isLiveLooseItem(found) ? found : null;
if (cached && (!endpoint.id || cached.id === endpoint.id) && isLiveLooseItem(cached)) return cached;
return null;
}
function connectionItems(worldRef) {
if (!worldRef) return [];
if (typeof worldRef.itemsOfType !== "function") return (worldRef.items || []).filter(item => isLiveLooseItem(item) && CONNECTION_ITEM_TYPES.has(item.type));
const out = [];
for (const type of CONNECTION_ITEM_TYPES) {
for (const item of worldRef.itemsOfType(type) || []) if (isLiveLooseItem(item)) out.push(item);
}
return out;
}
function removeOrphanedLinks(worldRef, reason = "link-endpoint-gone") {
if (!worldRef?.items) return 0;
const pb = global.TarinaiPhysicsBodySystem || null;
if (!pb?.endpoint) return 0;
const links = connectionItems(worldRef);
const linksById = new Map(links.map(link => [link.id, link]));
const dependents = new Map();
const queue = [];
const queued = new Set();
const enqueue = link => {
if (!link || queued.has(link.id)) return;
queued.add(link.id);
queue.push(link);
};
for (const link of links) {
let orphaned = false;
for (const endpoint of [pb.endpoint(link, 0), pb.endpoint(link, 1)]) {
const target = endpointTarget(worldRef, endpoint);
if (!target) {
orphaned = true;
continue;
}
if (endpoint?.kind === "item" && linksById.has(target.id)) {
if (!dependents.has(target.id)) dependents.set(target.id, []);
dependents.get(target.id).push(link);
}
}
if (orphaned) enqueue(link);
}
let removed = 0;
let queueIndex = 0;
while (queueIndex < queue.length) {
const link = queue[queueIndex++];
if (!isLiveLooseItem(link)) continue;
if (!markLooseItemGone(link, reason)) continue;
removed += 1;
for (const dependent of dependents.get(link.id) || []) enqueue(dependent);
}
let pendingCleared = false;
const pending = worldRef.pendingLinkEndpoint || null;
if (pending && !endpointTarget(worldRef, pending)) {
worldRef.pendingLinkEndpoint = null;
pendingCleared = true;
}
if (removed || pendingCleared) {
worldRef.drawListDirty = true;
worldRef.markItemBucketsDirty?.(reason);
worldRef.markSpatialDirty?.(reason);
}
return removed;
}
function reconcilePlushies(worldRef, reason = "plushie-attachment-reconcile") {
if (!worldRef?.items) return { restored: 0, removed: 0 };
let restored = 0;
let removed = 0;
const source = worldRef.itemsOfType?.("plushie") || worldRef.items;
for (const plushie of source || []) {
if (!plushie || plushie.dead || !plushie.isStructure || plushie.type !== "plushie") continue;
if (plushie.playerHeld || plushie._heldByPlayer || Number(plushie.plushieFlingTimer || 0) > 0) continue;
const owner = worldRef.liveTarinaiById?.(plushie.ownerId)
|| (worldRef.tarinai || []).find(t => t && !t.dead && t.id === plushie.ownerId)
|| null;
if (!plushie.ownerId || !owner || owner.dead) {
if (markStructureGone(plushie, "plushie-owner-gone")) removed += 1;
plushie.carriedById = "";
plushie.onHead = false;
continue;
}
if (plushie.carriedById === owner.id && plushie.onHead) continue;
const nextX = Number(owner.x || 0) || 0;
const nextY = (Number(owner.y || 0) || 0) - Math.max(22, (Number(owner.radius || 24) || 24) * 1.02);
plushie.ownerId = owner.id;
plushie.carriedById = owner.id;
plushie.onHead = true;
plushie.x = nextX;
plushie.y = nextY;
plushie.vx = 0;
plushie.vy = 0;
plushie.spinVelocity = 0;
restored += 1;
}
if (restored || removed) {
worldRef.drawListDirty = true;
worldRef.markItemBucketsDirty?.(reason);
worldRef.markSpatialDirty?.(reason);
}
return { restored, removed };
}
function maintainDependencies(worldRef) {
const linksRemoved = removeOrphanedLinks(worldRef, "link-endpoint-gone");
const plushies = reconcilePlushies(worldRef, "plushie-attachment-reconcile");
return { linksRemoved, plushiesRestored: plushies.restored, plushiesRemoved: plushies.removed };
}
function removeDependentLinks(worldRef, target, reason = "support-removed") {
if (!worldRef || !target || !target.id) return 0;
return removeOrphanedLinks(worldRef, reason);
}
function deleteStructure(worldRef, structure, opts = {}) {
if (!structure || structure.dead) return false;
const reason = opts.reason || "structure-deleted";
releaseStructureUsers(worldRef, structure, { reason: opts.userReason || "\u7f6e\u304d\u3082\u306e\u304c\u306a\u304f\u306a\u3063\u305f", wake: opts.wake !== false });
const ownerPanic = shouldPanicOwnerOnStructureGone(structure, reason);
const changed = markStructureGone(structure, reason);
if (!changed) return false;
worldRef.noteItemInactive?.(structure, reason);
removeDependentLinks(worldRef, structure, reason);
if (ownerPanic && opts.panicOwner !== false) triggerOwnedStructureDestroyedPanic(worldRef, structure, opts.breaker || null, reason);
markWorldAfterStructureChange(worldRef, reason);
return true;
}
function consumeGrassBedOnWake(worldRef, bed, user = null, reason = "wake") {
if (!bed || bed.dead || bed.type !== "grass_bed") return false;
bed.singleUsePending = false;
bed.singleUseConsumedById = user?.id || bed.singleUseConsumedById || null;
if (user?.target === bed) user.target = null;
const changed = markStructureGone(bed, "grass-bed-wake-used");
if (!changed) return false;
worldRef.noteItemInactive?.(bed, "grass-bed-wake-used");
markWorldAfterStructureChange(worldRef, "grass-bed-wake-used");
return true;
}
function deleteItem(worldRef, item, opts = {}) {
if (!item || item.dead) return false;
const reason = opts.reason || "delete-tool";
if (item.isStructure) return deleteStructure(worldRef, item, { reason, userReason: opts.userReason, wake: opts.wake, breaker: opts.breaker || null, panicOwner: opts.panicOwner });
const changed = markLooseItemGone(item, reason);
if (!changed) return false;
worldRef.noteItemInactive?.(item, reason);
removeDependentLinks(worldRef, item, reason);
markWorldAfterStructureChange(worldRef, reason, item, Math.max(36, Number(item.r || item.radius || 24) + 18));
return true;
}
const api = Object.freeze({
consumeGrassBedOnWake,
deleteItem,
maintainDependencies,
reconcilePlushies,
removeOrphanedLinks,
triggerOwnedStructureDestroyedPanic,
});
global.TarinaiStructureLifecycle = api;
})(typeof window !== "undefined" ? window : globalThis);