"use strict"; (function (global) { const STORAGE_PREFIX = "tarinai_save_slot_v2_"; const EXPORT_PREFIX = "TN2!"; const SLOT_COUNT = 9; const Snapshot = global.TarinaiSnapshot; if (!Snapshot) throw new Error("TarinaiSnapshot is not available for save_system.js"); const createSnapshot = (...args) => Snapshot.createSnapshot(...args); const restoreSnapshot = (...args) => Snapshot.restoreSnapshot(...args); const SAVE_TEXT_ALPHABET = (() => { let out = ""; for (let i = 33; i <= 126; i++) { const ch = String.fromCharCode(i); if (ch === "*" || ch === "'" || ch === "_") continue; out += ch; } return out; })(); const SAVE_TEXT_REVERSE = (() => { const map = new Map(); for (let i = 0; i < SAVE_TEXT_ALPHABET.length; i++) map.set(SAVE_TEXT_ALPHABET[i], i); return map; })(); if (SAVE_TEXT_ALPHABET.length !== 91) throw new Error("save alphabet must contain 91 printable ASCII chars"); function base91Encode(bytes) { let b = 0; let n = 0; let out = ""; for (const byte of bytes || []) { b |= (byte & 255) << n; n += 8; if (n > 13) { let v = b & 8191; if (v > 88) { b >>= 13; n -= 13; } else { v = b & 16383; b >>= 14; n -= 14; } out += SAVE_TEXT_ALPHABET[v % 91] + SAVE_TEXT_ALPHABET[Math.floor(v / 91)]; } } if (n) { out += SAVE_TEXT_ALPHABET[b % 91]; if (n > 7 || b > 90) out += SAVE_TEXT_ALPHABET[Math.floor(b / 91)]; } return out; } function base91Decode(text) { const clean = String(text || "").trim(); let v = -1; let b = 0; let n = 0; const out = []; for (const ch of clean) { const c = SAVE_TEXT_REVERSE.get(ch); if (c == null) continue; if (v < 0) { v = c; } else { v += c * 91; b |= v << n; n += (v & 8191) > 88 ? 13 : 14; do { out.push(b & 255); b >>= 8; n -= 8; } while (n > 7); v = -1; } } if (v >= 0) out.push((b | (v << n)) & 255); return new Uint8Array(out); } async function compressBytes(bytes) { if (typeof CompressionStream === "function") { const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("deflate-raw")); return new Uint8Array(await new Response(stream).arrayBuffer()); } return bytes; } async function decompressBytes(bytes, codec = "R") { if (codec === "D") { if (typeof DecompressionStream !== "function") throw new Error("deflate decoder is not available in this browser"); const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("deflate-raw")); return new Uint8Array(await new Response(stream).arrayBuffer()); } return bytes; } async function encodeSnapshot(snapshot) { const compact = snapshot && snapshot.v === 2 ? snapshot : createSnapshot(global.world); const json = JSON.stringify(compact); const bytes = new TextEncoder().encode(json); let codec = "R"; let packed = bytes; try { const compressed = await compressBytes(bytes); if (compressed.length < bytes.length) { codec = "D"; packed = compressed; } } catch (error) { console.warn("save compression fallback", error); } return `${EXPORT_PREFIX}${codec}${base91Encode(packed)}`; } async function decodeSnapshot(text) { const raw = String(text || "").trim(); if (!raw) throw new Error("empty import text"); if (raw.startsWith("{") || raw.startsWith("[")) { const data = JSON.parse(raw); if (data?.v !== 2) throw new Error("unsupported save version"); return data; } if (!raw.startsWith(EXPORT_PREFIX)) throw new Error("unsupported save text"); const codec = raw.charAt(EXPORT_PREFIX.length) || "R"; const payload = raw.slice(EXPORT_PREFIX.length + 1); const packed = base91Decode(payload); const bytes = await decompressBytes(packed, codec); const json = new TextDecoder().decode(bytes); const data = JSON.parse(json); if (data?.v !== 2) throw new Error("unsupported save version"); return data; } function slotKey(slot) { return `${STORAGE_PREFIX}${slot}`; } async function saveSlot(slot) { const snapshot = createSnapshot(global.world); localStorage.setItem(slotKey(slot), JSON.stringify(snapshot)); return snapshot; } async function loadSlot(slot) { const raw = localStorage.getItem(slotKey(slot)); if (!raw) throw new Error("empty slot"); const snapshot = JSON.parse(raw); if (snapshot?.v !== 2) throw new Error("unsupported slot version"); return restoreSnapshot(snapshot, global.world); } function deleteSlot(slot) { localStorage.removeItem(slotKey(slot)); } function readSlot(slot) { const raw = localStorage.getItem(slotKey(slot)); if (!raw) return null; try { return JSON.parse(raw); } catch (_) { return null; } } const htmlEscape = global.TarinaiUIHelpers.htmlEscape; function formatSlotSummary(snapshot) { if (!snapshot) return "\u7a7a\u304d"; const m = snapshot.m || []; const date = snapshot.c ? new Date(snapshot.c).toLocaleString("ja-JP", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }) : "\u65e5\u6642\u4e0d\u660e"; const day = m[0] || 1; const population = m[1] || 0; const fieldType = m[2] || "\u5ead"; const timeText = Number.isFinite(m[3]) ? `${Math.floor((m[3] / 10) % 24).toString().padStart(2, "0")}:00` : ""; return `${date} / ${fieldType} / ${day}\u65e5\u76ee ${timeText} / ${population}\u5339`; } 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

