"use strict"; (function (global) { const STORAGE_KEY = "tarinai_frozen_storage_v1"; const MAX_PENDING_LOSSES = 16; const Snapshot = global.TarinaiSnapshot; if (!Snapshot) throw new Error("TarinaiSnapshot is not available for freeze_system.js"); const clonePlain = Snapshot.clonePlain; const htmlEscape = global.TarinaiUIHelpers.htmlEscape; function frozenList(worldRef = global.world) { if (!worldRef) return []; if (!Array.isArray(worldRef.frozenTarinai)) worldRef.frozenTarinai = []; return worldRef.frozenTarinai; } function saveFrozenStore(worldRef = global.world) { try { global.localStorage?.setItem(STORAGE_KEY, JSON.stringify(frozenList(worldRef))); } catch (_) {} } function loadFrozenStore(worldRef = global.world) { if (!worldRef) return []; try { const raw = global.localStorage?.getItem(STORAGE_KEY); const parsed = raw ? JSON.parse(raw) : []; if (Array.isArray(parsed) && !worldRef.frozenTarinai?.length) worldRef.frozenTarinai = clonePlain(parsed) || []; } catch (_) { if (!Array.isArray(worldRef.frozenTarinai)) worldRef.frozenTarinai = []; } return frozenList(worldRef); } function spritePathFor(type = "smile") { const sprites = (typeof SPRITES !== "undefined" ? SPRITES : (global.SPRITES || [])); const meta = sprites.find(s => s.id === type) || sprites[0]; return meta?.path || "assets/sprites/tarinai_01_smile.webp"; } function ensureStyles() { if (document.getElementById("freezeSystemStyles")) return; const style = document.createElement("style"); style.id = "freezeSystemStyles"; style.textContent = ` .frozen-panel { margin-top: 10px; padding: 8px; border: 1px solid rgba(82, 63, 43, 0.18); border-radius: 14px; background: rgba(244, 249, 255, 0.72); } .frozen-panel-title { display:flex; align-items:center; justify-content:space-between; gap:8px; width:100%; border:0; background:transparent; padding:0; font-weight:700; font-size:13px; color:#4f5f70; margin:0; cursor:pointer; text-align:left; } .frozen-panel-title::before { content:"▸"; width:14px; color:rgba(79,95,112,.74); } .frozen-panel:not(.collapsed) .frozen-panel-title::before { content:"▾"; } .frozen-panel-body { margin-top:8px; } .frozen-panel.collapsed .frozen-panel-body { display:none; } .frozen-panel-count { font-size:12px; opacity:.72; } .frozen-list { display:grid; grid-template-columns: 1fr; gap:7px; } .frozen-card { display:flex; align-items:center; gap:8px; width:100%; border:1px solid rgba(72, 98, 124, .20); border-radius:12px; padding:7px; background:rgba(255,255,255,.78); cursor:grab; color:#354657; text-align:left; box-shadow: 0 2px 7px rgba(41,53,65,.06); } .frozen-card:active { cursor:grabbing; } .frozen-card.pending-thaw { outline:2px solid rgba(70, 142, 210, .46); background:rgba(227, 242, 255, .86); } .frozen-card img { width:34px; height:34px; object-fit:contain; flex:none; filter: drop-shadow(0 2px 3px rgba(34,44,55,.14)); } .frozen-card-main { min-width:0; flex:1; } .frozen-card-name { display:block; font-weight:700; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .frozen-card-meta { display:block; font-size:11px; opacity:.70; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .frozen-empty { font-size:12px; color:rgba(58,70,82,.68); padding:4px 2px; } canvas.freeze-drop-ready { outline: 3px solid rgba(70, 142, 210, .45); outline-offset: -3px; } `; document.head.appendChild(style); } function ensurePanel() { ensureStyles(); let panel = document.getElementById("frozenPanel"); if (panel) { const operateBody = document.querySelector('[data-tool-category="operate"] .tool-category-body'); if (operateBody && panel.parentNode !== operateBody) operateBody.appendChild(panel); return panel; } panel = document.createElement("section"); panel.id = "frozenPanel"; panel.className = "frozen-panel"; panel.classList.add("collapsed"); panel.innerHTML = `
`; const operateBody = document.querySelector('[data-tool-category="operate"] .tool-category-body'); const anchor = global.ui?.toolPalette || document.getElementById("toolPalette"); if (operateBody) operateBody.appendChild(panel); else if (anchor?.parentNode) anchor.parentNode.insertBefore(panel, anchor.nextSibling); else document.body.appendChild(panel); return panel; } function renderFrozenPanel(worldRef = global.world) { const panel = ensurePanel(); const listEl = panel.querySelector("#frozenList"); const countEl = panel.querySelector("#frozenPanelCount"); const list = frozenList(worldRef); if (countEl) countEl.textContent = `${list.length}`; if (!listEl) return; if (!list.length) { listEl.innerHTML = `
冷凍中のたりないはいません。
`; return; } listEl.innerHTML = list.map(entry => { const data = entry.data || entry; const losses = Array.isArray(entry.pendingLosses) && entry.pendingLosses.length ? ` / 訃報${entry.pendingLosses.length}` : ""; const gen = data.generation ? `第${data.generation}世代` : ""; const id = htmlEscape(entry.freezeId || entry.id || data.familyKey || ""); return ``; }).join(""); } function makeFrozenEntry(t, worldRef = global.world) { const data = Snapshot.snapshotEntity(worldRef, t, new Set(["world", "target", "panicTarget", "targetRef"])); data.sleeping = false; data.insideNestBoxId = ""; data.state = data.state === "sleep" || data.state === "seek_bed" ? "idle" : (data.state || "idle"); data.targetKey = ""; const id = (global.crypto?.randomUUID ? global.crypto.randomUUID() : `frozen-${Date.now()}-${Math.random()}`); return { freezeId: id, frozenAt: Date.now(), worldTime: worldRef?.time || 0, name: t.name || "たりない", familyKey: t.familyKey || "", liveId: t.id || "", liveToken: t.liveToken || 0, type: t.type || "smile", data, pendingLosses: [], }; } function findTarinaiAt(worldRef, x, y) { const arr = worldRef?.tarinai || []; for (let i = arr.length - 1; i >= 0; i--) { const t = arr[i]; if (!t || t.dead) continue; if (worldRef.isTarinaiHiddenInNestBox?.(t)) continue; if (t.contains ? t.contains(x, y) : distXY(x, y, t.x, t.y) <= (t.radius || 22) * 1.35) return t; } return null; } function freezeTarinai(t, worldRef = global.world) { if (!t || t.dead || !worldRef) return false; const entry = makeFrozenEntry(t, worldRef); worldRef.clearLiveReferencesTo?.(t); if (t.id && worldRef.liveTarinai?.has?.(t.id)) worldRef.liveTarinai.delete(t.id); worldRef.tarinai = (worldRef.tarinai || []).filter(o => o !== t); if (worldRef.selected === t) worldRef.selected = null; frozenList(worldRef).push(entry); worldRef.drawListDirty = true; worldRef.familyTreeDirty = true; worldRef.updateItemCounts?.(); worldRef.rebuildSpatial?.(true); saveFrozenStore(worldRef); renderFrozenPanel(worldRef); global.showToast?.(`${t.name}を冷凍保存しました。`); worldRef.log?.(`${t.name}をフィールド外で冷凍保存した。`, "observe", { participants: [t] }); global.renderStats?.(); global.render?.(); return true; } function updateLiveIdAfterThaw(worldRef, t) { if (!worldRef.liveTarinai) worldRef.liveTarinai = new Map(); const id = t.id || ""; const conflict = id && worldRef.liveTarinai.has(id); if (!id || conflict) { worldRef.assignTarinaiLiveId?.(t); return; } worldRef.liveTarinai.set(id, { token: t.liveToken || 0, target: t }); const m = /^t(\d+)$/.exec(id); if (m) worldRef.liveIdNext = Math.max(worldRef.liveIdNext || 1, Number(m[1]) + 1); worldRef.liveIdSerial = Math.max(worldRef.liveIdSerial || 0, t.liveToken || 0); } function applyPendingLossesOnThaw(t, entry, worldRef) { const losses = Array.isArray(entry.pendingLosses) ? entry.pendingLosses : []; if (!losses.length) return; const unique = []; const seen = new Set(); for (const loss of losses) { const key = loss.familyKey || loss.liveId || loss.name || JSON.stringify(loss); if (seen.has(key)) continue; seen.add(key); unique.push(loss); } const relPriority = unique.some(l => l.relation === "親" || l.relation === "子") ? 1 : 0; const stress = Math.min(36, 10 + unique.length * 6 + relPriority * 8); if (t.enterPanic) t.enterPanic({ reason: `${unique[0]?.name || "誰か"}がいないことに気づいた`, fear: 2.4 + Math.min(2.0, unique.length * 0.35), stress, cause: "pending_relation_death" }); else { t.stress = clamp((t.stress || 0) + stress, 0, 130); t.fearTimer = Math.max(t.fearTimer || 0, 2.4 + Math.min(2.0, unique.length * 0.35)); t.setActionState?.("panic", { target: t.panicDestination ? t.panicDestination() : null, reason: `${unique[0]?.name || "誰か"}がいないことに気づいた`, wake: true }); } t.bubble?.("いない", 1.1, "rgba(60,50,68,0.82)"); t.recordChangeCause?.("冷凍中の訃報", "ストレス", { value: stress }); t.addRecord?.(`冷凍中に${unique.map(l => l.name || "誰か").slice(0, 3).join("、")}が死亡していたことに気づいた。`, "death"); worldRef.log?.(`${t.name}は解凍時に身近な個体の死に気づき、パニックになった。`, "death", { participants: [t] }); } function thawFrozen(freezeId, x, y, worldRef = global.world) { const list = frozenList(worldRef); const index = list.findIndex(e => (e.freezeId || e.id) === freezeId); if (index < 0 || !worldRef) return false; const entry = list[index]; const data = clonePlain(entry.data || entry) || {}; let t = null; try { t = Snapshot.restoreTarinaiData(worldRef, data, { clearTransient: true }); } catch (_) { return false; } t.dead = false; t.deathReason = ""; t.x = clamp(Number(x) || worldRef.w / 2, CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding); t.y = clamp(Number(y) || worldRef.h / 2, CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding); t.vx = 0; t.vy = 0; t.target = null; t.panicTarget = null; t.insideNestBoxId = ""; t.sleeping = false; if (t.state === "sleep" || t.state === "seek_bed") t.goIdle?.("解凍されて少し驚いている"); t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.35); t.thought = "解凍されて少し驚いている"; t.frozenAt = 0; updateLiveIdAfterThaw(worldRef, t); if (typeof t.refreshEffectiveSize === "function") t.refreshEffectiveSize(); worldRef.tarinai.push(t); worldRef.recordFamily?.(t); applyPendingLossesOnThaw(t, entry, worldRef); list.splice(index, 1); worldRef.selected = t; worldRef.drawListDirty = true; worldRef.familyTreeDirty = true; worldRef.updateItemCounts?.(); worldRef.rebuildSpatial?.(true); saveFrozenStore(worldRef); renderFrozenPanel(worldRef); global.showToast?.(`${t.name}を解凍しました。`); worldRef.log?.(`${t.name}を解凍してフィールドに戻した。`, "birth", { participants: [t] }); global.renderStats?.(); global.render?.(); return true; } function bindFreezeSystem() { const worldRef = global.world; loadFrozenStore(worldRef); renderFrozenPanel(worldRef); const panel = ensurePanel(); if (panel.dataset.freezeBound !== "1") { panel.dataset.freezeBound = "1"; panel.addEventListener("click", (e) => { const title = e.target.closest(".frozen-panel-title"); if (!title) return; const collapsed = !panel.classList.toggle("collapsed"); title.setAttribute("aria-expanded", collapsed ? "true" : "false"); }); panel.addEventListener("dragstart", (e) => { const card = e.target.closest(".frozen-card[data-frozen-id]"); if (!card) return; const id = card.dataset.frozenId || ""; e.dataTransfer?.setData("text/plain", `tarinai-frozen:${id}`); e.dataTransfer?.setData("application/x-tarinai-frozen", id); e.dataTransfer.effectAllowed = "move"; global.document?.querySelector("canvas")?.classList.add("freeze-drop-ready"); }); panel.addEventListener("dragend", () => global.document?.querySelector("canvas")?.classList.remove("freeze-drop-ready")); panel.addEventListener("click", (e) => { const card = e.target.closest(".frozen-card[data-frozen-id]"); if (!card) return; pendingThawId = card.dataset.frozenId || ""; for (const c of panel.querySelectorAll(".frozen-card")) c.classList.toggle("pending-thaw", c === card); global.showToast?.("フィールドをクリックすると解凍します。ドラッグ&ドロップでも配置できます。"); }); } const canvas = global.canvas || document.getElementById("gameCanvas"); if (canvas && canvas.dataset.freezeDropBound !== "1") { canvas.dataset.freezeDropBound = "1"; canvas.addEventListener("dragover", (e) => { const text = e.dataTransfer?.types?.includes?.("application/x-tarinai-frozen") || [...(e.dataTransfer?.types || [])].includes("text/plain"); if (!text) return; e.preventDefault(); e.dataTransfer.dropEffect = "move"; canvas.classList.add("freeze-drop-ready"); }); canvas.addEventListener("dragleave", () => canvas.classList.remove("freeze-drop-ready")); canvas.addEventListener("drop", (e) => { const id = e.dataTransfer?.getData("application/x-tarinai-frozen") || String(e.dataTransfer?.getData("text/plain") || "").replace(/^tarinai-frozen:/, ""); if (!id) return; e.preventDefault(); canvas.classList.remove("freeze-drop-ready"); const p = typeof global.screenToWorld === "function" ? global.screenToWorld(e) : (() => { const rect = canvas.getBoundingClientRect(); const sx = e.clientX - rect.left; const sy = e.clientY - rect.top; return worldRef.screenToWorld ? worldRef.screenToWorld(sx, sy) : { x: sx, y: sy, inside: true }; })(); thawFrozen(id, p.x, p.y, worldRef); }); } } let pendingThawId = ""; function tryPendingThawAt(x, y, worldRef = global.world) { if (!pendingThawId) return false; const id = pendingThawId; pendingThawId = ""; document.querySelectorAll(".frozen-card.pending-thaw").forEach(el => el.classList.remove("pending-thaw")); return thawFrozen(id, x, y, worldRef); } function afterSnapshotRestored(worldRef = global.world) { if (!Array.isArray(worldRef?.frozenTarinai)) worldRef.frozenTarinai = []; saveFrozenStore(worldRef); renderFrozenPanel(worldRef); } function patchWorldPrototype() { const apply = (proto) => { const originalHandleClick = proto.handleClick; proto.handleClick = function (x, y) { if (tryPendingThawAt(x, y, this)) return; if (this.tool === "freeze") { global.showToast?.("冷凍は『つまむ』でたりないを冷凍個体UIへ運んでください。"); return; } return originalHandleClick.call(this, x, y); }; }; if (global.TarinaiPatcher?.patchWorld?.("freezeSystem", apply)) return; const World = global.World; if (!World || World.prototype.__freezeSystemPatched) return; World.prototype.__freezeSystemPatched = true; apply(World.prototype); } patchWorldPrototype(); global.TarinaiFreezeSystem = { bindFreezeSystem, renderFrozenPanel, freezeTarinai, thawFrozen, frozenList, loadFrozenStore, saveFrozenStore, afterSnapshotRestored, }; })(typeof window !== "undefined" ? window : globalThis);