"use strict"; (function (global) { const SaveSchema = global.TarinaiSaveSchema; if (!SaveSchema) throw new Error("TarinaiSaveSchema is not available for snapshot_system.js"); const SNAPSHOT_VERSION = 5; if (SNAPSHOT_VERSION !== SaveSchema.SNAPSHOT_VERSION) throw new Error("snapshot schema version mismatch"); const NEED_KEYS = ["food", "sleep", "health", "safety", "social", "fulfill"]; const DEFAULT_SKIP = new Set(["world", "target", "panicTarget", "targetRef", "targetEntity", "source"]); const { FIELD_IDS, GROUND_IDS, WEATHER_IDS, MOOD_IDS, STATE_IDS, POWER_MODE_IDS, SIZE_MODE_IDS, LIFE_MODE_IDS, PIN_STATE_IDS, STAGE_IDS, ITEM_TYPE_IDS, enumIndex, enumValue, } = SaveSchema; function spriteIds() { const list = typeof SPRITES !== "undefined" ? SPRITES : (global.SPRITES || []); return (list || []).map(s => s?.id).filter(Boolean); } function spriteIndex(id = "smile") { const ids = spriteIds(); const i = ids.indexOf(String(id || "")); return i >= 0 ? i : Math.max(0, ids.indexOf("smile")); } function spriteValue(index = 0) { const ids = spriteIds(); return ids[Number(index) | 0] || "smile"; } 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 mortonKeyXY(x = 0, y = 0) { const ix = Math.max(0, Math.min(65535, Math.round(Number(x) || 0))); const iy = Math.max(0, Math.min(65535, Math.round(Number(y) || 0))); let out = 0; for (let i = 0; i < 16; i++) out += (((ix >> i) & 1) * (2 ** (2 * i))) + (((iy >> i) & 1) * (2 ** (2 * i + 1))); return out; } 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 || "", familyKey: value.familyKey || "" }; if ((worldRef?.items || []).includes(value)) return { kind: "item", id: value.id || "", type: value.type || "" }; 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 === "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", groundType: worldRef?.groundType || "soil", time: Number(worldRef?.time) || 0, day: Number(worldRef?.day) || 1, weather: worldRef?.weather || "sunny", deadCount: Number(worldRef?.deadCount) || 0, lastBirthAt: Number(worldRef?.lastBirthAt) || -999, maxGeneration: Number(worldRef?.maxGeneration) || 1, colonyMood: worldRef?.colonyMood?.id || worldRef?.colonyMood || "relaxed", }; } function needsToArray(needs = {}) { return NEED_KEYS.map(k => q(needs[k], 1)); } function arrayToNeeds(arr = []) { const out = typeof global.createDefaultNeeds === "function" ? global.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 personalityToArray(p = {}) { return ["aggression", "openness", "sociability", "neuroticism"].map(k => q((Number(p?.[k]) || 0) + 1, 100, 100)); } function arrayToPersonality(arr = []) { const keys = ["aggression", "openness", "sociability", "neuroticism"]; const out = {}; keys.forEach((k, i) => { out[k] = u(arr[i], 100, 1) - 1; }); return out; } function geneticsToArray(g = {}) { return ["lifeSpanMul", "speedMul", "sizeMul", "fightMul"].map(k => q(Number(g?.[k]) || 1, 100, 100)); } function arrayToGenetics(arr = []) { const keys = ["lifeSpanMul", "speedMul", "sizeMul", "fightMul"]; const out = {}; keys.forEach((k, i) => { out[k] = u(arr[i], 100, 1); }); return out; } function flagPack(t = {}) { let flags = 0; if (t.favorite) flags |= 1 << 0; if (t.zunchiDisease) flags |= 1 << 1; if (t.sleepDisease) flags |= 1 << 2; if (t.explosionDisease) flags |= 1 << 3; if (t.fightDisease) flags |= 1 << 4; if (t.isZunchiSlave) flags |= 1 << 5; if (t.zunchiSlaveLocked) flags |= 1 << 6; if (t.birthRitualLeader) flags |= 1 << 7; if (t.hasPaired) flags |= 1 << 8; return flags; } function flagApply(t, flags = 0) { t.favorite = !!(flags & (1 << 0)); t.zunchiDisease = !!(flags & (1 << 1)); t.sleepDisease = !!(flags & (1 << 2)); t.explosionDisease = !!(flags & (1 << 3)); t.fightDisease = !!(flags & (1 << 4)); t.isZunchiSlave = !!(flags & (1 << 5)); t.zunchiSlaveLocked = !!(flags & (1 << 6)); t.birthRitualLeader = !!(flags & (1 << 7)); t.hasPaired = !!(flags & (1 << 8)); if (t.isZunchiSlave) t.zunchiSlaveLocked = true; } function compactWorld(worldRef) { const config = typeof CONFIG !== "undefined" ? CONFIG : (global.CONFIG || {}); const dayLength = Math.max(1, Number(config.dayLength || 120)); const worldTick10 = q((Math.max(1, worldRef?.day || 1) - 1) * dayLength + (Number(worldRef?.time) || 0), 10); return [ enumIndex(FIELD_IDS, worldRef?.fieldType || "garden"), enumIndex(GROUND_IDS, worldRef?.groundType || "soil"), enumIndex(MOOD_IDS, worldRef?.colonyMood?.id || worldRef?.colonyMood || "relaxed"), worldTick10, enumIndex(WEATHER_IDS, worldRef?.weather || "sunny"), q(worldRef?.lastBirthAt, 10, -9990), q(worldRef?.maxGeneration, 1, 1), q(worldRef?.deadCount, 1, 0), worldRef?.worldSeed || global.TarinaiSeedFactory?.ensureWorldSeed?.(worldRef) || "", q(worldRef?.birthSerial, 1, 0), ]; } function applyCompactWorld(worldRef, arr = []) { const config = typeof CONFIG !== "undefined" ? CONFIG : (global.CONFIG || {}); const dayLength = Math.max(1, Number(config.dayLength || 120)); const totalTime = u(arr[3], 10, 0); worldRef.fieldType = enumValue(FIELD_IDS, arr[0], "garden"); worldRef.weather = enumValue(WEATHER_IDS, arr[4], "sunny"); worldRef.worldSeed = String(arr[8] || "") || global.TarinaiSeedFactory?.createWorldSeed?.() || `w${Date.now().toString(36)}`; worldRef.birthSerial = Math.max(0, u(arr[9], 1, 0)); worldRef.day = Math.max(1, Math.floor(totalTime / dayLength) + 1); worldRef.time = totalTime - (worldRef.day - 1) * dayLength; worldRef.nextWeatherChange = rand(CONFIG.weatherChangeMin, CONFIG.weatherChangeMax); worldRef.deadCount = u(arr[7], 1, 0); worldRef.liveIdNext = 1; worldRef.liveIdSerial = 1; worldRef.lastBirthAt = u(arr[5], 10, -999); worldRef.maxGeneration = Math.max(1, u(arr[6], 1, 1)); const moodId = enumValue(MOOD_IDS, arr[2], "relaxed"); worldRef.colonyMood = worldRef.colonyMoodDefinition?.(moodId) || { id: moodId, label: moodId, effects: { personality: {} } }; worldRef.setGroundType?.(enumValue(GROUND_IDS, arr[1], "soil"), { silent: true, force: true }); } 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 = Math.min(15, Math.max(0, Math.round(Number(rel?.fightsWon) || 0))); const losses = Math.min(15, Math.max(0, Math.round(Number(rel?.fightsLost) || 0))); if (Math.abs(affinity) < 5 && fear < 5 && wins + losses < 1) continue; out.push([idx, q(affinity, 2), q(fear, 2), wins | (losses << 4)]); } 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; const fight = Number(row[3]) || 0; out[other.id] = { affinity: u(row[1], 2), fear: u(row[2], 2), fightsWon: fight & 15, fightsLost: (fight >> 4) & 15, lastEvent: fight ? "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 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.itemEffectTimers?.laxative, 10), q(t.itemEffectTimers?.ammo, 10), q(t.mercuryLifeMultiplier, 100, 100) ]; const modes = [enumIndex(POWER_MODE_IDS, t.powerItemMode || ""), enumIndex(SIZE_MODE_IDS, t.sizeItemMode || ""), enumIndex(LIFE_MODE_IDS, t.lifeItemMode || "")]; const pinIdx = itemIndex.get(t.stuckPushpinId) ?? -1; const nestIdx = itemIndex.get(t.insideNestBoxId) ?? -1; const state = t.state === "sleep" ? "sleep" : (STATE_IDS.includes(t.state) ? t.state : "idle"); let targetIdx = -1; if (state === "sleep") { const targetId = t.sleepSession?.targetId || t.pendingGrassBedWakeId || t.target?.id || ""; targetIdx = itemIndex.get(targetId) ?? itemIndex.get(t.target?.id) ?? -1; } return [ flagPack(t), t.birthSeed || t.familyKey || t.id || `s${idx}`, q(t.x, 1), q(t.y, 1), q(t.age, 10), q(t.generation, 1, 1), q(t.hunger, 1), q(t.energy, 1), q(t.circadianSleepPressure, 1), needsToArray(t.needs || t.needRaw || {}), personalityToArray(t.currentPersonality), parentIdx, compactRelationships(t, tarinaiIndex), timers, modes, pinIdx, nestIdx, [enumIndex(STATE_IDS, state), targetIdx] ]; } function expandTarinai(row, index, worldRef) { const TarinaiClass = global.Tarinai || (typeof Tarinai !== "undefined" ? Tarinai : null); if (!TarinaiClass) throw new Error("Tarinai is not available"); const needs = arrayToNeeds(row[9]); const age = u(row[4], 10); const birthSeed = String(row[1] || `s${index}`); const profile = global.TarinaiSeedFactory?.birthProfile?.(birthSeed) || {}; const opts = { familyKey: birthSeed, birthSeed, birthSerial: global.TarinaiSeedFactory?.serialFromBirthSeed?.(birthSeed) || 0, id: `st${index}`, liveToken: 1, currentPersonality: arrayToPersonality(row[10]), x: u(row[2], 1), y: u(row[3], 1), vx: 0, vy: 0, age, birthTime: (worldRef?.time || 0) - age, generation: Math.max(1, u(row[5], 1, 1)), hunger: u(row[6], 1), loneliness: Number(needs.social) || 0, energy: u(row[7], 1), circadianSleepPressure: u(row[8], 1), needs, state: enumValue(STATE_IDS, row[17]?.[0], "idle"), isZunchiSlave: Boolean((Number(row[0]) || 0) & (1 << 5)), }; const t = new TarinaiClass(worldRef, opts); t.familyKey = birthSeed; t.birthSeed = birthSeed; t.id = `st${index}`; t.liveToken = 1; t.needRaw = { ...needs }; t.needDisplay = { ...needs }; t.previousNeeds = { ...needs }; t.needRawBase = { ...needs }; flagApply(t, Number(row[0]) || 0); t.__savedParentIdx = Array.isArray(row[11]) ? row[11] : []; t.__savedRelationships = Array.isArray(row[12]) ? row[12] : []; const timers = Array.isArray(row[13]) ? row[13] : []; 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.itemEffectTimers = { ...(t.itemEffectTimers || {}), laxative: u(timers[6], 10), ammo: u(timers[7], 10) }; t.mercuryLifeMultiplier = u(timers[8], 100, t.mercuryLifeMultiplier || 1); const modes = Array.isArray(row[14]) ? row[14] : []; t.powerItemMode = enumValue(POWER_MODE_IDS, modes[0], ""); t.sizeItemMode = enumValue(SIZE_MODE_IDS, modes[1], ""); t.lifeItemMode = enumValue(LIFE_MODE_IDS, modes[2], ""); t.__savedPinIdx = row[15]; t.__savedNestIdx = row[16]; t.__savedStateInfo = row[17]; if (typeof t.refreshEffectiveSize === "function") t.refreshEffectiveSize(); return t; } function itemExtra(item, tarinaiIndex = new Map()) { const type = item?.type || ""; if (item?.isStructure) { const owner = tarinaiIndex.get(item.ownerId) ?? -1; const carried = item.carriedById ? (tarinaiIndex.get(item.carriedById) ?? -1) : -1; const consumed = item.singleUseConsumedById ? (tarinaiIndex.get(item.singleUseConsumedById) ?? -1) : -1; return [q(item.hp, 10), q(item.maxHp, 10), owner, carried, item.singleUsePending ? 1 : 0, consumed]; } if (type === "grass") return [q(item.growth, 100), q(item.health, 100), q(item.fertilityBoost, 100), item.manualGrass || item.placedByPlayer ? 1 : 0]; if (type === "zunchi") return [enumIndex(STAGE_IDS, item.stage || "fresh"), q(item.freshness, 100), q(item.fertility, 100)]; if (type === "signboard") return [item.text || ""]; if (type === "rotator") return [ q(item.angle, 1000, q(typeof global.defaultItemAngle === "function" ? global.defaultItemAngle(type) : 0, 1000)), q(item.rotatorSpeed, 1000), q(item.rotatorThickness || 12, 10), (Array.isArray(item.rotatorSegments) ? item.rotatorSegments : [[-78,0,78,0],[0,-52,0,52]]).slice(0, 96).map(seg => [q(seg[0], 1), q(seg[1], 1), q(seg[2], 1), q(seg[3], 1)]) ]; if (type === "duplicator") return [enumIndex(ITEM_TYPE_IDS, item.storedFoodType || "", -1)]; if (type === "ball") return [q(item.vx, 10), q(item.vy, 10)]; if (type === "ant_nest") return [q(item.antCount, 1), q(item.queenSpawnAt, 10)]; if (type === "ant_corpse") return [q(item.decayTimer, 10)]; if (type === "firecracker") return [q(item.fuseTimer, 10)]; if (typeof global.isPinType === "function" && global.isPinType(type)) return [enumIndex(PIN_STATE_IDS, item.pinState || "loose"), tarinaiIndex.get(item.pinTargetId) ?? -1, q(item.pinAttachAngle, 1000), q(item.pinAttachDistance, 10), q(item.pinOffsetY, 10)]; if (typeof global.isRotatableItemType === "function" && global.isRotatableItemType(type)) return [q(item.angle, 1000, q(typeof global.defaultItemAngle === "function" ? global.defaultItemAngle(type) : 0, 1000)), type === "gate_fence" && item.gateOpen ? 1 : 0]; if (typeof global.isServingFoodType === "function" && global.isServingFoodType(type)) return [q(item.foodServingsRemaining ?? item.amount, 10), q(item.foodServingsMax, 10)]; return []; } function compactItem(item, index, tarinaiIndex = new Map()) { const typeId = enumIndex(ITEM_TYPE_IDS, item?.type || "", -1); if (typeId < 0 || item?.type === "trace") return null; return [typeId, q(item?.x, 1), q(item?.y, 1), q(item?.amount ?? item?.hp, 10), itemExtra(item, tarinaiIndex)]; } function applyItemExtra(item, extra, tarinaiList = []) { if (!Array.isArray(extra)) return; const type = item.type || ""; if (item.isStructure) { item.hp = u(extra[0], 10, item.hp || 1); item.maxHp = u(extra[1], 10, item.maxHp || item.hp || 1); item.amount = Math.max(0.001, item.hp || item.amount || 1); item.ownerId = tarinaiList[extra[2]]?.id || ""; item.carriedById = tarinaiList[extra[3]]?.id || ""; item.singleUsePending = !!extra[4]; item.singleUseConsumedById = tarinaiList[extra[5]]?.id || ""; if (type === "plushie") { item.plushieSpriteId = "plushie_bear"; item.onHead = Boolean(item.carriedById); } 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], 100, item.growth || 0); item.health = u(extra[1], 100, item.health || 1); item.fertilityBoost = u(extra[2], 100, 0); item.manualGrass = !!extra[3]; item.placedByPlayer = !!extra[3]; global.TarinaiGrass?.normalize?.(item); } else if (type === "zunchi") { item.stage = enumValue(STAGE_IDS, extra[0], "fresh"); item.freshness = u(extra[1], 100, item.freshness || 1); item.fertility = u(extra[2], 100, item.fertility || 1); } else if (type === "signboard") { item.text = String(extra[0] || ""); } else if (type === "rotator") { let e = extra; if (typeof e[0] === "string") { try { e = JSON.parse(e[0]); } catch (_) { e = []; } } const fallback = typeof global.defaultItemAngle === "function" ? global.defaultItemAngle(type) : 0; item.angle = typeof global.normalizedItemAngle === "function" ? global.normalizedItemAngle(u(e[0], 1000, fallback), fallback) : u(e[0], 1000, fallback); item.rotatorSpeed = u(e[1], 1000, item.rotatorSpeed || 0); item.rotatorThickness = Math.max(4, Math.min(34, u(e[2], 10, item.rotatorThickness || 12))); item.rotatorSegments = (Array.isArray(e[3]) ? e[3] : [[-78,0,78,0],[0,-52,0,52]]).slice(0,96).map(seg => [u(seg[0],1), u(seg[1],1), u(seg[2],1), u(seg[3],1)]).filter(seg => Math.hypot(seg[2]-seg[0], seg[3]-seg[1]) >= 4); if (!item.rotatorSegments.length) item.rotatorSegments = [[-78,0,78,0]]; const extent = Math.max(...item.rotatorSegments.flatMap(seg => [Math.hypot(seg[0], seg[1]), Math.hypot(seg[2], seg[3])]), 64) + item.rotatorThickness + 8; item.r = Math.max(item.r || 64, Math.min(460, extent)); } else if (type === "duplicator") { item.storedFoodType = enumValue(ITEM_TYPE_IDS, extra[0], ""); item.storedFoodLabel = global.foodLabel ? global.foodLabel(item.storedFoodType) : item.storedFoodType; if (item.roles) item.roles.food = Boolean(item.storedFoodType); } else if (type === "ball") { item.vx = u(extra[0], 10); item.vy = u(extra[1], 10); } else if (type === "ant_nest") { item.antCount = u(extra[0], 1); item.antWorkers = []; item.queenSpawnAt = u(extra[1], 10); } else if (type === "ant_corpse") { item.workerSprite = "ant_worker"; item.decayTimer = u(extra[0], 10); } else if (type === "firecracker") { item.fuseTimer = u(extra[0], 10); item.fuseMax = Math.max(item.fuseMax || 5, item.fuseTimer || 0); } else if (typeof global.isPinType === "function" && global.isPinType(type)) { item.pinState = enumValue(PIN_STATE_IDS, extra[0], "loose"); item.pinTargetId = tarinaiList[extra[1]]?.id || ""; item.pinAttachAngle = u(extra[2], 1000); item.pinAttachDistance = u(extra[3], 10); item.pinOffsetY = u(extra[4], 10); } else if (typeof global.isRotatableItemType === "function" && global.isRotatableItemType(type)) { const fallback = typeof global.defaultItemAngle === "function" ? global.defaultItemAngle(type) : 0; item.angle = typeof global.normalizedItemAngle === "function" ? global.normalizedItemAngle(u(extra[0], 1000, fallback), fallback) : u(extra[0], 1000, fallback); if (type === "gate_fence") item.gateOpen = !!extra[1]; } else if (typeof global.isServingFoodType === "function" && global.isServingFoodType(type)) { item.foodServingsRemaining = u(extra[0], 10, item.amount || 1); item.foodServingsMax = u(extra[1], 10, item.foodServingsRemaining || item.amount || 1); item.amount = item.foodServingsRemaining; } } function createSnapshot(worldRef = global.world) { if (!worldRef) throw new Error("world is not ready"); const tarinaiLive = (worldRef.tarinai || []) .filter(t => t && !t.dead) .slice() .sort((a, b) => mortonKeyXY(a.x, a.y) - mortonKeyXY(b.x, b.y) || String(a.id || "").localeCompare(String(b.id || ""))); 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 itemRecords = []; for (const it of (worldRef.items || [])) { if (!it || it.dead || it.type === "trace" || enumIndex(ITEM_TYPE_IDS, it.type || "", -1) < 0) continue; const row = compactItem(it, itemRecords.length, tarinaiIndex); if (row) itemRecords.push({ item: it, row }); } itemRecords.sort((a, b) => (a.row[0] || 0) - (b.row[0] || 0) || mortonKeyXY(a.row[1], a.row[2]) - mortonKeyXY(b.row[1], b.row[2]) || String(a.item?.id || "").localeCompare(String(b.item?.id || ""))); const itemIndex = new Map(); itemRecords.forEach((rec, i) => { if (rec.item?.id) itemIndex.set(rec.item.id, i); }); const itemRows = itemRecords.map(rec => rec.row); return { v: SNAPSHOT_VERSION, a: "tj1", c: Date.now(), m: [worldRef.day || 1, tarinaiLive.length, worldRef.fieldType || "garden", q(worldRef.time || 0, 10), itemRows.length, worldRef.groundType || "soil"], w: compactWorld(worldRef), t: tarinaiLive.map((t, i) => compactTarinai(t, i, familyIndex, tarinaiIndex, itemIndex)), i: itemRows, }; } function restoreSnapshot(snapshot, worldRef = global.world) { if (!snapshot || typeof snapshot !== "object" || snapshot.v !== SNAPSHOT_VERSION || snapshot.a !== "tj1") throw new Error("invalid light save data"); if (!worldRef) throw new Error("world is not ready"); const fieldType = enumValue(FIELD_IDS, snapshot.w?.[0], "garden"); if (typeof global.applyFieldLayout === "function") global.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 t of worldRef.tarinai) { t.parents = (t.__savedParentIdx || []).map(i => worldRef.tarinai[i]?.familyKey).filter(Boolean); t.children = []; } for (const child of worldRef.tarinai) { for (const parentIdx of child.__savedParentIdx || []) { const parent = worldRef.tarinai[parentIdx]; if (parent?.familyKey && child.familyKey && !parent.children.includes(child.familyKey)) parent.children.push(child.familyKey); } } const liveByFamilyKey = new Map((worldRef.tarinai || []).map(t => [t.familyKey, t]).filter(([id]) => id)); for (const t of worldRef.tarinai) { t.parentNames = (t.parents || []).map(id => liveByFamilyKey.get(id)?.name || "").filter(Boolean); delete t.__savedParentIdx; } const itemRows = Array.isArray(snapshot.i) ? snapshot.i : []; for (let idx = 0; idx < itemRows.length; idx++) { const row = itemRows[idx] || []; const type = enumValue(ITEM_TYPE_IDS, row[0], ""); if (!type) continue; const extra = Array.isArray(row[4]) ? row[4] : []; const owner = global.StructureRegistry?.get?.(type) ? worldRef.tarinai[extra[2]] || null : null; const item = global.StructureRegistry?.get?.(type) ? global.StructureRegistry.create(type, owner, u(row[1], 1), u(row[2], 1), worldRef) : new Item(type, u(row[1], 1), u(row[2], 1)); item.id = `li${idx}`; item.world = worldRef; item.amount = u(row[3], 10, item.amount || 1); applyItemExtra(item, extra, 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; const stateInfo = Array.isArray(t.__savedStateInfo) ? t.__savedStateInfo : []; const state = enumValue(STATE_IDS, stateInfo[0], "idle"); if (state === "sleep") { const sleepTarget = worldRef.items[stateInfo[1]] || null; t.target = sleepTarget; t.sleeping = true; t.state = "sleep"; t.sleepSession = { startedAt: worldRef.time || 0, minDuration: 6, targetEnergy: 86, maxDuration: 36, targetId: sleepTarget?.id || null, consumesGrassBedOnWake: sleepTarget?.type === "grass_bed" }; if (sleepTarget?.type === "grass_bed") { t.pendingGrassBedWakeId = sleepTarget.id; sleepTarget.singleUsePending = true; sleepTarget.singleUseConsumedById = t.id; } } else { t.state = "idle"; t.sleeping = false; t.target = null; t.sleepSession = null; } t.relationships = expandRelationships(t.__savedRelationships || [], worldRef.tarinai, worldRef); delete t.__savedPinIdx; delete t.__savedNestIdx; delete t.__savedRelationships; delete t.__savedStateInfo; } worldRef.liveIdNext = Math.max(1, worldRef.tarinai.length + 1); worldRef.liveIdSerial = Math.max(1, worldRef.tarinai.length + 1); worldRef.birthSerial = Math.max(Number(worldRef.birthSerial) || 0, ...worldRef.tarinai.map(t => Number(t.birthSerial) || 0)); 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 = {}; worldRef.updateItemCounts?.(); worldRef.enforceGrassLimit?.("load-grass-limit"); worldRef.compactItems?.(); worldRef.updateItemCounts?.(); worldRef.updateEffectCounts?.(); worldRef.rebuildSpatial?.(true); worldRef.markTerrainDirty?.("load"); worldRef.clampCamera?.(); worldRef.emit?.("snapshot:restored", { snapshotVersion: SNAPSHOT_VERSION }); return { world: worldRef, snapshotVersion: SNAPSHOT_VERSION }; } global.TarinaiSnapshot = { version: SNAPSHOT_VERSION, clonePlain, copyOwnData, applyOwnData, refFor, resolveRef, worldData, createSnapshot, restoreSnapshot, }; })(typeof window !== "undefined" ? window : globalThis);