\u30bb\u30fc\u30d6\u6a5f\u80fd\u306f\u5b9f\u9a13\u4e2d\u3067\u3042\u308a\u3001\u4fdd\u8a3c\u5bfe\u8c61\u5916\u3067\u3059\u3002

9\u3064\u306e\u30b9\u30ed\u30c3\u30c8\u306b\u73fe\u5728\u306e\u30b3\u30ed\u30cb\u30fc\u3092\u4fdd\u5b58\u3067\u304d\u307e\u3059\u3002\u30cf\u30c3\u30b7\u30e5\u30c6\u30ad\u30b9\u30c8\u306f\u5225\u7aef\u672b\u3084\u5225\u30d6\u30e9\u30a6\u30b6\u3078\u306e\u79fb\u52d5\u306b\u4f7f\u3048\u307e\u3059\u3002

`; 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(); 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?.(`\u30b9\u30ed\u30c3\u30c8${slot}\u306b\u4fdd\u5b58\u3057\u307e\u3057\u305f\u3002`); } catch (error) { console.warn(error); global.showToast?.("\u4fdd\u5b58\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u7a7a\u304d\u5bb9\u91cf\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002"); } return; } if (loadBtn) { const slot = Number(loadBtn.dataset.loadSlot); if (!global.confirm?.(`\u30b9\u30ed\u30c3\u30c8${slot}\u3092\u8aad\u307f\u8fbc\u307f\u307e\u3059\u3002\u73fe\u5728\u306e\u72b6\u614b\u306f\u4e0a\u66f8\u304d\u3055\u308c\u307e\u3059\u3002`)) return; try { await loadSlot(slot); closeDialog(); global.audio?.notify?.(); global.showToast?.(`\u30b9\u30ed\u30c3\u30c8${slot}\u3092\u8aad\u307f\u8fbc\u307f\u307e\u3057\u305f\u3002`); } catch (error) { console.warn(error); global.showToast?.("\u8aad\u307f\u8fbc\u307f\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002"); } return; } if (deleteBtn) { const slot = Number(deleteBtn.dataset.deleteSlot); if (!global.confirm?.(`\u30b9\u30ed\u30c3\u30c8${slot}\u3092\u524a\u9664\u3057\u307e\u3059\u304b\uff1f`)) return; deleteSlot(slot); renderSlotList(); global.audio?.delete?.(); global.showToast?.(`\u30b9\u30ed\u30c3\u30c8${slot}\u3092\u524a\u9664\u3057\u307e\u3057\u305f\u3002`); return; } if (e.target.closest("#saveCloseBtn")) closeDialog(); if (e.target.closest("#saveExportBtn")) { const area = dialog.querySelector("#saveHashText"); if (area) { area.value = await encodeSnapshot(createSnapshot(global.world)); area.focus(); area.select(); } global.showToast?.("\u73fe\u5728\u306e\u72b6\u614b\u3092\u30cf\u30c3\u30b7\u30e5\u30c6\u30ad\u30b9\u30c8\u306b\u66f8\u304d\u51fa\u3057\u307e\u3057\u305f\u3002"); } if (e.target.closest("#saveImportBtn")) { const area = dialog.querySelector("#saveHashText"); const text = area?.value || ""; if (!text.trim()) { global.showToast?.("\u8aad\u307f\u8fbc\u3080\u30cf\u30c3\u30b7\u30e5\u30c6\u30ad\u30b9\u30c8\u3092\u8cbc\u308a\u4ed8\u3051\u3066\u304f\u3060\u3055\u3044\u3002"); return; } if (!global.confirm?.("\u30cf\u30c3\u30b7\u30e5\u30c6\u30ad\u30b9\u30c8\u3092\u8aad\u307f\u8fbc\u307f\u307e\u3059\u3002\u73fe\u5728\u306e\u72b6\u614b\u306f\u4e0a\u66f8\u304d\u3055\u308c\u307e\u3059\u3002")) return; try { restoreSnapshot(await decodeSnapshot(text), global.world); closeDialog(); global.showToast?.("\u30cf\u30c3\u30b7\u30e5\u30c6\u30ad\u30b9\u30c8\u304b\u3089\u8aad\u307f\u8fbc\u307f\u307e\u3057\u305f\u3002"); } catch (error) { console.warn(error); global.showToast?.("\u8aad\u307f\u8fbc\u307f\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u30c6\u30ad\u30b9\u30c8\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002"); } } }); } global.TarinaiSaveSystem = { createSnapshot, restoreSnapshot, encodeSnapshot, decodeSnapshot, saveSlot, loadSlot, deleteSlot, readSlot, bindSaveSystem, openDialog, }; })(typeof window !== "undefined" ? window : globalThis);