"use strict"; (function (global) { const SaveSchema = global.TarinaiSaveSchema; if (!SaveSchema) throw new Error("TarinaiSaveSchema is not available for snapshot_system.js"); const SNAPSHOT_VERSION = SaveSchema.SNAPSHOT_VERSION; 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, canonicalMoodId, moodValue, STATE_IDS, POWER_MODE_IDS, SIZE_MODE_IDS, LIFE_MODE_IDS, PIN_STATE_IDS, STAGE_IDS, ITEM_TYPE_IDS, itemTypeIndex, enumIndex, enumValue, } = SaveSchema; 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; } const birthHash32 = global.TarinaiCoreHelpers?.birthHash32 || (seed => { const match = /^b[0-9a-z]+_([0-9a-z]+)$/i.exec(String(seed || "")); if (match) { const parsed = parseInt(match[1], 36); if (Number.isFinite(parsed)) return parsed >>> 0; } let h = 2166136261; const value = String(seed || ""); for (let i = 0; i < value.length; i++) { h ^= value.charCodeAt(i); h = Math.imul(h, 16777619); } return h >>> 0; }); function compactBirthDescriptor(t = null, fallbackSerial = 1) { const seed = String(t?.birthSeed || t?.familyKey || t?.id || ""); const parsed = global.TarinaiSeedFactory?.serialFromBirthSeed?.(seed) || 0; const serial = Math.max(1, Math.floor(Number(t?.birthSerial) || parsed || fallbackSerial || 1)); return [serial, birthHash32(seed)]; } function expandBirthDescriptor(descriptor = [], worldSeed = "", fallbackSerial = 1) { const serial = Math.max(1, Math.floor(Number(descriptor?.[0]) || fallbackSerial || 1)); const expected = global.TarinaiSeedFactory?.childSeed?.(worldSeed, serial, []) || `b${serial.toString(36)}_0`; const hash = Number.isFinite(Number(descriptor?.[1])) ? (Number(descriptor[1]) >>> 0) : birthHash32(expected); const expectedHash = birthHash32(expected); return { serial, seed: hash === expectedHash ? expected : `b${serial.toString(36)}_${hash.toString(36)}`, }; } 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 cloneSnapshotPlain(value, depth = 0) { if (value == null) return value; const type = typeof value; if (type === "number") return Number.isFinite(value) ? value : null; 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 = cloneSnapshotPlain(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 = cloneSnapshotPlain(v, depth + 1); if (c !== undefined) out[key] = c; } return out; } function cloneEntityState(entity, skip = []) { const blocked = new Set(["world", "id", ...skip]); const out = {}; for (const [key, value] of Object.entries(entity || {})) { if (blocked.has(key) || DEFAULT_SKIP.has(key)) continue; const cloned = cloneSnapshotPlain(value); if (cloned !== undefined) out[key] = cloned; } return out; } function compactSnapshotExtension(worldRef, tarinaiLive, itemRecords) { const tarIds = tarinaiLive.map(t => String(t?.id || "")); const itemIds = itemRecords.map(rec => String(rec?.item?.id || "")); const tarStates = tarinaiLive.map(t => { // SV-11 is intentionally not serialized: actionCooldowns remain session-local. const state = cloneEntityState(t, [ "liveToken", "familyKey", "birthSeed", "birthSerial", "parents", "parentNames", "children", "actionCooldowns", "relationships", "target", "panicTarget", "targetRef", "targetEntity", "source", ]); state.__targetId = String(t?.target?.id || t?.targetId || ""); state.__panicTargetId = String(t?.panicTarget?.id || ""); state.__relationships = cloneSnapshotPlain(t?.relationships || {}) || {}; return state; }); const itemStates = itemRecords.map(rec => cloneEntityState(rec?.item, ["type", "targetRef", "source"])); const antStates = (worldRef?.ants || []).filter(a => a && !a.dead).map(a => { const state = cloneEntityState(a, ["targetRef", "source"]); state.id = String(a.id || ""); return state; }); return { ids: { t: tarIds, i: itemIds }, w: { nextWeatherChange: finiteOrNull(worldRef?.nextWeatherChange), effects: cloneSnapshotPlain((worldRef?.effects || []).slice(-256)) || [], logs: cloneSnapshotPlain((worldRef?.logs || []).slice(-512)) || [], relationNotices: cloneSnapshotPlain(worldRef?.relationNotices || {}) || {}, resolvedFightIds: cloneSnapshotPlain(worldRef?.resolvedFightIds || {}) || {}, eventCounters: cloneSnapshotPlain(worldRef?.eventCounters || {}) || {}, }, t: tarStates, i: itemStates, a: antStates, r: cloneSnapshotPlain((worldRef?.residues || []).filter(r => r && !r.dead && Number(r.amount || 0) > 0)) || [], }; } const ID_KEYED_SNAPSHOT_MAPS = new Set(["__relationships", "relationships"]); function snapshotFieldContainsEntityId(key = "") { const name = String(key || ""); return name === "__targetId" || name === "__panicTargetId" || /(?:Id|Ids)$/.test(name); } function remapSnapshotValue(value, idMap, depth = 0, fieldName = "") { if (depth > 12 || value == null) return value; if (typeof value === "string") { return snapshotFieldContainsEntityId(fieldName) ? (idMap.get(value) || value) : value; } if (Array.isArray(value)) return value.map(v => remapSnapshotValue(v, idMap, depth + 1, fieldName)); if (!isPlainObject(value)) return value; const out = {}; const remapKeys = ID_KEYED_SNAPSHOT_MAPS.has(String(fieldName || "")); for (const [key, child] of Object.entries(value)) { const mappedKey = remapKeys ? (idMap.get(key) || key) : key; out[mappedKey] = remapSnapshotValue(child, idMap, depth + 1, key); } return out; } function applySnapshotExtension(extension, worldRef) { if (!extension || typeof extension !== "object") return false; const oldTarIds = Array.isArray(extension.ids?.t) ? extension.ids.t : []; const oldItemIds = Array.isArray(extension.ids?.i) ? extension.ids.i : []; const idMap = new Map(); oldTarIds.forEach((id, index) => { if (id && worldRef.tarinai?.[index]?.id) idMap.set(String(id), worldRef.tarinai[index].id); }); oldItemIds.forEach((id, index) => { if (id && worldRef.items?.[index]?.id) idMap.set(String(id), worldRef.items[index].id); }); const tarStates = Array.isArray(extension.t) ? extension.t : []; for (let index = 0; index < worldRef.tarinai.length; index++) { const t = worldRef.tarinai[index]; const raw = isPlainObject(tarStates[index]) ? tarStates[index] : null; if (!t || !raw) continue; const state = remapSnapshotValue(raw, idMap); const targetId = String(state.__targetId || ""); const panicTargetId = String(state.__panicTargetId || ""); const relationships = isPlainObject(state.__relationships) ? state.__relationships : null; delete state.__targetId; delete state.__panicTargetId; delete state.__relationships; delete state.actionCooldowns; Object.assign(t, state); // Explicitly preserve the requested exception. t.actionCooldowns = {}; if (relationships) t.relationships = relationships; t.target = targetId ? (worldRef.tarinai.find(v => v?.id === targetId) || worldRef.items.find(v => v?.id === targetId) || null) : null; t.panicTarget = panicTargetId ? (worldRef.tarinai.find(v => v?.id === panicTargetId) || worldRef.items.find(v => v?.id === panicTargetId) || null) : null; if (t.sleepSession && typeof t.sleepSession === "object" && t.sleepSession.targetId) { t.sleepSession.targetId = idMap.get(String(t.sleepSession.targetId)) || String(t.sleepSession.targetId); } if (typeof t.refreshEffectiveSize === "function") { const savedEnergy = Number(t.energy); t.refreshEffectiveSize(); if (Number.isFinite(savedEnergy)) t.energy = Math.max(0, Math.min(Number(t.maxEnergy) || 240, savedEnergy)); } } const itemStates = Array.isArray(extension.i) ? extension.i : []; for (let index = 0; index < worldRef.items.length; index++) { const item = worldRef.items[index]; const raw = isPlainObject(itemStates[index]) ? itemStates[index] : null; if (!item || !raw) continue; Object.assign(item, remapSnapshotValue(raw, idMap)); item.world = worldRef; if (item.type === "grass") { if (!Number.isFinite(Number(item.grassStage))) item.grassStage = global.grassStageFromGrowth?.(item.growth, item.amount); global.TarinaiGrass?.normalize?.(item); } } worldRef.residues = []; worldRef.residueSerial = 0; for (const row of (Array.isArray(extension.r) ? extension.r : [])) { if (!row || typeof row !== "object") continue; worldRef.addResidue?.(String(row.type || "trace"), Number(row.x) || 0, Number(row.y) || 0, remapSnapshotValue(row, idMap)); } worldRef.rebuildResidueIndex?.(); worldRef.ants = []; const antRows = Array.isArray(extension.a) ? extension.a : []; const AntClass = global.AntActor || (typeof AntActor !== "undefined" ? AntActor : null); if (AntClass) { for (const row of antRows) { if (!isPlainObject(row)) continue; const state = remapSnapshotValue(row, idMap); const ant = new AntClass(worldRef, state); Object.assign(ant, state); ant.world = worldRef; ant.targetRef = ant.targetId ? (worldRef.tarinai.find(t => t?.id === ant.targetId) || null) : null; worldRef.ants.push(ant); } } const worldState = isPlainObject(extension.w) ? remapSnapshotValue(extension.w, idMap) : {}; if (Number.isFinite(Number(worldState.nextWeatherChange))) worldRef.nextWeatherChange = Number(worldState.nextWeatherChange); worldRef.effects = Array.isArray(worldState.effects) ? worldState.effects : []; worldRef.logs = Array.isArray(worldState.logs) ? worldState.logs : []; worldRef.relationNotices = isPlainObject(worldState.relationNotices) ? worldState.relationNotices : {}; worldRef.resolvedFightIds = isPlainObject(worldState.resolvedFightIds) ? worldState.resolvedFightIds : {}; worldRef.eventCounters = isPlainObject(worldState.eventCounters) ? worldState.eventCounters : {}; return true; } function needsToArray(needs = {}) { return NEED_KEYS.map(k => q(needs[k], 1)); } function arrayToNeeds(arr = []) { const out = global.createDefaultNeeds(); 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 = {}) { const normalized = global.normalizeGenetics(g, ""); return [ q(Number(normalized?.lifeSpanMul) || 1, 100, 100), q(Number(normalized?.attackMul) || 1, 100, 100), q(Number(normalized?.speedMul) || 1, 100, 100), q(Number(normalized?.sizeMul) || 1, 100, 100), q(Number(normalized?.temperatureOffset) || 0, 10, 0), ]; } function arrayToGenetics(arr = []) { const out = { lifeSpanMul: u(arr[0], 100, 1), attackMul: u(arr[1], 100, 1), speedMul: u(arr[2], 100, 1), sizeMul: u(arr[3], 100, 1), temperatureOffset: u(arr[4], 10, 0), }; return global.normalizeGenetics(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; if (t.isTarinaiChampion) flags |= 1 << 9; if (t.isAcquiredTarinai) flags |= 1 << 10; if (t.mamekusaredakeDisease) flags |= 1 << 11; 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)); t.isTarinaiChampion = !!(flags & (1 << 9)); t.isAcquiredTarinai = !!(flags & (1 << 10)); t.mamekusaredakeDisease = !!(flags & (1 << 11)); 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 rawTime = Math.max(0, Number(worldRef?.time) || 0); const elapsedDays = Number.isFinite(Number(worldRef?.elapsedDays)) ? Math.max(0, Math.floor(Number(worldRef.elapsedDays) || 0)) : Math.max(0, Math.floor(rawTime / dayLength)); const timeOfDay = ((rawTime % dayLength) + dayLength) % dayLength; const worldTick10 = q(elapsedDays * dayLength + timeOfDay, 10); return [ enumIndex(FIELD_IDS, worldRef?.fieldType || "garden"), enumIndex(GROUND_IDS, worldRef?.groundType || "soil"), enumIndex(MOOD_IDS, canonicalMoodId?.(worldRef?.colonyMood?.id || worldRef?.colonyMood || "relaxed") || "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), q(worldRef?.tarinaiPopulationLimit, 1, 0), q(worldRef?.objectLimit, 1, 0), [], // legacy achievement slot 12 [], // legacy achievement slot 13 0, // legacy achievement slot 14 q(worldRef?.achievementLastInterventionAt, 10, 0), q(worldRef?.achievementNoDeathStartAt, 10, -10), 0, // legacy achievement slot 17 elapsedDays, 0, // legacy achievement slot 19 ]; } function applyCompactWorld(worldRef, arr = [], meta = []) { const config = typeof CONFIG !== "undefined" ? CONFIG : (global.CONFIG || {}); const dayLength = Math.max(1, Number(config.dayLength || 120)); const encodedTotalTime = Math.max(0, u(arr[3], 10, 0)); const hasEncodedElapsedDays = arr[18] !== null && arr[18] !== undefined && Number.isFinite(Number(arr[18])); const hasMetaElapsedDays = meta[3] !== null && meta[3] !== undefined && Number.isFinite(Number(meta[3])); const hasMetaDay = meta[0] !== null && meta[0] !== undefined && Number.isFinite(Number(meta[0])); const encodedElapsedDays = hasEncodedElapsedDays ? Math.max(0, Math.floor(Number(arr[18]) || 0)) : (hasMetaElapsedDays ? Math.max(0, Math.floor(Number(meta[3]) || 0)) : (hasMetaDay ? Math.max(0, Math.floor((Number(meta[0]) || 1) - 1)) : Math.max(0, Math.floor(encodedTotalTime / dayLength / 2)))); const timeOfDay = ((encodedTotalTime % dayLength) + dayLength) % dayLength; const totalTime = encodedElapsedDays * dayLength + timeOfDay; 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.tarinaiPopulationLimit = worldRef.normalizeColonyLimit?.(u(arr[10], 1, 0)) ?? Math.min(300, Math.max(0, u(arr[10], 1, 0))); worldRef.objectLimit = worldRef.normalizeColonyLimit?.(u(arr[11], 1, 0)) ?? Math.min(300, Math.max(0, u(arr[11], 1, 0))); worldRef.achievementLastInterventionAt = Math.max(0, u(arr[15], 10, totalTime)); worldRef.achievementNoDeathStartAt = u(arr[16], 10, -1); worldRef.time = Math.max(0, totalTime); worldRef.elapsedDays = encodedElapsedDays; worldRef.day = worldRef.elapsedDays + 1; 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 = moodValue?.(arr[2], "relaxed") || 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 finiteOrNull(value) { if (value === null || value === undefined || value === "") return null; const n = Number(value); return Number.isFinite(n) ? n : null; } function compactAchievementMetadata(worldRef, tarinaiLive = [], itemRecords = [], options = {}) { const isUnlocked = id => Boolean(global.TarinaiAchievements?.isUnlocked?.(id)); const needSlaveGenerations = !isUnlocked("natural_zunchi_slave_5_generations"); const needKingGenerations = !isUnlocked("natural_tarinai_king_3_generations"); const needEnemyFriend = !isUnlocked("enemy_enemy_friend"); const needSelfSufficient = !isUnlocked("self_sufficient"); const needIdleObserver = !isUnlocked("idle_observer_5_minutes"); const needSafeColony = !isUnlocked("safe_colony_25_5_minutes"); const needHappyColony = !isUnlocked("colony_happy"); const needEliteFew = !isUnlocked("elite_few"); const needOverprotective = !isUnlocked("overprotective"); const needSauna = !isUnlocked("sauna_cold_plunge"); const needQuickDelete = !isUnlocked("unplanned_city_30"); const needMinimalist = !isUnlocked("minimalist_happy"); const needPlayerPlacedMetadata = needQuickDelete || needMinimalist; const metadata = { w: [ [], // legacy lineage slot [], // legacy lineage slot 0, // legacy direct-feed slot 0, // legacy robot-clean slot; old saves are still imported below needEnemyFriend ? Math.max(0, Math.floor(Number(worldRef?.achievementEnemyAntKills || 0) || 0)) : 0, null, // legacy last-damage slot needSelfSufficient ? finiteOrNull(worldRef?.achievementSelfSufficientStartAt) : -1, null, // legacy last-direct-feed slot needIdleObserver ? finiteOrNull(worldRef?.achievementLastInterventionAt) : 0, needSafeColony ? finiteOrNull(worldRef?.achievementNoDeathStartAt) : -1, needHappyColony ? finiteOrNull(worldRef?.achievementHappyStartAt) : -1, needEliteFew ? finiteOrNull(worldRef?.achievementEliteFewStartAt) : -1, ], t: tarinaiLive.map(t => [ needOverprotective ? Math.max(0, Math.floor(Number(t?._achievementDirectFeedCount || 0) || 0)) : 0, needOverprotective ? Math.max(0, Math.floor(Number(t?._achievementDirectTreatmentCount || 0) || 0)) : 0, null, needSauna ? finiteOrNull(t?._achievementSaunaHotAt) : null, needSlaveGenerations ? Math.max(0, Math.floor(Number(t?._achievementNaturalSlaveLineageDepth || 0) || 0)) : 0, needKingGenerations ? Math.max(0, Math.floor(Number(t?._achievementNaturalKingLineageDepth || 0) || 0)) : 0, ]), i: itemRecords.map(rec => [ needPlayerPlacedMetadata && rec?.item?._achievementPlayerPlaced ? 1 : 0, needQuickDelete ? finiteOrNull(rec?.item?._achievementPlacedAt) : null, ]), }; if (options.includeAchievements) { const spellState = global.TarinaiAchievements?.exportSpellState?.(); if (Array.isArray(spellState)) metadata.s = spellState; } return metadata; } function applyAchievementMetadata(worldRef, metadata = null) { if (!worldRef || !metadata || typeof metadata !== "object") return false; const w = Array.isArray(metadata.w) ? metadata.w : []; const legacyRobotCleanCount = Math.max(0, Math.floor(Number(w[3] || 0) || 0)); global.TarinaiAchievements?.importWorldAchievementProgress?.({ robotCleanCount: legacyRobotCleanCount, source: "snapshot-metadata" }); worldRef.achievementEnemyAntKills = Math.max(0, Math.floor(Number(w[4] || 0) || 0)); worldRef.achievementSelfSufficientStartAt = w[6] != null && Number.isFinite(Number(w[6])) ? Number(w[6]) : -1; worldRef.achievementLastInterventionAt = w[8] != null && Number.isFinite(Number(w[8])) ? Number(w[8]) : Math.max(0, Number(worldRef.time || 0) || 0); worldRef.achievementNoDeathStartAt = w[9] != null && Number.isFinite(Number(w[9])) ? Number(w[9]) : -1; worldRef.achievementHappyStartAt = w[10] != null && Number.isFinite(Number(w[10])) ? Number(w[10]) : -1; worldRef.achievementEliteFewStartAt = w[11] != null && Number.isFinite(Number(w[11])) ? Number(w[11]) : -1; const tarRows = Array.isArray(metadata.t) ? metadata.t : []; (worldRef.tarinai || []).forEach((t, index) => { const row = Array.isArray(tarRows[index]) ? tarRows[index] : []; t._achievementDirectFeedCount = Math.max(0, Math.floor(Number(row[0] || 0) || 0)); t._achievementDirectTreatmentCount = Math.max(0, Math.floor(Number(row[1] || 0) || 0)); t._achievementFightMochiAt = null; t._achievementFightMochiWorld = null; t._achievementSaunaHotAt = row[3] != null && Number.isFinite(Number(row[3])) ? Number(row[3]) : null; t._achievementNaturalSlaveLineageDepth = Math.max(0, Math.floor(Number(row[4] || 0) || 0)); t._achievementNaturalKingLineageDepth = Math.max(0, Math.floor(Number(row[5] || 0) || 0)); }); const itemRows = Array.isArray(metadata.i) ? metadata.i : []; (worldRef.items || []).forEach((item, index) => { const row = Array.isArray(itemRows[index]) ? itemRows[index] : []; item._achievementPlayerPlaced = Boolean(row[0]); item._achievementPlayerPlacedCounted = item._achievementPlayerPlaced; item._achievementPlacedAt = row[1] != null && Number.isFinite(Number(row[1])) ? Number(row[1]) : null; }); worldRef.achievementPlayerPlacedActiveCount = (worldRef.items || []).reduce((count, item) => count + (item && !item.dead && item._achievementPlayerPlaced ? 1 : 0), 0); return 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))); out.push([idx, q(affinity, 2), q(fear, 2), wins | (losses << 4), String(rel?.lastEvent || ""), q(rel?.lastTime, 10)]); } 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: String(row[4] || ""), lastTime: u(row[5], 10, 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), q(t.itemEffectTimers?.sedative, 10), q(t.mamekusaredakeGrowth, 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 = STATE_IDS.includes(String(t.state || "")) ? String(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), compactBirthDescriptor(t, idx + 1), 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], geneticsToArray(t.genetics), [q(t.totalFightWins, 1), q(t.totalFightLosses, 1), q(t.tarinaiChampionSince, 10)] ]; } 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 birthIdentity = expandBirthDescriptor(row[1], worldRef?.worldSeed || "", index + 1); const birthSeed = birthIdentity.seed; const profile = global.TarinaiSeedFactory.birthProfile(birthSeed) || {}; const opts = { familyKey: birthSeed, birthSeed, birthSerial: birthIdentity.serial, id: `st${index}`, liveToken: 1, currentPersonality: arrayToPersonality(row[10]), genetics: Array.isArray(row[18]) && row[18].length ? arrayToGenetics(row[18]) : profile.genetics, 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: "idle", isZunchiSlave: Boolean((Number(row[0]) || 0) & (1 << 5)), isTarinaiChampion: Boolean((Number(row[0]) || 0) & (1 << 9)), isAcquiredTarinai: Boolean((Number(row[0]) || 0) & (1 << 10)), mamekusaredakeDisease: Boolean((Number(row[0]) || 0) & (1 << 11)), totalFightWins: Array.isArray(row[19]) ? u(row[19][0], 1) : 0, totalFightLosses: Array.isArray(row[19]) ? u(row[19][1], 1) : 0, tarinaiChampionSince: Array.isArray(row[19]) ? u(row[19][2], 10) : 0, }; 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.mamekusaredakeGrowth = u(timers[10], 100, 0); t.itemEffectTimers = { ...(t.itemEffectTimers || {}), laxative: u(timers[6], 10), ammo: u(timers[7], 10), sedative: u(timers[9], 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]; const savedEnergy = u(row[7], 1); if (typeof t.refreshEffectiveSize === "function") t.refreshEffectiveSize(); t.energy = Math.max(0, Math.min(Number(t.maxEnergy) || 240, savedEnergy)); return t; } function itemExtra(item, tarinaiIndex = new Map(), itemIndex = 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; const base = [q(item.hp, 10), q(item.maxHp, 10), owner, carried, item.singleUsePending ? 1 : 0, consumed, q(item.createdAt, 10)]; if (type === "grave") return base.concat([ String(item.memorialFamilyKey || ""), String(item.memorialName || ""), String(item.memorialType || ""), String(item.memorialDeathReason || ""), q(item.memorialDeathTime, 10, -1), String(item.memorialBuilderFamilyKey || ""), JSON.stringify(Array.isArray(item.memorialEligibleFamilyKeys) ? item.memorialEligibleFamilyKeys : []), item.memorialFavorite ? 1 : 0, item.memorialTarinaiKing ? 1 : 0, item.memorialZunchiSlave ? 1 : 0, ]); return base; } if (type === "grass") return [q(item.growth, 100), q(item.health, 100), q(item.fertilityBoost, 100), item.manualGrass || item.placedByPlayer ? 1 : 0, q(item.grassStage, 1), item.burning ? 1 : 0, q(item.burnTimer, 10)]; if (type === "zunchi") return [enumIndex(STAGE_IDS, item.stage || "fresh"), q(item.freshness, 100), q(item.fertility, 100), q(item.stageTimer, 10), q(item.age, 10), item.burning ? 1 : 0, q(item.burnTimer, 10)]; if (type === "signboard") return [item.text || ""]; if (type === "robot_cleaner") return [Math.max(0, Math.min(0x7f, Number(item.robotCleanerMask ?? 3) | 0)), item.robotCleanerHighSpeed ? 1 : 0]; if (type === "circuit_board") return [JSON.stringify(global.TarinaiCircuitBoardSystem?.serializeConfig?.(item) || item.circuitConfig || {})]; if (global.TarinaiPhysicsBodySystem.isPhysicsType(type)) { const packed = global.TarinaiPhysicsBodySystem.compactItemExtra(item, tarinaiIndex, itemIndex); if (packed) return packed; } if (global.TarinaiPhysicsBodySystem.isPhysicsType(type)) return { pf: 2, missing: true }; if (type === "duplicator") return [itemTypeIndex(item.storedFoodType || "", -1)]; if (type === "ball" || type === "balloon") 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" || type === "flame_firecracker") return [q(item.fuseTimer, 10)]; if (type === "sticky_bomb") return [q(item.fuseTimer, 10), tarinaiIndex.get(item.stickyBombCarrierId) ?? -1, q(item.stickyBombPassCount, 1)]; if (type === "fire") return [q(item.flameLife, 10)]; if (type === "nest_box") return [q(item.burnTimer, 10), q(item.nestFireEvacuationTimer, 10), item.burning ? 1 : 0, item.nestFireEvacuated ? 1 : 0]; if (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 (type === "fan") return [ q(item.angle, 1000, q(global.defaultItemAngle(type), 1000)), item.fanSwingOn ? 1 : 0, q(item.fanSwingCenter ?? item.angle ?? 0, 1000), q(item.fanSwingRange ?? (35 * Math.PI / 180), 1000), q(item.fanSwingSpeed ?? (48 * Math.PI / 180), 1000), q(item.fanSwingPhase || 0, 1000), q(item.fanBodyAngle ?? item.angle ?? 0, 1000), ]; if (type === "pressure_switch") { const targets = ["tarinai", "item", "time", "hunger_avg", "stress_avg", "sleep_avg", "low_hp_count", "sick_count", "temperature", "zunchi_count", "ant_count", "water_count"]; return [ Math.max(0, targets.indexOf(item.pressureTarget || "tarinai")), q(item.pressureMin ?? 1, 10), q(item.pressureMax ?? 300, 10), Math.max(60, Math.min(840, Number(item.pressureWidth || 220) | 0)), Math.max(60, Math.min(840, Number(item.pressureHeight || 160) | 0)), Math.max(0, Math.min(1435, Number(item.pressureTimeStart ?? 360) | 0)), Math.max(0, Math.min(1435, Number(item.pressureTimeEnd ?? 1080) | 0)), ]; } if (global.isRotatableItemType(type)) return [q(item.angle, 1000, q(global.defaultItemAngle(type), 1000)), type === "gate_fence" && item.gateOpen ? 1 : 0]; if (global.isServingFoodType(type)) return [q(item.foodServingsRemaining ?? item.amount, 10), q(item.foodServingsMax, 10)]; return []; } function compactItem(item, index, tarinaiIndex = new Map(), itemIndex = new Map()) { const typeId = itemTypeIndex(item?.type || "", -1); if (typeId < 0) return null; return [typeId, q(item?.x, 1), q(item?.y, 1), q(item?.amount ?? item?.hp, 10), itemExtra(item, tarinaiIndex, itemIndex)]; } function compactResidue(residue) { if (!residue || residue.dead || Number(residue.amount || 0) <= 0) return null; const typeCode = residue.type === "splat" ? 1 : (residue.type === "trace" ? 0 : -1); if (typeCode < 0) return null; return [ typeCode, q(residue.x, 1), q(residue.y, 1), q(residue.amount, 10), q(residue.r, 10, typeCode === 1 ? 260 : 160), q(residue.seed, 100, 0), ]; } function restoreResidueRow(worldRef, row) { if (!worldRef || !Array.isArray(row)) return null; const type = Number(row[0]) === 1 ? "splat" : "trace"; return worldRef.addResidue?.(type, u(row[1], 1), u(row[2], 1), { amount: u(row[3], 10, type === "splat" ? 260 : 220), r: u(row[4], 10, type === "splat" ? 26 : 16), seed: u(row[5], 100, 0), reason: "restore-residue", }) || null; } function applyItemExtra(item, extra, tarinaiList = [], itemList = []) { const type = item.type || ""; if (global.TarinaiPhysicsBodySystem.applyCompactExtra(item, extra, tarinaiList, itemList)) return; if (!Array.isArray(extra)) { if (global.TarinaiPhysicsBodySystem.isPhysicsType(type)) global.TarinaiPhysicsBodySystem.invalidateItem(item, "physics-extra-missing"); return; } 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 || ""; item.createdAt = Number.isFinite(Number(extra[6])) ? u(extra[6], 10, item.createdAt || NaN) : (Number.isFinite(Number(item.createdAt)) ? item.createdAt : NaN); if (type === "plushie") { item.onHead = Boolean(item.carriedById); } if (type === "grave") { item.memorialFamilyKey = String(extra[7] || ""); item.memorialName = String(extra[8] || ""); item.memorialType = String(extra[9] || ""); item.memorialDeathReason = String(extra[10] || ""); item.memorialDeathTime = Number(extra[11]) >= 0 ? u(extra[11], 10, null) : null; item.memorialBuilderFamilyKey = String(extra[12] || ""); try { item.memorialEligibleFamilyKeys = JSON.parse(String(extra[13] || "[]")); } catch (_) { item.memorialEligibleFamilyKeys = []; } if (!Array.isArray(item.memorialEligibleFamilyKeys)) item.memorialEligibleFamilyKeys = []; item.memorialFavorite = Boolean(extra[14]); item.memorialTarinaiKing = Boolean(extra[15]); item.memorialZunchiSlave = Boolean(extra[16]); } const def = global.StructureRegistry?.get?.(type); if (def) { item.label = def.label; item.roles = cloneSnapshotPlain(def.roles) || item.roles || {}; item.needEffects = cloneSnapshotPlain(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]; item.grassStage = u(extra[4], 1, global.grassStageFromGrowth?.(item.growth, item.amount) || 0); item.burning = Boolean(extra[5]); item.burnTimer = u(extra[6], 10, 0); 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); item.stageTimer = u(extra[3], 10, 0); item.age = u(extra[4], 10, item.age || 0); item.burning = Boolean(extra[5]); item.burnTimer = u(extra[6], 10, 0); } else if (type === "signboard") { item.text = String(extra[0] || ""); } else if (type === "robot_cleaner") { const mask = Number(extra[0]); item.robotCleanerMask = Number.isFinite(mask) ? Math.max(0, Math.min(0x7f, mask | 0)) : (global.TarinaiItemDynamicToolSystem?.robotCleanerDefaultMask?.() ?? 3); item.robotCleanerHighSpeed = Boolean(extra[1]); item.robotCleanerSpeed = global.TarinaiItemDynamicToolSystem?.robotCleanerEffectiveSpeed?.(item) ?? (item.robotCleanerHighSpeed ? 120 : 60); item.robotCleanerTargetId = ""; item.robotCleanerTargetType = ""; } else if (global.TarinaiPhysicsBodySystem.isPhysicsType(type)) { global.TarinaiPhysicsBodySystem.invalidateItem(item, "physics-extra-missing"); return; } else if (type === "duplicator") { item.storedFoodType = enumValue(ITEM_TYPE_IDS, extra[0], ""); item.storedFoodLabel = global.toolLabel ? global.toolLabel(item.storedFoodType) : item.storedFoodType; if (item.roles) item.roles.food = Boolean(item.storedFoodType); } else if (type === "ball" || type === "balloon") { 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.decayTimer = u(extra[0], 10); } else if (type === "firecracker" || type === "flame_firecracker") { item.fuseTimer = u(extra[0], 10); item.fuseMax = Math.max(item.fuseMax || 5, item.fuseTimer || 0); } else if (type === "sticky_bomb") { item.fuseTimer = u(extra[0], 10, item.fuseTimer || 12); item.fuseMax = Math.max(item.fuseMax || 12, item.fuseTimer || 0); item.stickyBombCarrierId = tarinaiList[extra[1]]?.id || ""; item.stickyBombPassCount = Math.max(0, u(extra[2], 1, 0)); } else if (type === "fire") { item.flameLife = u(extra[0], 10, item.flameLife || 6); } else if (type === "nest_box") { item.burnTimer = u(extra[0], 10, 0); item.nestFireEvacuationTimer = u(extra[1], 10, 0); item.burning = Boolean(extra[2]) || item.burnTimer > 0.02 || item.nestFireEvacuationTimer > 0.02; item.nestFireEvacuated = Boolean(extra[3]); if (item.burning) { item.flameWaterCooldown = 0; item.nestFireEffectCooldown = 0; } } else if (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 (type === "circuit_board") { let config = {}; try { config = JSON.parse(String(extra[0] || "{}")); } catch (_) { config = {}; } global.TarinaiCircuitBoardSystem?.applySerializedConfig?.(item, config); global.TarinaiCircuitBoardSystem?.evaluate?.(item); } else if (type === "fan") { const fallback = global.defaultItemAngle(type); item.angle = global.normalizedItemAngle(u(extra[0], 1000, fallback), fallback); item.fanSwingOn = Boolean(extra[1]); item.fanSwingCenter = global.normalizedItemAngle(u(extra[2], 1000, item.angle), item.angle); item.fanSwingRange = Math.max(0, Math.min(150 * Math.PI / 180, u(extra[3], 1000, 35 * Math.PI / 180))); item.fanSwingSpeed = Math.max(0, Math.min(360 * Math.PI / 180, u(extra[4], 1000, 48 * Math.PI / 180))); item.fanSwingPhase = u(extra[5], 1000, item.fanSwingPhase || 0); item.fanBodyAngle = global.normalizedItemAngle(u(extra[6], 1000, item.angle), item.angle); } else if (type === "pressure_switch") { const targets = ["tarinai", "item", "time", "hunger_avg", "stress_avg", "sleep_avg", "low_hp_count", "sick_count", "temperature", "zunchi_count", "ant_count", "water_count"]; item.pressureTarget = targets[Number(extra[0]) | 0] || "tarinai"; item.pressureMin = u(extra[1], 10, 1); item.pressureMax = u(extra[2], 10, 300); if (item.pressureMin > item.pressureMax) { const swap = item.pressureMin; item.pressureMin = item.pressureMax; item.pressureMax = swap; } item.pressureWidth = Math.max(60, Math.min(840, Number(extra[3] || 220) | 0)); item.pressureHeight = Math.max(60, Math.min(840, Number(extra[4] || 160) | 0)); item.pressureTimeStart = Math.max(0, Math.min(1435, Number(extra[5] ?? 360) | 0)); item.pressureTimeEnd = Math.max(0, Math.min(1435, Number(extra[6] ?? 1080) | 0)); item.signalActive = false; } else if (global.isRotatableItemType(type)) { const fallback = global.defaultItemAngle(type); item.angle = global.normalizedItemAngle(u(extra[0], 1000, fallback), fallback); if (type === "gate_fence") item.gateOpen = !!extra[1]; } else if (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; global.updateServingFoodVisualSize(item); } } function createSnapshot(worldRef = global.world, options = {}) { 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" || it.type === "splat") continue; const typeId = itemTypeIndex(it.type || "", -1); if (typeId < 0) continue; itemRecords.push({ item: it, sortTypeId: typeId, sortX: q(it.x, 1), sortY: q(it.y, 1) }); } itemRecords.sort((a, b) => (a.sortTypeId || 0) - (b.sortTypeId || 0) || mortonKeyXY(a.sortX, a.sortY) - mortonKeyXY(b.sortX, b.sortY) || 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 residueRows = (worldRef.residues || []) .filter(residue => residue && !residue.dead && Number(residue.amount || 0) > 0) .slice() .sort((a, b) => String(a.type || "").localeCompare(String(b.type || "")) || mortonKeyXY(a.x, a.y) - mortonKeyXY(b.x, b.y)) .map(compactResidue) .filter(Boolean); const itemRows = itemRecords.map((rec, i) => compactItem(rec.item, i, tarinaiIndex, itemIndex)).filter(Boolean); return { v: SNAPSHOT_VERSION, a: "tj1", m: [worldRef.day || 1, tarinaiLive.length, worldRef.fieldType || "garden", Math.max(0, Math.floor(Number(worldRef.elapsedDays ?? ((Number(worldRef.day || 1) || 1) - 1)) || 0))], w: compactWorld(worldRef), t: tarinaiLive.map((t, i) => compactTarinai(t, i, familyIndex, tarinaiIndex, itemIndex)), i: itemRows, r: residueRows, g: compactAchievementMetadata(worldRef, tarinaiLive, itemRecords, options), x: compactSnapshotExtension(worldRef, tarinaiLive, itemRecords), }; } 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(); applyCompactWorld(worldRef, snapshot.w || [], snapshot.m || []); 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] && typeof row[4] === "object")) ? 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); item.__savedExtra = extra; worldRef.items.push(item); } for (const item of worldRef.items) { applyItemExtra(item, item.__savedExtra || [], worldRef.tarinai, worldRef.items); delete item.__savedExtra; } for (const row of (Array.isArray(snapshot.r) ? snapshot.r : [])) restoreResidueRow(worldRef, row); 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; } applyAchievementMetadata(worldRef, snapshot.g || null); const updateNeedsRuntime = global.TarinaiNeedsRuntime?.updateNeeds; if (typeof updateNeedsRuntime === "function") { for (const t of worldRef.tarinai) updateNeedsRuntime(t, worldRef, 0); } const restoredExtendedState = applySnapshotExtension(snapshot.x || null, worldRef); // Legacy snapshots without extended state start from a neutral physical state. delete worldRef._restoreSettleUntil; if (!restoredExtendedState) for (const t of worldRef.tarinai) { if (!t || t.dead) continue; t.vx = 0; t.vy = 0; t.impulseVx = 0; t.impulseVy = 0; t.prevX = t.x; t.prevY = t.y; t._lastPhysicalVelocityX = 0; t._lastPhysicalVelocityY = 0; t.lastVelocityShockDamageAt = -999; t.lastPhysicalCollisionDamageAt = -999; } 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; if (!restoredExtendedState) { worldRef.relationNotices = {}; worldRef.resolvedFightIds = {}; worldRef.eventCounters = {}; } worldRef.rebuildTarinaiCountCache?.("restore"); worldRef.updateItemCounts?.(); worldRef.enforceGrassLimit?.("load-grass-limit"); worldRef.compactItems?.(); worldRef.updateItemCounts?.(); worldRef.updateEffectCounts?.(); global.TarinaiAchievements?.evaluateEvent?.(worldRef, "restore", { source: "snapshot" }); worldRef.rebuildSpatial?.(true); worldRef.markTerrainDirty?.("load"); worldRef.clampCamera?.(); const achievementImport = snapshot.g?.s ? global.TarinaiAchievements?.importSpellState?.(snapshot.g.s) || { included: true, unlockedAdded: 0, progressChanged: false } : { included: false, unlockedAdded: 0, progressChanged: false }; return { world: worldRef, snapshotVersion: SNAPSHOT_VERSION, achievementImport }; } global.TarinaiSnapshot = { version: SNAPSHOT_VERSION, createSnapshot, restoreSnapshot, }; if (global.__TARINAI_TEST__) { global.TarinaiSnapshotTestHooks = Object.freeze({ compactAchievementMetadata, applyAchievementMetadata, }); } })(typeof window !== "undefined" ? window : globalThis);