tarinai/js/snapshot_system.js

257 lines
11 KiB
JavaScript
Raw Normal View History

2026-06-22 02:03:19 +09:00
"use strict";
(function (global) {
const SNAPSHOT_VERSION = 1;
const DEFAULT_SKIP = new Set(["world", "target", "panicTarget", "targetRef", "targetEntity", "source"]);
function isPlainObject(value) {
if (!value || typeof value !== "object") return false;
const proto = Object.getPrototypeOf(value);
return proto === Object.prototype || proto === null;
}
function clonePlain(value, depth = 0) {
if (value == null) return value;
const type = typeof value;
if (type === "number") return Number.isFinite(value) ? value : 0;
if (type === "string" || type === "boolean") return value;
if (type === "function" || type === "symbol" || type === "undefined") return undefined;
if (depth > 8) return undefined;
if (Array.isArray(value)) {
const out = [];
for (const v of value) {
const c = clonePlain(v, depth + 1);
if (c !== undefined) out.push(c);
}
return out;
}
if (!isPlainObject(value)) return undefined;
const out = {};
for (const [key, v] of Object.entries(value)) {
if (DEFAULT_SKIP.has(key)) continue;
const c = clonePlain(v, depth + 1);
if (c !== undefined) out[key] = c;
}
return out;
}
function copyOwnData(obj, skip = DEFAULT_SKIP) {
const out = {};
if (!obj) return out;
for (const key of Object.keys(obj)) {
if (skip?.has?.(key)) continue;
const cloned = clonePlain(obj[key]);
if (cloned !== undefined) out[key] = cloned;
}
return out;
}
function applyOwnData(target, data, skip = DEFAULT_SKIP) {
if (!target || !data) return target;
for (const [key, value] of Object.entries(data)) {
if (skip?.has?.(key) || key.startsWith("__")) continue;
const c = clonePlain(value);
if (c !== undefined) target[key] = c;
}
return target;
}
function refFor(worldRef, value) {
if (!value || typeof value !== "object") return null;
if ((worldRef?.tarinai || []).includes(value)) return { kind: "tarinai", id: value.id || "", token: value.liveToken || 0, familyKey: value.familyKey || "" };
if ((worldRef?.items || []).includes(value)) return { kind: "item", id: value.id || "", type: value.type || "" };
if ((worldRef?.ants || []).includes(value)) return { kind: "ant", id: value.id || "" };
if (Number.isFinite(value.x) && Number.isFinite(value.y)) return { kind: "point", x: value.x, y: value.y, dead: Boolean(value.dead) };
return null;
}
function resolveRef(worldRef, ref) {
if (!ref || typeof ref !== "object") return null;
if (ref.kind === "tarinai") return (worldRef.tarinai || []).find(t => t && !t.dead && ((ref.id && t.id === ref.id) || (ref.familyKey && t.familyKey === ref.familyKey))) || null;
if (ref.kind === "item") return (worldRef.items || []).find(it => it && !it.dead && it.id === ref.id) || null;
if (ref.kind === "ant") return (worldRef.ants || []).find(a => a && !a.dead && a.id === ref.id) || null;
if (ref.kind === "point") return { x: Number(ref.x) || 0, y: Number(ref.y) || 0, dead: Boolean(ref.dead) };
return null;
}
function worldData(worldRef) {
const keys = [
"w", "h", "viewportW", "viewportH", "time", "day", "paused", "speed", "tool", "toolSize", "toolSizes", "frozenTarinai",
"fieldType", "fieldZoom", "cameraX", "cameraY", "weather", "nextWeatherChange", "deadCount",
"liveIdNext", "liveIdSerial", "lastBirthAt", "maxGeneration", "family", "familyVersion", "familyTreeDirtyReason",
"relationNotices", "resolvedFightIds", "eventCounters", "foodSpoilagePenalty", "foodSpoilageEvents",
"lastPhase", "colonyMood", "lastColonyMoodDay", "antUpdatePhase", "shakeTimer", "shakeDuration", "shakeStrength"
];
const out = {};
for (const key of keys) {
const cloned = clonePlain(worldRef?.[key]);
if (cloned !== undefined) out[key] = cloned;
}
return out;
}
function snapshotEntity(worldRef, entity, skip = new Set(["world", "target", "panicTarget", "targetRef"])) {
const data = copyOwnData(entity, skip);
const targetRef = refFor(worldRef, entity?.target);
const panicTargetRef = refFor(worldRef, entity?.panicTarget);
const targetRefObject = refFor(worldRef, entity?.targetRef);
if (targetRef) data.__targetRef = targetRef;
if (panicTargetRef) data.__panicTargetRef = panicTargetRef;
if (targetRefObject) data.__targetRefObject = targetRefObject;
return data;
}
function createSnapshot(worldRef = global.world) {
if (!worldRef) throw new Error("world is not ready");
const selectedRef = refFor(worldRef, worldRef.selected);
return {
version: SNAPSHOT_VERSION,
app: "tarinai_colony_game",
createdAt: Date.now(),
summary: {
day: worldRef.day || 1,
time: worldRef.clockString ? worldRef.clockString() : "",
fieldType: worldRef.fieldType || "garden",
population: (worldRef.tarinai || []).filter(t => t && !t.dead).length,
items: (worldRef.items || []).filter(it => it && !it.dead).length,
},
world: worldData(worldRef),
tarinai: (worldRef.tarinai || []).filter(Boolean).map(t => snapshotEntity(worldRef, t)),
items: (worldRef.items || []).filter(Boolean).map(it => snapshotEntity(worldRef, it)),
ants: (worldRef.ants || []).filter(Boolean).map(a => snapshotEntity(worldRef, a)),
effects: (worldRef.effects || []).filter(Boolean).map(ef => snapshotEntity(worldRef, ef)),
logs: clonePlain((worldRef.logs || []).slice(0, 100)) || [],
selectedRef,
};
}
function restoreTarinaiData(worldRef, data, opts = {}) {
const TarinaiClass = global.Tarinai || (typeof Tarinai !== "undefined" ? Tarinai : null);
if (!TarinaiClass) throw new Error("Tarinai is not available");
const t = new TarinaiClass(worldRef, data || {});
applyOwnData(t, data || {}, new Set(["world", "target", "panicTarget", "targetRef"]));
t.world = worldRef;
if (opts.clearTransient !== false) {
t.target = null;
t.panicTarget = null;
t.sleeping = false;
t.insideNestBoxId = "";
}
if (typeof t.refreshEffectiveSize === "function") t.refreshEffectiveSize();
return t;
}
function restoreSnapshot(snapshot, worldRef = global.world) {
if (!snapshot || typeof snapshot !== "object") throw new Error("invalid save data");
if (!worldRef) throw new Error("world is not ready");
const data = snapshot.version ? snapshot : JSON.parse(JSON.stringify(snapshot));
const fieldType = data.world?.fieldType || "garden";
if (typeof applyFieldLayout === "function") applyFieldLayout(fieldType);
if (typeof worldRef.reset === "function") worldRef.reset(0, fieldType);
worldRef.tarinai = [];
worldRef.items = [];
worldRef.ants = [];
worldRef.effects = [];
worldRef.logs = Array.isArray(data.logs) ? clonePlain(data.logs) || [] : [];
worldRef.selected = null;
applyOwnData(worldRef, data.world || {}, new Set(["tarinai", "items", "ants", "effects", "logs", "selected", "liveTarinai", "events", "spatial"]));
worldRef.events = global.TarinaiEvents || null;
worldRef.liveTarinai = new Map();
worldRef.liveIdFree = [];
const items = Array.isArray(data.items) ? data.items : [];
for (const saved of items) {
if (!saved || !saved.type) continue;
const item = new Item(saved.type, Number(saved.x) || 0, Number(saved.y) || 0);
applyOwnData(item, saved, new Set(["target", "targetRef", "world"]));
worldRef.items.push(item);
}
const tarinaiList = Array.isArray(data.tarinai) ? data.tarinai : [];
for (const saved of tarinaiList) {
if (!saved) continue;
const t = restoreTarinaiData(worldRef, saved, { clearTransient: false });
worldRef.tarinai.push(t);
if (t.id) worldRef.liveTarinai.set(t.id, { token: t.liveToken || 0, target: t });
}
const ants = Array.isArray(data.ants) ? data.ants : [];
for (const saved of ants) {
if (!saved) continue;
const ant = new AntActor(worldRef, saved);
applyOwnData(ant, saved, new Set(["world", "targetRef"]));
ant.world = worldRef;
worldRef.ants.push(ant);
}
const effects = Array.isArray(data.effects) ? data.effects : [];
for (const saved of effects) {
if (!saved || !saved.type) continue;
const ef = new Effect(saved.type, Number(saved.x) || 0, Number(saved.y) || 0, saved);
applyOwnData(ef, saved, new Set(["world", "target", "targetRef"]));
worldRef.effects.push(ef);
}
const itemById = new Map(worldRef.items.map(it => [it.id, it]));
const antById = new Map(worldRef.ants.map(a => [a.id, a]));
const tarinaiById = new Map(worldRef.tarinai.map(t => [t.id, t]));
const resolve = (ref) => {
if (!ref) return null;
if (ref.kind === "item") return itemById.get(ref.id) || null;
if (ref.kind === "ant") return antById.get(ref.id) || null;
if (ref.kind === "tarinai") return tarinaiById.get(ref.id) || worldRef.tarinai.find(t => t.familyKey === ref.familyKey) || null;
return resolveRef(worldRef, ref);
};
for (const [i, saved] of tarinaiList.entries()) {
const t = worldRef.tarinai[i];
if (!t) continue;
t.target = resolve(saved.__targetRef);
t.panicTarget = resolve(saved.__panicTargetRef);
}
for (const [i, saved] of ants.entries()) {
const ant = worldRef.ants[i];
if (!ant) continue;
ant.targetRef = resolve(saved.__targetRefObject);
}
worldRef.selected = resolve(data.selectedRef);
worldRef.family = clonePlain(data.world?.family) || worldRef.family || {};
worldRef.relationNotices = clonePlain(data.world?.relationNotices) || {};
worldRef.resolvedFightIds = clonePlain(data.world?.resolvedFightIds) || {};
worldRef.eventCounters = clonePlain(data.world?.eventCounters) || {};
worldRef.colonyMood = clonePlain(data.world?.colonyMood) || worldRef.colonyMoodDefinition?.("relaxed") || { id: "relaxed", label: "のんびり", effects: { personality: {} } };
worldRef.familyTreeDirty = true;
worldRef.drawListDirty = true;
worldRef.terrainDirty = true;
if (typeof worldRef.updateItemCounts === "function") worldRef.updateItemCounts();
if (typeof worldRef.updateEffectCounts === "function") worldRef.updateEffectCounts();
if (typeof worldRef.rebuildSpatial === "function") worldRef.rebuildSpatial(true);
if (typeof worldRef.markTerrainDirty === "function") worldRef.markTerrainDirty("load");
if (typeof worldRef.clampCamera === "function") worldRef.clampCamera();
if (typeof renderLog === "function") renderLog(worldRef.logs || []);
if (typeof resetArchiveRenderState === "function") resetArchiveRenderState();
if (typeof renderArchive === "function") renderArchive();
if (typeof renderSelected === "function") renderSelected();
if (typeof renderStats === "function") renderStats();
if (typeof render === "function") render();
if (typeof syncTopButtons === "function") syncTopButtons();
global.TarinaiFreezeSystem?.afterSnapshotRestored?.(worldRef);
return true;
}
global.TarinaiSnapshot = {
version: SNAPSHOT_VERSION,
clonePlain,
copyOwnData,
applyOwnData,
refFor,
resolveRef,
worldData,
snapshotEntity,
createSnapshot,
restoreSnapshot,
restoreTarinaiData,
};
})(typeof window !== "undefined" ? window : globalThis);