"use strict"; (function (global) { const Snapshot = global.TarinaiSnapshot; const Codec = global.TarinaiSaveCodec; const Storage = global.TarinaiSaveStorage; const Restore = global.TarinaiRestoreCoordinator; if (!Snapshot || !Codec || !Storage || !Restore) throw new Error("save_system.js dependencies are not available"); const SLOT_COUNT = Storage.SLOT_COUNT; const htmlEscape = global.TarinaiUIHelpers.htmlEscape; function createSnapshot(worldRef = global.world) { return Snapshot.createSnapshot(worldRef); } function restoreSnapshot(snapshot, worldRef = global.world, options = {}) { return Restore.restoreSnapshot(snapshot, worldRef, { source: options.source || "save", syncUi: options.syncUi }); } async function encodeSnapshot(snapshot = createSnapshot(global.world)) { return Codec.encodeSnapshot(snapshot); } async function encodeSnapshotWithProfile(snapshot = createSnapshot(global.world)) { if (typeof Codec.encodeSnapshotWithProfile === "function") return Codec.encodeSnapshotWithProfile(snapshot); return { text: await Codec.encodeSnapshot(snapshot), profile: null }; } async function decodeSnapshot(text) { return Codec.decodeSnapshot(text); } async function saveSlot(slot) { const snapshot = createSnapshot(global.world); const hash = await encodeSnapshot(snapshot); return Storage.writeSlot(slot, hash, { c: snapshot.c, m: snapshot.m }); } async function loadSlot(slot) { const hash = Storage.requireSlot(slot); const snapshot = await decodeSnapshot(hash); return restoreSnapshot(snapshot, global.world, { source: "slot" }); } function deleteSlot(slot) { Storage.deleteSlot(slot); } function readSlot(slot) { return Storage.readSlot(slot); } function formatSlotSummary(entry) { if (!entry) return "\u7a7a\u304d"; const meta = entry.m || entry; const m = meta.m || []; const date = meta.savedAt ? new Date(meta.savedAt).toLocaleString("ja-JP", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }) : (meta.c ? new Date(meta.c).toLocaleString("ja-JP", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }) : "日時不明"); const day = m[0] || 1; const population = m[1] || 0; const fieldType = m[2] || "庭"; const timeText = Number.isFinite(m[3]) ? `${Math.floor((m[3] / 10) % 24).toString().padStart(2, "0")}:00` : ""; const hashLen = entry.hash ? ` / ${entry.hash.length}字` : ""; return `${date} / ${fieldType} / ${day}日目 ${timeText} / ${population}匹${hashLen}`; } function bytesText(value = 0) { const n = Math.max(0, Math.round(Number(value) || 0)); return `${n.toLocaleString("ja-JP")}B`; } function percentText(value = 0, total = 0) { const t = Number(total) || 0; if (t <= 0) return "-"; return `${((Number(value) || 0) / t * 100).toFixed(1)}%`; } function profileRow(label, bytes, total) { return `
${htmlEscape(label)}${htmlEscape(bytesText(bytes))}${htmlEscape(percentText(bytes, total))}
`; } function renderHashProfile(profile, dialog = ensureDialog()) { const box = dialog?.querySelector?.("#saveHashProfile"); if (!box) return; if (!profile) { box.innerHTML = ""; return; } const sections = profile.sections || {}; const comp = profile.compression || {}; const raw = comp.rawBytes || sections.totalRaw || 0; const itemEntries = Object.entries(profile.itemTypes || {}) .sort((a, b) => (b[1]?.bytes || 0) - (a[1]?.bytes || 0)) .slice(0, 10); const topItems = itemEntries.length ? itemEntries.map(([type, entry]) => `${htmlEscape(type)} ${Number(entry.count || 0)}個 / ${htmlEscape(bytesText(entry.bytes || 0))}`).join("") : `なし`; box.innerHTML = `
ハッシュ ${Number(comp.totalChars || 0).toLocaleString("ja-JP")}字 圧縮前 ${htmlEscape(bytesText(raw))} 圧縮後 ${htmlEscape(bytesText(comp.packedBytes || 0))} 圧縮率 ${comp.ratio ? `${(comp.ratio * 100).toFixed(1)}%` : "-"} 文字表 ${comp.japaneseText ? "日本語2048" : "不明"}
${profileRow("ヘッダ", sections.header, raw)} ${profileRow("ワールド", sections.world, raw)} ${profileRow("たりない合計", sections["tarinai.total"], raw)} ${profileRow("生成seed", sections["tarinai.seed"], raw)} ${profileRow("基本値", sections["tarinai.core"], raw)} ${profileRow("欲求", sections["tarinai.needs"], raw)} ${profileRow("性格差分", sections["tarinai.traits"], raw)} ${profileRow("家族", sections["tarinai.family"], raw)} ${profileRow("関係", sections["tarinai.relationships"], raw)} ${profileRow("タイマー/効果", (sections["tarinai.timers"] || 0) + (sections["tarinai.modes"] || 0) + (sections["tarinai.tail"] || 0), raw)} ${profileRow("アイテム合計", sections["items.total"], raw)} ${profileRow("アイテム基本", sections["items.core"], raw)} ${profileRow("アイテム追加", sections["items.extra"], raw)}
アイテム上位:${topItems}
`; } function ensureDialog() { let dialog = document.getElementById("saveDialog"); if (dialog) return dialog; dialog = document.createElement("div"); dialog.id = "saveDialog"; dialog.className = "field-dialog hidden"; dialog.setAttribute("role", "dialog"); dialog.setAttribute("aria-modal", "true"); dialog.innerHTML = `

\u30bb\u30fc\u30d6 / \u30ed\u30fc\u30c9

セーブは実験的機能であり保証対象外です。

`; document.body.appendChild(dialog); return dialog; } function renderSlotList() { const dialog = ensureDialog(); const list = dialog.querySelector("#saveSlotList"); if (!list) return; list.innerHTML = ""; for (let i = 1; i <= SLOT_COUNT; i++) { const snapshot = readSlot(i); const row = document.createElement("div"); row.className = "save-slot-row"; row.innerHTML = `
\u30b9\u30ed\u30c3\u30c8 ${i}
${htmlEscape(formatSlotSummary(snapshot))}
`; list.appendChild(row); } } function openDialog() { const dialog = ensureDialog(); renderSlotList(); dialog.classList.remove("hidden"); } function closeDialog() { document.getElementById("saveDialog")?.classList.add("hidden"); } function bindSaveSystem() { const btn = document.getElementById("saveBtn") || global.ui?.saveBtn; if (!btn || btn.dataset.saveBound === "1") return; btn.dataset.saveBound = "1"; btn.addEventListener("click", () => { global.audio?.uiClick?.(); openDialog(); }); const dialog = ensureDialog(); if (dialog.dataset.saveDialogBound === "1") return; dialog.dataset.saveDialogBound = "1"; dialog.addEventListener("click", async (e) => { if (e.target === dialog) { closeDialog(); return; } const saveBtn = e.target.closest("[data-save-slot]"); const loadBtn = e.target.closest("[data-load-slot]"); const deleteBtn = e.target.closest("[data-delete-slot]"); if (saveBtn) { const slot = Number(saveBtn.dataset.saveSlot); try { await saveSlot(slot); renderSlotList(); global.audio?.notify?.(); global.showToast?.(`スロット${slot}にセーブしました。`); } catch (error) { console.warn(error); global.showToast?.("セーブに失敗しました。空き容量を確認してください。"); } return; } if (loadBtn) { const slot = Number(loadBtn.dataset.loadSlot); if (!global.confirm?.(`スロット${slot}をロードします。現在の状態は上書きされます。`)) return; try { await loadSlot(slot); closeDialog(); global.audio?.notify?.(); global.showToast?.(`スロット${slot}をロードしました。`); } catch (error) { console.warn(error); global.showToast?.("ロードに失敗しました。"); } return; } if (deleteBtn) { const slot = Number(deleteBtn.dataset.deleteSlot); if (!global.confirm?.(`スロット${slot}を消しますか?`)) return; deleteSlot(slot); renderSlotList(); global.audio?.delete?.(); global.showToast?.(`スロット${slot}を消しました。`); return; } if (e.target.closest("#saveCloseBtn")) closeDialog(); if (e.target.closest("#saveExportBtn")) { const area = dialog.querySelector("#saveHashText"); if (area) { const result = await encodeSnapshotWithProfile(createSnapshot(global.world)); area.value = result.text || ""; renderHashProfile(result.profile, dialog); area.focus(); area.select(); } global.showToast?.("呪文をセーブしました。"); } if (e.target.closest("#saveImportBtn")) { const area = dialog.querySelector("#saveHashText"); const text = area?.value || ""; if (!text.trim()) { global.showToast?.("ロードする呪文を貼り付けてください。"); return; } if (!global.confirm?.("呪文をロードします。現在の状態は上書きされます。")) return; try { restoreSnapshot(await decodeSnapshot(text), global.world, { source: "import" }); closeDialog(); global.showToast?.("呪文からロードしました。"); } catch (error) { console.warn(error); global.showToast?.("ロードに失敗しました。呪文を確認してください。"); } } }); } global.TarinaiSaveSystem = { createSnapshot, restoreSnapshot, encodeSnapshot, encodeSnapshotWithProfile, decodeSnapshot, saveSlot, loadSlot, deleteSlot, readSlot, bindSaveSystem, openDialog, }; })(typeof window !== "undefined" ? window : globalThis);