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