"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; 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; 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; 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);