refactor
This commit is contained in:
parent
0a6148c73f
commit
e8f1d1db1e
60 changed files with 2417 additions and 612 deletions
223
js/save_system.js
Normal file
223
js/save_system.js
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
"use strict";
|
||||
|
||||
(function (global) {
|
||||
const STORAGE_PREFIX = "tarinai_save_slot_v1_";
|
||||
const EXPORT_PREFIX = "TARINAI_SAVE_V1:";
|
||||
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);
|
||||
|
||||
function bytesToBase64(bytes) {
|
||||
let binary = "";
|
||||
const chunk = 0x8000;
|
||||
for (let i = 0; i < bytes.length; i += chunk) binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
||||
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
||||
}
|
||||
|
||||
function base64ToBytes(text) {
|
||||
let b64 = String(text || "").trim().replace(/^#+/, "").replace(EXPORT_PREFIX, "").replace(/-/g, "+").replace(/_/g, "/");
|
||||
while (b64.length % 4) b64 += "=";
|
||||
const binary = atob(b64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function encodeSnapshot(snapshot) {
|
||||
const json = JSON.stringify(snapshot);
|
||||
const bytes = new TextEncoder().encode(json);
|
||||
return EXPORT_PREFIX + bytesToBase64(bytes);
|
||||
}
|
||||
|
||||
function decodeSnapshot(text) {
|
||||
const raw = String(text || "").trim();
|
||||
if (!raw) throw new Error("empty import text");
|
||||
if (raw.startsWith("{") || raw.startsWith("[")) return JSON.parse(raw);
|
||||
const bytes = base64ToBytes(raw);
|
||||
const json = new TextDecoder().decode(bytes);
|
||||
return JSON.parse(json);
|
||||
}
|
||||
|
||||
function slotKey(slot) { return `${STORAGE_PREFIX}${slot}`; }
|
||||
|
||||
function saveSlot(slot) {
|
||||
const snapshot = createSnapshot(global.world);
|
||||
localStorage.setItem(slotKey(slot), JSON.stringify(snapshot));
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function loadSlot(slot) {
|
||||
const raw = localStorage.getItem(slotKey(slot));
|
||||
if (!raw) throw new Error("empty slot");
|
||||
return restoreSnapshot(JSON.parse(raw), 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 "空き";
|
||||
const d = snapshot.summary || {};
|
||||
const date = snapshot.createdAt ? new Date(snapshot.createdAt).toLocaleString("ja-JP", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }) : "日時不明";
|
||||
return `${date} / ${d.fieldType || "庭"} / ${d.day || 1}日目 ${d.time || ""} / ${d.population || 0}匹`;
|
||||
}
|
||||
|
||||
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 = `
|
||||
<div class="field-dialog-panel save-dialog-panel">
|
||||
<h2 id="saveDialogTitle">セーブ / ロード</h2>
|
||||
<p class="hint">9つのスロットに現在のコロニーを保存できます。ハッシュテキストは別端末や別ブラウザへの移動に使えます。</p>
|
||||
<div id="saveSlotList" class="save-slot-list"></div>
|
||||
<div class="save-export-panel">
|
||||
<div class="save-export-actions">
|
||||
<button id="saveExportBtn" class="btn" type="button">現在の状態を書き出し</button>
|
||||
<button id="saveImportBtn" class="btn primary" type="button">ハッシュテキストを読み込み</button>
|
||||
</div>
|
||||
<textarea id="saveHashText" class="save-hash-text" spellcheck="false" placeholder="ここにハッシュテキストが表示されます。読み込む場合はここに貼り付けてください。"></textarea>
|
||||
</div>
|
||||
<div class="field-dialog-actions">
|
||||
<button id="saveCloseBtn" class="btn" type="button">閉じる</button>
|
||||
</div>
|
||||
</div>`;
|
||||
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 = `
|
||||
<div class="save-slot-title">スロット ${i}</div>
|
||||
<div class="save-slot-meta">${htmlEscape(formatSlotSummary(snapshot))}</div>
|
||||
<div class="save-slot-actions">
|
||||
<button class="btn mini" data-save-slot="${i}" type="button">保存</button>
|
||||
<button class="btn mini" data-load-slot="${i}" type="button" ${snapshot ? "" : "disabled"}>読込</button>
|
||||
<button class="btn mini danger" data-delete-slot="${i}" type="button" ${snapshot ? "" : "disabled"}>削除</button>
|
||||
</div>`;
|
||||
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", (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 {
|
||||
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 {
|
||||
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) {
|
||||
area.value = encodeSnapshot(createSnapshot(global.world));
|
||||
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(decodeSnapshot(text), global.world);
|
||||
closeDialog();
|
||||
global.showToast?.("ハッシュテキストから読み込みました。");
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
global.showToast?.("読み込みに失敗しました。テキストを確認してください。");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
global.TarinaiSaveSystem = {
|
||||
createSnapshot,
|
||||
restoreSnapshot,
|
||||
encodeSnapshot,
|
||||
decodeSnapshot,
|
||||
saveSlot,
|
||||
loadSlot,
|
||||
deleteSlot,
|
||||
readSlot,
|
||||
bindSaveSystem,
|
||||
openDialog,
|
||||
};
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
Loading…
Add table
Add a link
Reference in a new issue