tarinai/js/snapshot_system.js
2026-06-24 18:49:17 +09:00

547 lines
26 KiB
JavaScript

"use strict";
(function (global) {
const SNAPSHOT_VERSION = 2;
const NEED_KEYS = ["food", "sleep", "health", "safety", "social", "fulfill"];
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 q(value, scale = 1, fallback = 0) {
const n = Number(value);
return Number.isFinite(n) ? Math.round(n * scale) : fallback;
}
function u(value, scale = 1, fallback = 0) {
const n = Number(value);
return Number.isFinite(n) ? n / scale : fallback;
}
function compactNumberArray(obj, keys, scale = 1) {
return keys.map(k => q(obj?.[k], scale));
}
function expandNumberArray(arr, keys, scale = 1) {
const out = {};
if (!Array.isArray(arr)) return out;
keys.forEach((key, i) => { out[key] = u(arr[i], scale); });
return out;
}
function needsToArray(needs = {}) {
return NEED_KEYS.map(k => q(needs[k], 1));
}
function arrayToNeeds(arr = []) {
const out = typeof createDefaultNeeds === "function" ? createDefaultNeeds() : { food: 0, sleep: 0, health: 0, safety: 0, social: 0, fulfill: 0 };
NEED_KEYS.forEach((k, i) => { out[k] = u(arr[i], 1); });
return out;
}
function flagPack(t = {}) {
let flags = 0;
if (t.dead) flags |= 1 << 0;
if (t.favorite) flags |= 1 << 1;
if (t.zunchiDisease) flags |= 1 << 2;
if (t.sleepDisease) flags |= 1 << 3;
if (t.explosionDisease) flags |= 1 << 4;
if (t.fightDisease) flags |= 1 << 5;
if (t.isZunchiSlave) flags |= 1 << 6;
if (t.zunchiSlaveLocked) flags |= 1 << 7;
if (t.birthRitualLeader) flags |= 1 << 8;
return flags;
}
function flagApply(t, flags = 0) {
t.dead = !!(flags & (1 << 0));
t.favorite = !!(flags & (1 << 1));
t.zunchiDisease = !!(flags & (1 << 2));
t.sleepDisease = !!(flags & (1 << 3));
t.explosionDisease = !!(flags & (1 << 4));
t.fightDisease = !!(flags & (1 << 5));
t.isZunchiSlave = !!(flags & (1 << 6));
t.zunchiSlaveLocked = !!(flags & (1 << 7));
t.birthRitualLeader = !!(flags & (1 << 8));
}
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) {
return {
fieldType: worldRef?.fieldType || "garden",
time: Number(worldRef?.time) || 0,
day: Number(worldRef?.day) || 1,
cameraX: Number(worldRef?.cameraX) || 0,
cameraY: Number(worldRef?.cameraY) || 0,
weather: worldRef?.weather || "sunny",
nextWeatherChange: Number(worldRef?.nextWeatherChange) || 0,
deadCount: Number(worldRef?.deadCount) || 0,
liveIdNext: Number(worldRef?.liveIdNext) || 1,
liveIdSerial: Number(worldRef?.liveIdSerial) || 1,
lastBirthAt: Number(worldRef?.lastBirthAt) || -999,
maxGeneration: Number(worldRef?.maxGeneration) || 1,
colonyMood: worldRef?.colonyMood?.id || worldRef?.colonyMood || "relaxed",
};
}
function compactWorld(worldRef) {
const mood = worldRef?.colonyMood?.id || worldRef?.colonyMood || "relaxed";
return [
worldRef?.fieldType || "garden",
q(worldRef?.time, 10),
q(worldRef?.day, 1, 1),
q(worldRef?.cameraX, 1),
q(worldRef?.cameraY, 1),
worldRef?.weather || "sunny",
q(worldRef?.nextWeatherChange, 10),
q(worldRef?.deadCount, 1),
q(worldRef?.liveIdNext, 1, 1),
q(worldRef?.liveIdSerial, 1, 1),
q(worldRef?.lastBirthAt, 10, -9990),
q(worldRef?.maxGeneration, 1, 1),
mood,
];
}
function applyCompactWorld(worldRef, arr = []) {
worldRef.fieldType = arr[0] || "garden";
worldRef.time = u(arr[1], 10);
worldRef.day = u(arr[2], 1, 1);
worldRef.cameraX = u(arr[3], 1);
worldRef.cameraY = u(arr[4], 1);
worldRef.weather = arr[5] || "sunny";
worldRef.nextWeatherChange = u(arr[6], 10);
worldRef.deadCount = u(arr[7], 1);
worldRef.liveIdNext = Math.max(1, u(arr[8], 1, 1));
worldRef.liveIdSerial = Math.max(1, u(arr[9], 1, 1));
worldRef.lastBirthAt = u(arr[10], 10, -999);
worldRef.maxGeneration = Math.max(1, u(arr[11], 1, 1));
const moodId = arr[12] || "relaxed";
worldRef.colonyMood = worldRef.colonyMoodDefinition?.(moodId) || worldRef.colonyMoodDefinition?.("relaxed") || { id: moodId, label: moodId, effects: { personality: {} } };
}
function compactBehavior(b = null, tarinaiIndex = new Map(), itemIndex = new Map()) {
if (!b || typeof b !== "object") return null;
const ref = b.targetRef || b.target || null;
let target = null;
if (ref?.kind === "tarinai") target = ["t", tarinaiIndex.get(ref.id) ?? -1];
else if (ref?.kind === "item") target = ["i", itemIndex.get(ref.id) ?? -1];
else if (Number.isFinite(ref?.x) && Number.isFinite(ref?.y)) target = ["p", q(ref.x, 1), q(ref.y, 1)];
return [b.id || "", b.need || "", b.subNeed || "", b.phase || "", b.forced ? 1 : 0, target].filter((v, i) => i < 5 || v);
}
function expandBehavior(arr = null) {
if (!Array.isArray(arr) || !arr[0]) return null;
return {
id: arr[0] || "",
need: arr[1] || "",
subNeed: arr[2] || "",
phase: arr[3] || "",
forced: !!arr[4],
};
}
function compactRelationships(t, tarinaiIndex) {
const out = [];
for (const [id, rel] of Object.entries(t.relationships || {})) {
const idx = tarinaiIndex.get(id);
if (!Number.isInteger(idx) || idx < 0) continue;
const affinity = Number(rel?.affinity) || 0;
const fear = Number(rel?.fear) || 0;
const wins = Number(rel?.fightsWon) || 0;
const losses = Number(rel?.fightsLost) || 0;
if (Math.abs(affinity) < 1 && fear < 1 && wins < 1 && losses < 1) continue;
out.push([idx, q(affinity, 10), q(fear, 10), q(wins, 1), q(losses, 1)]);
}
return out;
}
function expandRelationships(rows = [], tarinaiList = [], worldRef = null) {
const out = {};
for (const row of rows || []) {
if (!Array.isArray(row)) continue;
const other = tarinaiList[row[0]];
if (!other?.id) continue;
out[other.id] = {
affinity: u(row[1], 10),
fear: u(row[2], 10),
fightsWon: u(row[3], 1),
fightsLost: u(row[4], 1),
lastEvent: (u(row[3], 1) || u(row[4], 1)) ? "fight" : "",
lastTime: worldRef?.time || 0,
};
}
return out;
}
function compactTarinai(t, idx, familyIndex, tarinaiIndex, itemIndex) {
const parentIdx = (t.parents || []).map(id => familyIndex.get(id)).filter(Number.isInteger);
const childIdx = (t.children || []).map(id => familyIndex.get(id)).filter(Number.isInteger);
const timers = [
q(t.reproductionTimer, 10), q(t.fightCooldown, 10), q(t.loveMochiTimer, 10), q(t.fightMochiTimer, 10),
q(t.zunchiDiseaseSeverity, 10), q(t.zunchiStain, 10), q(t.postBirthPeaceTimer, 10), q(t.awakeLockTimer, 10),
q(t.itemEffectTimers?.laxative, 10), q(t.itemEffectTimers?.ammo, 10), q(t.mercuryLifeMultiplier, 100),
];
const modes = [t.powerItemMode || "", t.sizeItemMode || "", t.lifeItemMode || ""];
const pinIdx = itemIndex.get(t.stuckPushpinId) ?? -1;
const nestIdx = itemIndex.get(t.insideNestBoxId) ?? -1;
return [
t.name || "", t.type || "smile", t.birthPersonality || null, t.currentPersonality || null, t.genetics || null,
q(t.x, 1), q(t.y, 1), q(t.vx, 10), q(t.vy, 10), q(t.scale, 1000), q(t.adultScale, 1000),
q(t.age, 10), q(t.lifeSpan, 10), q(t.birthTime, 10), q(t.generation, 1, 1), t.hasPaired ? 1 : 0,
q(t.hunger, 1), q(t.loneliness, 1), q(t.energy, 1), q(t.circadianSleepPressure, 1), needsToArray(t.needs || t.needRaw || {}),
t.state || "idle", flagPack(t), parentIdx, childIdx, compactRelationships(t, tarinaiIndex), timers, modes,
pinIdx, nestIdx, q(t.totalFightWins, 1), q(t.totalFightLosses, 1), t.deathReason || "", compactBehavior(t.behavior, tarinaiIndex, itemIndex),
];
}
function itemExtra(item, tarinaiIndex = new Map()) {
const type = item?.type || "";
if (item?.isStructure) return ["S", q(item.hp, 10), q(item.maxHp, 10), q(item.attachment, 10), q(item.usedCount, 1), tarinaiIndex.get(item.ownerId) ?? -1, item.carriedById ? (tarinaiIndex.get(item.carriedById) ?? -1) : -1, item.onHead ? 1 : 0, item.plushieSpriteId || ""];
if (type === "grass") return [q(item.growth, 1000), q(item.health, 1000), q(item.seedTimer, 10), q(item.eatenAmount, 10), q(item.fertilityBoost, 1000), q(item.lifeSpan, 10), q(item.wither, 1000)];
if (type === "bed") return [q(item.comfort, 1000), q(item.wear, 1000)];
if (type === "signboard") return [item.text || ""];
if (type === "duplicator") return [item.storedFoodType || "", item.storedFoodLabel || ""];
if (type === "ball") return [q(item.vx, 10), q(item.vy, 10), q(item.spin, 1000), q(item.spinVelocity, 1000)];
if (type === "zunchi") return [item.stage || "fresh", q(item.stageTimer, 10), q(item.fertility, 1000), q(item.freshness, 1000), item.zunchiVariant || ""];
if (type === "ant_nest") return [q(item.antCount, 1), Array.isArray(item.antWorkers) ? item.antWorkers.map(v => q(v, 10)) : [], q(item.queenSpawnAt, 10)];
if (type === "ant_corpse") return [item.workerSprite || "ant_worker", q(item.decayTimer, 10)];
if (type === "firecracker") return [q(item.fuseTimer, 10), q(item.fuseMax, 10)];
if (typeof isPinType === "function" && isPinType(type)) return [q(item.vx, 10), q(item.vy, 10), q(item.spin, 1000), q(item.spinVelocity, 1000), item.pinState || "loose", tarinaiIndex.get(item.pinTargetId) ?? -1, q(item.pinAttachAngle, 1000), q(item.pinAttachDistance, 10), q(item.pinOffsetY, 10)];
if (typeof isServingFoodType === "function" && isServingFoodType(type)) return [item.toolSize || "medium", q(item.foodServingsRemaining ?? item.amount, 10), q(item.foodServingsMax, 10)];
return null;
}
function compactItem(item, index, tarinaiIndex = new Map()) {
return [
item?.type || "", q(item?.x, 1), q(item?.y, 1), q(item?.r, 10), q(item?.amount, 10), q(item?.age, 10), q(item?.seed, 10), item?.dead ? 1 : 0, itemExtra(item, tarinaiIndex),
];
}
function applyItemExtra(item, extra, tarinaiList = []) {
if (!Array.isArray(extra)) return;
const type = item.type || "";
if (extra[0] === "S") {
item.isStructure = true;
item.hp = u(extra[1], 10);
item.maxHp = u(extra[2], 10);
item.attachment = u(extra[3], 10);
item.usedCount = u(extra[4], 1);
item.ownerId = tarinaiList[extra[5]]?.id || "";
item.carriedById = tarinaiList[extra[6]]?.id || "";
item.onHead = !!extra[7];
item.plushieSpriteId = extra[8] || item.plushieSpriteId || "";
const def = global.StructureRegistry?.get?.(type);
if (def) {
item.label = def.label;
item.roles = clonePlain(def.roles) || item.roles || {};
item.needEffects = clonePlain(def.needEffects) || item.needEffects || {};
}
return;
}
if (type === "grass") {
item.growth = u(extra[0], 1000); item.health = u(extra[1], 1000); item.seedTimer = u(extra[2], 10); item.eatenAmount = u(extra[3], 10); item.fertilityBoost = u(extra[4], 1000); item.lifeSpan = u(extra[5], 10); item.wither = u(extra[6], 1000);
} else if (type === "bed") {
item.comfort = u(extra[0], 1000); item.wear = u(extra[1], 1000);
} else if (type === "signboard") {
item.text = String(extra[0] || "");
} else if (type === "duplicator") {
item.storedFoodType = extra[0] || ""; item.storedFoodLabel = extra[1] || "";
item.roles.food = Boolean(item.storedFoodType);
} else if (type === "ball") {
item.vx = u(extra[0], 10); item.vy = u(extra[1], 10); item.spin = u(extra[2], 1000); item.spinVelocity = u(extra[3], 1000);
} else if (type === "zunchi") {
item.stage = extra[0] || "fresh"; item.stageTimer = u(extra[1], 10); item.fertility = u(extra[2], 1000); item.freshness = u(extra[3], 1000); item.zunchiVariant = extra[4] || item.zunchiVariant || "zunchi";
} else if (type === "ant_nest") {
item.antCount = u(extra[0], 1); item.antWorkers = Array.isArray(extra[1]) ? extra[1].map(v => u(v, 10)) : []; item.queenSpawnAt = u(extra[2], 10);
} else if (type === "ant_corpse") {
item.workerSprite = extra[0] || "ant_worker"; item.decayTimer = u(extra[1], 10);
} else if (type === "firecracker") {
item.fuseTimer = u(extra[0], 10); item.fuseMax = u(extra[1], 10);
} else if (typeof isPinType === "function" && isPinType(type)) {
item.vx = u(extra[0], 10); item.vy = u(extra[1], 10); item.spin = u(extra[2], 1000); item.spinVelocity = u(extra[3], 1000); item.pinState = extra[4] || "loose"; item.pinTargetId = tarinaiList[extra[5]]?.id || ""; item.pinAttachAngle = u(extra[6], 1000); item.pinAttachDistance = u(extra[7], 10); item.pinOffsetY = u(extra[8], 10);
} else if (typeof isServingFoodType === "function" && isServingFoodType(type)) {
item.toolSize = extra[0] || "medium"; item.foodServingsRemaining = u(extra[1], 10); item.foodServingsMax = u(extra[2], 10); item.amount = item.foodServingsRemaining;
}
}
function compactAnt(ant, itemIndex = new Map(), tarinaiIndex = new Map()) {
return [ant.kind || "worker", itemIndex.get(ant.homeId) ?? -1, tarinaiIndex.get(ant.targetId) ?? -1, q(ant.x, 1), q(ant.y, 1), q(ant.vx, 10), q(ant.vy, 10), q(ant.hp, 10), q(ant.maxHp, 10), ant.state || "search", q(ant.age, 10), q(ant.seed, 10), q(ant.returnTimer, 10), q(ant.foundingX, 1), q(ant.foundingY, 1), q(ant.foundingDelayUntil, 10), q(ant.foundingAttempts, 1), ant.dead ? 1 : 0];
}
function snapshotEntity(worldRef, entity, skip = new Set(["world", "target", "panicTarget", "targetRef"])) {
// Compatibility helper for non-save subsystems such as freeze. The exported save format no longer uses this broad snapshot.
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 tarinaiLive = (worldRef.tarinai || []).filter(t => t && !t.dead);
const itemsLive = (worldRef.items || []).filter(Boolean).filter(it => it && !it.dead);
const tarinaiIndex = new Map();
const familyIndex = new Map();
tarinaiLive.forEach((t, i) => {
if (t.id) tarinaiIndex.set(t.id, i);
if (t.familyKey) familyIndex.set(t.familyKey, i);
});
const itemIndex = new Map();
itemsLive.forEach((it, i) => { if (it.id) itemIndex.set(it.id, i); });
const now = Date.now();
return {
v: SNAPSHOT_VERSION,
a: "tcg",
c: now,
m: [worldRef.day || 1, (worldRef.tarinai || []).filter(t => t && !t.dead).length, worldRef.fieldType || "garden", q(worldRef.time || 0, 10), itemsLive.length],
w: compactWorld(worldRef),
t: tarinaiLive.map((t, i) => compactTarinai(t, i, familyIndex, tarinaiIndex, itemIndex)),
i: itemsLive.map((it, i) => compactItem(it, i, tarinaiIndex)),
n: (worldRef.ants || []).filter(a => a && !a.dead).map(a => compactAnt(a, itemIndex, tarinaiIndex)),
s: null,
};
}
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 expandTarinai(row, index, worldRef) {
const TarinaiClass = global.Tarinai || (typeof Tarinai !== "undefined" ? Tarinai : null);
if (!TarinaiClass) throw new Error("Tarinai is not available");
const opts = {
familyKey: `sf${index}`,
id: `st${index}`,
liveToken: 1,
name: row[0] || undefined,
type: row[1] || "smile",
birthPersonality: row[2] || undefined,
currentPersonality: row[3] || undefined,
genetics: row[4] || undefined,
x: u(row[5], 1), y: u(row[6], 1), vx: u(row[7], 10), vy: u(row[8], 10),
scale: u(row[9], 1000, 0.28), adultScale: u(row[10], 1000, 0.28),
age: u(row[11], 10), lifeSpan: u(row[12], 10, 1600), birthTime: u(row[13], 10), generation: Math.max(1, u(row[14], 1, 1)),
hasPaired: !!row[15], hunger: u(row[16], 1), loneliness: u(row[17], 1), energy: u(row[18], 1), circadianSleepPressure: u(row[19], 1),
needs: arrayToNeeds(row[20]), state: row[21] || "idle", behavior: expandBehavior(row[33]),
};
const t = new TarinaiClass(worldRef, opts);
t.familyKey = `sf${index}`;
t.id = `st${index}`;
t.liveToken = 1;
t.needRaw = { ...t.needs };
t.needDisplay = { ...t.needs };
t.previousNeeds = { ...t.needs };
flagApply(t, Number(row[22]) || 0);
t.__savedParentIdx = Array.isArray(row[23]) ? row[23] : [];
t.__savedChildIdx = Array.isArray(row[24]) ? row[24] : [];
t.__savedRelationships = Array.isArray(row[25]) ? row[25] : [];
const timers = Array.isArray(row[26]) ? row[26] : [];
t.reproductionTimer = u(timers[0], 10); t.fightCooldown = u(timers[1], 10); t.loveMochiTimer = u(timers[2], 10); t.fightMochiTimer = u(timers[3], 10);
t.zunchiDiseaseSeverity = u(timers[4], 10); t.zunchiStain = u(timers[5], 10); t.postBirthPeaceTimer = u(timers[6], 10); t.awakeLockTimer = u(timers[7], 10);
t.itemEffectTimers = { ...(t.itemEffectTimers || {}), laxative: u(timers[8], 10), ammo: u(timers[9], 10) };
t.mercuryLifeMultiplier = u(timers[10], 100, t.mercuryLifeMultiplier || 1);
const modes = Array.isArray(row[27]) ? row[27] : [];
t.powerItemMode = modes[0] || ""; t.sizeItemMode = modes[1] || ""; t.lifeItemMode = modes[2] || "";
t.__savedPinIdx = row[28]; t.__savedNestIdx = row[29];
t.totalFightWins = u(row[30], 1); t.totalFightLosses = u(row[31], 1); t.deathReason = row[32] || "";
if (typeof t.refreshEffectiveSize === "function") t.refreshEffectiveSize();
return t;
}
function restoreSnapshot(snapshot, worldRef = global.world) {
if (!snapshot || typeof snapshot !== "object" || snapshot.v !== SNAPSHOT_VERSION) throw new Error("invalid v2 save data");
if (!worldRef) throw new Error("world is not ready");
const fieldType = snapshot.w?.[0] || "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 = [];
worldRef.selected = null;
worldRef.events = global.TarinaiEvents || null;
worldRef.liveTarinai = new Map();
worldRef.liveIdFree = [];
applyCompactWorld(worldRef, snapshot.w || []);
const tarRows = Array.isArray(snapshot.t) ? snapshot.t : [];
for (let idx = 0; idx < tarRows.length; idx++) {
const t = expandTarinai(tarRows[idx] || [], idx, worldRef);
worldRef.tarinai.push(t);
if (t.id) worldRef.liveTarinai.set(t.id, { token: t.liveToken || 1, target: t });
}
for (const [idx, t] of worldRef.tarinai.entries()) {
t.parents = (t.__savedParentIdx || []).map(i => worldRef.tarinai[i]?.familyKey).filter(Boolean);
t.children = (t.__savedChildIdx || []).map(i => worldRef.tarinai[i]?.familyKey).filter(Boolean);
delete t.__savedParentIdx; delete t.__savedChildIdx;
}
const itemRows = Array.isArray(snapshot.i) ? snapshot.i : [];
for (let idx = 0; idx < itemRows.length; idx++) {
const row = itemRows[idx] || [];
const type = row[0] || "";
if (!type) continue;
let item = null;
if (Array.isArray(row[8]) && row[8][0] === "S" && global.StructureRegistry?.get?.(type)) {
const owner = worldRef.tarinai[row[8][5]] || null;
item = global.StructureRegistry.create(type, owner, u(row[1], 1), u(row[2], 1), worldRef);
} else {
item = new Item(type, u(row[1], 1), u(row[2], 1));
}
item.id = `si${idx}`;
item.r = u(row[3], 10, item.r || 12);
item.amount = u(row[4], 10, item.amount || 0);
item.age = u(row[5], 10, 0);
item.seed = u(row[6], 10, item.seed || 0);
item.dead = !!row[7];
applyItemExtra(item, row[8], worldRef.tarinai);
worldRef.items.push(item);
}
for (const t of worldRef.tarinai) {
if (Number.isInteger(t.__savedPinIdx) && t.__savedPinIdx >= 0) t.stuckPushpinId = worldRef.items[t.__savedPinIdx]?.id || null;
if (Number.isInteger(t.__savedNestIdx) && t.__savedNestIdx >= 0) t.insideNestBoxId = worldRef.items[t.__savedNestIdx]?.id || null;
t.relationships = expandRelationships(t.__savedRelationships || [], worldRef.tarinai, worldRef);
delete t.__savedPinIdx; delete t.__savedNestIdx; delete t.__savedRelationships;
}
const antRows = Array.isArray(snapshot.n) ? snapshot.n : [];
for (let idx = 0; idx < antRows.length; idx++) {
const row = antRows[idx] || [];
const ant = new AntActor(worldRef, {
id: `sa${idx}`,
kind: row[0] || "worker",
homeId: worldRef.items[row[1]]?.id || "",
targetId: worldRef.tarinai[row[2]]?.id || "",
targetToken: worldRef.tarinai[row[2]]?.liveToken || 0,
x: u(row[3], 1), y: u(row[4], 1), vx: u(row[5], 10), vy: u(row[6], 10), hp: u(row[7], 10), maxHp: u(row[8], 10), state: row[9] || "search", age: u(row[10], 10), seed: u(row[11], 10), returnTimer: u(row[12], 10), foundingX: u(row[13], 1), foundingY: u(row[14], 1), foundingDelayUntil: u(row[15], 10), foundingAttempts: u(row[16], 1), dead: !!row[17],
});
ant.targetRef = ant.targetId ? worldRef.liveTarinaiById?.(ant.targetId, ant.targetToken) || null : null;
worldRef.ants.push(ant);
}
worldRef.family = {};
worldRef.familyVersion = (worldRef.familyVersion || 0) + 1;
for (const t of worldRef.tarinai) worldRef.recordFamily?.(t);
worldRef.familyCleanVersion = null;
worldRef.familyTreeDirty = true;
worldRef.drawListDirty = true;
worldRef.terrainDirty = true;
worldRef.relationNotices = {};
worldRef.resolvedFightIds = {};
worldRef.eventCounters = {};
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-v2");
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);