257 lines
11 KiB
JavaScript
257 lines
11 KiB
JavaScript
|
|
"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);
|