"use strict"; (function (global) { const SNAPSHOT_VERSION = 1; // Alpha note: save/import data is intentionally not stable yet. // During alpha, hash-text compatibility may be broken by balance and data-shape changes. const COMPACT_SNAPSHOT_VERSION = 9; const DEFAULT_SKIP = new Set(["world", "target", "panicTarget", "targetRef", "targetEntity", "source"]); const COORDINATE_KEYS = new Set(["x", "y", "prevX", "prevY", "foundingX", "foundingY", "cameraX", "cameraY"]); function isPlainObject(value) { if (!value || typeof value !== "object") return false; const proto = Object.getPrototypeOf(value); return proto === Object.prototype || proto === null; } function clonePlain(value, depth = 0) { if (value == null) return value; const type = typeof value; if (type === "number") return Number.isFinite(value) ? value : 0; if (type === "string" || type === "boolean") return value; if (type === "function" || type === "symbol" || type === "undefined") return undefined; if (depth > 8) return undefined; if (Array.isArray(value)) { const out = []; for (const v of value) { const c = clonePlain(v, depth + 1); if (c !== undefined) out.push(c); } return out; } if (!isPlainObject(value)) return undefined; const out = {}; for (const [key, v] of Object.entries(value)) { if (DEFAULT_SKIP.has(key)) continue; const c = clonePlain(v, depth + 1); if (c !== undefined) out[key] = c; } return out; } function copyOwnData(obj, skip = DEFAULT_SKIP) { const out = {}; if (!obj) return out; for (const key of Object.keys(obj)) { if (skip?.has?.(key)) continue; const cloned = clonePlain(obj[key]); if (cloned !== undefined) out[key] = cloned; } return out; } function applyOwnData(target, data, skip = DEFAULT_SKIP) { if (!target || !data) return target; for (const [key, value] of Object.entries(data)) { if (skip?.has?.(key) || key.startsWith("__")) continue; const c = clonePlain(value); if (c !== undefined) target[key] = c; } return target; } function refFor(worldRef, value) { if (!value || typeof value !== "object") return null; if ((worldRef?.tarinai || []).includes(value)) return { kind: "tarinai", id: value.id || "", token: value.liveToken || 0, familyKey: value.familyKey || "" }; if ((worldRef?.items || []).includes(value)) return { kind: "item", id: value.id || "", type: value.type || "" }; if ((worldRef?.ants || []).includes(value)) return { kind: "ant", id: value.id || "" }; if (Number.isFinite(value.x) && Number.isFinite(value.y)) return { kind: "point", x: value.x, y: value.y, dead: Boolean(value.dead) }; return null; } function resolveRef(worldRef, ref) { if (!ref || typeof ref !== "object") return null; if (ref.kind === "tarinai") return (worldRef.tarinai || []).find(t => t && !t.dead && ((ref.id && t.id === ref.id) || (ref.familyKey && t.familyKey === ref.familyKey))) || null; if (ref.kind === "item") return (worldRef.items || []).find(it => it && !it.dead && it.id === ref.id) || null; if (ref.kind === "ant") return (worldRef.ants || []).find(a => a && !a.dead && a.id === ref.id) || null; if (ref.kind === "point") return { x: Number(ref.x) || 0, y: Number(ref.y) || 0, dead: Boolean(ref.dead) }; return null; } function worldData(worldRef) { const keys = [ "w", "h", "viewportW", "viewportH", "time", "day", "paused", "speed", "tool", "toolSize", "toolSizes", "frozenTarinai", "fieldType", "fieldZoom", "cameraX", "cameraY", "weather", "nextWeatherChange", "deadCount", "liveIdNext", "liveIdSerial", "lastBirthAt", "maxGeneration", "family", "familyVersion", "familyTreeDirtyReason", "relationNotices", "resolvedFightIds", "eventCounters", "foodSpoilagePenalty", "foodSpoilageEvents", "lastPhase", "colonyMood", "lastColonyMoodDay", "antUpdatePhase", "shakeTimer", "shakeDuration", "shakeStrength" ]; const out = {}; for (const key of keys) { const cloned = clonePlain(worldRef?.[key]); if (cloned !== undefined) out[key] = cloned; } return out; } function snapshotEntity(worldRef, entity, skip = new Set(["world", "target", "panicTarget", "targetRef"])) { const data = copyOwnData(entity, skip); const targetRef = refFor(worldRef, entity?.target); const panicTargetRef = refFor(worldRef, entity?.panicTarget); const targetRefObject = refFor(worldRef, entity?.targetRef); if (targetRef) data.__targetRef = targetRef; if (panicTargetRef) data.__panicTargetRef = panicTargetRef; if (targetRefObject) data.__targetRefObject = targetRefObject; return data; } const COMPACT_WORLD_KEYS = ["w", "h", "time", "day", "speed", "fieldType", "fieldZoom", "cameraX", "cameraY", "weather", "nextWeatherChange", "deadCount", "liveIdNext", "liveIdSerial", "lastBirthAt", "maxGeneration", "family", "colonyMood", "lastColonyMoodDay"]; const COMPACT_TARINAI_KEYS = ["familyKey", "id", "liveToken", "name", "type", "x", "y", "vx", "vy", "impulseVx", "impulseVy", "adultScale", "scale", "age", "lifeSpan", "generation", "birthTime", "hunger", "loneliness", "energy", "stress", "affection", "need", "state", "dead", "deathReason", "birthPersonality", "currentPersonality", "personalityDaily", "genetics", "parents", "parentNames", "children", "relationships", "favorite", "hasPaired", "reproductionTimer", "loveMochiTimer", "fightMochiTimer", "zunchiDisease", "zunchiDiseaseSeverity", "sleepDisease", "explosionDisease", "explosionDiseaseTimer", "fightDisease", "totalFightLosses", "totalFightWins", "isZunchiSlave", "zunchiSlaveLocked", "formerName", "zunchiSlaveSpriteVariant", "zunchiStain", "zunchiStainSeed", "digest", "poopCount", "oshiriByoZunchiStock", "stuckPushpinId", "itemEffectTimers", "laxativePoopTimer", "powerItemMode", "sizeItemMode", "lifeItemMode", "mercuryLifeSpanBase", "mercuryLifeMultiplier", "mysteryDrugHistory", "insideNestBoxId", "sunbathTimer", "sunbathCooldown", "sunbathSpriteVariant", "goodMode", "facing", "visualFacing", "fallTimer", "fallDir", "hurtTimer", "fearTimer", "stressBarTimer", "hpBarTimer"]; const COMPACT_ITEM_KEYS = ["id", "type", "x", "y", "r", "amount", "age", "seed", "dead", "toolSize", "foodServingScale", "foodServingsMax", "foodServingsRemaining", "passiveFoodDecayTimer", "growth", "health", "seedTimer", "eatenAmount", "fertilityBoost", "lifeSpan", "wither", "comfort", "wear", "text", "textEditedAt", "vx", "vy", "prevX", "prevY", "spin", "spinVelocity", "stage", "stageTimer", "fertility", "freshness", "zunchiVariant", "antCount", "antSpawnTimer", "antSpawnCooldown", "antWorkers", "queenSpawnAt", "workerSprite", "decayTimer", "fuseTimer", "fuseMax", "dropTimer", "dropMax", "dropImpactDone", "impactFlash", "lingerTimer", "lingerDuration", "pinState", "pinTargetId", "pinAttachAngle", "pinAttachDistance", "pinOffsetY", "pinDamageTick", "pinFallCheckTimer", "pinLogAt", "pinFlipX", "deletable", "machineSlotType", "machineSlotSize", "machineSpawnTimer", "machineLastSlotType", "machineProduced", "machineProducerId", "sourceMachineType", "infiniteServing", "noDecay", "cannonAngle", "cannonFireTimer", "cannonProjectile", "projectileSourceId", "projectileTimer"]; const COMPACT_ANT_KEYS = ["id", "kind", "homeId", "targetId", "targetToken", "x", "y", "vx", "vy", "r", "maxHp", "hp", "state", "age", "seed", "wanderAngle", "nextTurn", "returnTimer", "foundingX", "foundingY", "foundingDelayUntil", "carryLogAt", "foundingAttempts", "dead"]; // v4 hash format is intentionally alpha-only and compatibility may break. // It stores gameplay state in short sparse arrays: non-gameplay UI/log/effect data is omitted. const V4_WORLD_KEYS = COMPACT_WORLD_KEYS; const V4_TARINAI_KEYS = COMPACT_TARINAI_KEYS; const V4_ANT_KEYS = COMPACT_ANT_KEYS; const V4_ITEM_TYPES = ["zunchi","sweet","love_mochi","fight_mochi","water_bowl","sleep_drug","laxative","protein","niteropu","ammo","mystery_drug","mercury","giant_drug","dwarf_drug","zunda_juice","grass","stone","genkotsu","bed","nest_box","ant_nest","ball","signboard","firecracker","pushpin","oshibyo","duplicator","cannon","fence_v","fence_h","water","trace","splat","food","ant_corpse"]; const V4_SPRITE_TYPES = ["smile","angry","teary","jito","drool","cry","sleep","pokan","stretch","hurt","weak","back","flee","zunda_eat","hurt2","normal_smirk","normal_tongue","fear","flee_fear2","sleep2","normal_happy","fear_blue","fear_cry","stress_dizzy","stress_sweat","intimidate","birth_ritual","zunchi_slave","hungry_70","zunchi_slave_alt","sunbath_1","sunbath_2","sunbath_3"]; const V4_ITEM_EXTRA_KEYS = ["id", "r", "amount", "age", "dead", "toolSize", "foodServingScale", "foodServingsMax", "foodServingsRemaining", "growth", "health", "seedTimer", "eatenAmount", "fertilityBoost", "lifeSpan", "wither", "comfort", "wear", "text", "vx", "vy", "prevX", "prevY", "spin", "spinVelocity", "stage", "stageTimer", "fertility", "freshness", "zunchiVariant", "antCount", "antSpawnTimer", "antSpawnCooldown", "antWorkers", "queenSpawnAt", "workerSprite", "fuseTimer", "fuseMax", "dropTimer", "dropMax", "dropImpactDone", "impactFlash", "lingerTimer", "lingerDuration", "pinState", "pinTargetId", "pinAttachAngle", "pinAttachDistance", "pinOffsetY", "pinDamageTick", "pinFallCheckTimer", "pinLogAt", "pinFlipX", "deletable", "machineSlotType", "machineSlotSize", "machineSpawnTimer", "machineLastSlotType", "machineProduced", "machineProducerId", "sourceMachineType", "infiniteServing", "noDecay", "cannonAngle", "cannonFireTimer", "cannonProjectile", "projectileSourceId", "projectileTimer"]; function itemTypeList() { return V4_ITEM_TYPES; } function spriteTypeList() { return V4_SPRITE_TYPES; } function codeFromList(value, list) { const idx = list.indexOf(value); return idx >= 0 ? idx : value; } function valueFromList(value, list) { return typeof value === "number" ? (list[value] || "") : value; } function isEmptyCompactValue(value) { return value === undefined || value === null || value === "" || (Array.isArray(value) && value.length === 0) || (isPlainObject(value) && Object.keys(value).length === 0); } function sameCompactValue(a, b, key = "") { const ca = compactValue(a, 0, key); const cb = compactValue(b, 0, key); return JSON.stringify(ca) === JSON.stringify(cb); } function packSparseByKeys(data, keys, defaults = null, options = {}) { const out = []; for (let i = 0; i < keys.length; i++) { const key = keys[i]; if (options.skip?.has?.(key)) continue; let value = data?.[key]; if (key === "relationships") value = compactRelationships(value); const c = compactValue(value, 0, key); if (isEmptyCompactValue(c)) continue; if (defaults && sameCompactValue(c, defaults[key], key)) continue; out.push(i, c); } return out; } function unpackSparseByKeys(pairs, keys) { const out = {}; if (!Array.isArray(pairs)) return out; for (let i = 0; i < pairs.length - 1; i += 2) { const key = keys[pairs[i]]; if (!key) continue; out[key] = clonePlain(pairs[i + 1]); } return out; } function itemDefaultsFor(type = "") { const def = typeof itemDefinition === "function" ? itemDefinition(type) : null; const out = { r: typeof itemRadiusFor === "function" ? itemRadiusFor(type, 12) : (def?.radius ?? 12), amount: typeof itemAmountFor === "function" ? itemAmountFor(type, 80) : (def?.amount ?? 80), age: 0, dead: false, dropTimer: 0, dropMax: 0, dropImpactDone: false, }; if (typeof isServingFoodType === "function" && isServingFoodType(type)) { out.toolSize = "medium"; out.foodServingScale = 1; out.foodServingsMax = typeof foodServingsForSize === "function" ? foodServingsForSize("medium") : 5; out.foodServingsRemaining = out.foodServingsMax; out.amount = out.foodServingsMax; } if (type === "grass") { out.health = 1; out.eatenAmount = 0; out.fertilityBoost = 0; out.wither = 0; } if (type === "bed") out.wear = 0; if (type === "signboard") out.text = ""; if (type === "duplicator" || type === "cannon") Object.assign(out, { machineSlotType: "", machineSlotSize: "medium", machineSpawnTimer: 0, machineLastSlotType: "", cannonAngle: type === "cannon" ? -Math.PI * 0.25 : 0, cannonFireTimer: 0 }); if (type === "zunchi") { out.stage = "fresh"; out.stageTimer = 0; out.fertility = 0; out.freshness = 1; } if (type === "firecracker") { out.fuseTimer = 5; out.fuseMax = 5; } if (typeof isPinType === "function" && isPinType(type)) { Object.assign(out, { amount: 100, pinState: "loose", pinTargetId: "", pinAttachAngle: 0, pinAttachDistance: 0, pinOffsetY: 0, pinDamageTick: 0, pinFallCheckTimer: 0, pinLogAt: -999, deletable: true }); } return out; } function packV4Item(worldRef, item, typeList) { const data = snapshotEntity(worldRef, item); const type = data.type || item?.type || ""; const defaults = itemDefaultsFor(type); const extras = packSparseByKeys(data, V4_ITEM_EXTRA_KEYS, defaults, { skip: new Set(["type", "x", "y"]) }); const row = [codeFromList(type, typeList), compactCoordinate(data.x || 0), compactCoordinate(data.y || 0)]; if (extras.length) row.push(extras); return row; } function unpackV4Item(row, typeList) { if (!Array.isArray(row)) return null; const type = valueFromList(row[0], typeList); const data = unpackSparseByKeys(row[3] || [], V4_ITEM_EXTRA_KEYS); data.type = type; data.x = row[1] || 0; data.y = row[2] || 0; return data; } function packV4Tarinai(worldRef, tarinai, spriteList) { const data = snapshotEntity(worldRef, tarinai); const pairs = packSparseByKeys(data, V4_TARINAI_KEYS, null); for (let i = 0; i < pairs.length - 1; i += 2) { if (V4_TARINAI_KEYS[pairs[i]] === "type") pairs[i + 1] = codeFromList(pairs[i + 1], spriteList); } return pairs; } function unpackV4Tarinai(row, spriteList) { const data = unpackSparseByKeys(row, V4_TARINAI_KEYS); if (data.type !== undefined) data.type = valueFromList(data.type, spriteList); return data; } function createV4Snapshot(worldRef = global.world) { if (!worldRef) throw new Error("world is not ready"); const typeList = itemTypeList(); const spriteList = spriteTypeList(); const world = worldData(worldRef); const frozen = Array.isArray(world.frozenTarinai) ? world.frozenTarinai : []; delete world.frozenTarinai; return { v: COMPACT_SNAPSHOT_VERSION, w: packSparseByKeys(world, V4_WORLD_KEYS, { w: 1000, h: 720, time: 0, day: 1, speed: 1, fieldType: "garden", fieldZoom: 1, cameraX: 0, cameraY: 0, weather: "sunny", deadCount: 0, liveIdNext: 1, liveIdSerial: 0, lastBirthAt: -999, maxGeneration: 1, family: {}, colonyMood: { id: "relaxed", label: "relaxed", description: "", effects: { personality: {} } }, lastColonyMoodDay: 0 }), t: (worldRef.tarinai || []).filter(Boolean).map(t => packV4Tarinai(worldRef, t, spriteList)), i: (worldRef.items || []).filter(it => it && !it.dead && !["trace", "splat"].includes(it.type)).map(it => packV4Item(worldRef, it, typeList)), a: (worldRef.ants || []).filter(a => a && !a.dead).map(a => packSparseByKeys(snapshotEntity(worldRef, a), V4_ANT_KEYS)), f: frozen.map(compactFrozenEntry), }; } function expandV4Snapshot(snapshot) { if (!snapshot || snapshot.v !== 4) return snapshot; const typeList = itemTypeList(); const spriteList = spriteTypeList(); const world = unpackSparseByKeys(snapshot.w, V4_WORLD_KEYS); world.frozenTarinai = (snapshot.f || []).map(expandFrozenEntry); return { version: SNAPSHOT_VERSION, app: "tarinai_colony_game", createdAt: Date.now(), summary: { day: world.day || 1, time: "", fieldType: world.fieldType || "garden", population: Array.isArray(snapshot.t) ? snapshot.t.length : 0, items: Array.isArray(snapshot.i) ? snapshot.i.length : 0 }, world, tarinai: (snapshot.t || []).map(row => unpackV4Tarinai(row, spriteList)), items: (snapshot.i || []).map(row => unpackV4Item(row, typeList)).filter(Boolean), ants: (snapshot.a || []).map(row => unpackSparseByKeys(row, V4_ANT_KEYS)), effects: [], logs: [], selectedRef: null, }; } // v5 hash format: shorter alpha-only hash text. It keeps gameplay state, but intentionally excludes logs, bubbles, UI-only timers, thumbnails, and other presentation state. const V5_SNAPSHOT_VERSION = 5; const V5_WORLD_KEYS = ["w", "h", "time", "day", "speed", "fieldType", "fieldZoom", "cameraX", "cameraY", "weather", "nextWeatherChange", "deadCount", "liveIdNext", "liveIdSerial", "lastBirthAt", "maxGeneration", "colonyMood", "lastColonyMoodDay"]; const V5_TARINAI_KEYS = ["familyKey", "id", "liveToken", "name", "type", "x", "y", "vx", "vy", "impulseVx", "impulseVy", "adultScale", "scale", "age", "lifeSpan", "generation", "birthTime", "hunger", "loneliness", "energy", "stress", "affection", "need", "state", "dead", "deathReason", "birthPersonality", "currentPersonality", "personalityDaily", "genetics", "parents", "children", "relationships", "favorite", "hasPaired", "reproductionTimer", "loveMochiTimer", "fightMochiTimer", "zunchiDisease", "zunchiDiseaseSeverity", "sleepDisease", "explosionDisease", "explosionDiseaseTimer", "fightDisease", "totalFightLosses", "totalFightWins", "isZunchiSlave", "zunchiSlaveLocked", "formerName", "zunchiSlaveSpriteVariant", "zunchiStain", "zunchiStainSeed", "digest", "poopCount", "oshiriByoZunchiStock", "stuckPushpinId", "itemEffectTimers", "laxativePoopTimer", "powerItemMode", "sizeItemMode", "lifeItemMode", "mercuryLifeSpanBase", "mercuryLifeMultiplier", "mysteryDrugHistory", "insideNestBoxId", "sunbathTimer", "sunbathCooldown", "sunbathSpriteVariant", "goodMode", "facing", "visualFacing", "fallTimer", "fallDir", "hurtTimer", "fearTimer"]; const V5_ITEM_EXTRA_KEYS = ["id", "r", "amount", "age", "dead", "toolSize", "foodServingScale", "foodServingsMax", "foodServingsRemaining", "growth", "health", "seedTimer", "eatenAmount", "fertilityBoost", "lifeSpan", "wither", "comfort", "wear", "text", "vx", "vy", "prevX", "prevY", "spin", "spinVelocity", "stage", "stageTimer", "fertility", "freshness", "zunchiVariant", "antCount", "antSpawnTimer", "antSpawnCooldown", "antWorkers", "queenSpawnAt", "workerSprite", "fuseTimer", "fuseMax", "dropTimer", "dropMax", "dropImpactDone", "impactFlash", "lingerTimer", "lingerDuration", "pinState", "pinTargetId", "pinAttachAngle", "pinAttachDistance", "pinOffsetY", "pinDamageTick", "pinFallCheckTimer", "pinLogAt", "pinFlipX", "deletable", "machineSlotType", "machineSlotSize", "machineSpawnTimer", "machineLastSlotType", "machineProduced", "machineProducerId", "sourceMachineType", "infiniteServing", "noDecay", "cannonAngle", "cannonFireTimer", "cannonProjectile", "projectileSourceId", "projectileTimer"]; const V5_ANT_KEYS = ["id", "kind", "homeId", "targetId", "targetToken", "x", "y", "vx", "vy", "r", "maxHp", "hp", "state", "age", "seed", "wanderAngle", "nextTurn", "returnTimer", "foundingX", "foundingY", "foundingDelayUntil", "foundingAttempts", "dead"]; const V5_NEEDS = ["\u98df\u3079\u7269", "\u4ef2\u9593", "\u7720\u308a", "\u52c7\u6c17", "\u6c34", "\u9759\u3051\u3055", "\u3042\u305f\u305f\u304b\u3055", "\u610f\u5473", "\u7a7a\u9593", "\u81ea\u4fe1"]; const V5_STATES = ["idle", "seek_food", "seek_friend", "seek_bed", "seek_water", "sleep", "eat", "panic", "fight", "birth_ritual", "intimidate", "ant_attack", "ant_intimidate", "zunchi_sick", "fight_sick", "play_ball", "follow_parent", "cursor_enemy", "cursor_friend", "sunbath"]; const V5_SIZES = ["small", "medium", "large"]; const V5_PIN_STATES = ["loose", "lodged"]; const V5_ZUNCHI_STAGES = ["fresh", "aged", "dry", "dead"]; const V5_MOODS = ["relaxed", "calm", "prickly", "stinky", "weaklings", "zunda_party", "ball_party", "sun_party", "ant_overrun"]; const PERSONALITY_ORDER = ["aggression", "openness", "sociability", "neuroticism"]; const GENETIC_ORDER = ["lifeSpanMul", "attackMul", "speedMul", "sizeMul"]; function codeOrValue(value, list) { if (value === undefined || value === null || value === "") return value; const idx = list.indexOf(value); return idx >= 0 ? idx : value; } function valueOrCode(value, list) { return typeof value === "number" ? (list[value] || "") : value; } function compactMetric(value) { if (!Number.isFinite(value)) return 0; const rounded = Math.round(value * 10) / 10; return Object.is(rounded, -0) ? 0 : rounded; } function approxSame(a, b, key = "") { if (COORDINATE_KEYS.has(key)) return compactCoordinate(a) === compactCoordinate(b); if (typeof a === "number" || typeof b === "number") return compactNumber(Number(a) || 0) === compactNumber(Number(b) || 0); return JSON.stringify(compactValue(a, 0, key)) === JSON.stringify(compactValue(b, 0, key)); } function packAxisObject(obj, keys, fallback = null) { if (!obj || typeof obj !== "object") return undefined; const arr = keys.map(key => compactNumber(obj[key] ?? fallback?.[key] ?? 0)); while (arr.length && (arr[arr.length - 1] === 0 || arr[arr.length - 1] === null || arr[arr.length - 1] === undefined)) arr.pop(); return arr.length ? arr : undefined; } function unpackAxisObject(arr, keys, fallback = null) { const out = fallback && typeof fallback === "object" ? clonePlain(fallback) || {} : {}; if (!Array.isArray(arr)) return out; for (let i = 0; i < Math.min(arr.length, keys.length); i++) out[keys[i]] = compactNumber(Number(arr[i]) || 0); for (const key of keys) if (!Number.isFinite(out[key])) out[key] = 0; return out; } function compactItemEffectTimers(value) { if (!value || typeof value !== "object") return undefined; const arr = [compactNumber(value.laxative || 0), compactNumber(value.ammo || 0)]; while (arr.length && !arr[arr.length - 1]) arr.pop(); return arr.length ? arr : undefined; } function expandItemEffectTimers(value) { if (!Array.isArray(value)) return value; return { laxative: Number(value[0] || 0), ammo: Number(value[1] || 0) }; } function compactPersonalityDaily(value) { if (!value || typeof value !== "object") return undefined; const out = []; if (Number.isFinite(value.day) && value.day !== 1) out.push(0, value.day); if (Number.isFinite(value.total) && value.total) out.push(1, compactNumber(value.total)); if (value.byKey && typeof value.byKey === "object" && Object.keys(value.byKey).length) out.push(2, compactValue(value.byKey)); return out.length ? out : undefined; } function expandPersonalityDaily(value) { if (!Array.isArray(value)) return value; const out = { day: 1, total: 0, byKey: {}, byCause: {} }; for (let i = 0; i < value.length - 1; i += 2) { if (value[i] === 0) out.day = value[i + 1]; else if (value[i] === 1) out.total = value[i + 1]; else if (value[i] === 2) out.byKey = clonePlain(value[i + 1]) || {}; } return out; } function compactRelationshipsV5(rels) { if (!rels || typeof rels !== "object") return undefined; const rows = []; for (const [id, r] of Object.entries(rels)) { if (!id || !r) continue; const row = [id, compactMetric(r.affinity || 0), compactMetric(r.fear || 0), compactNumber(r.fightsWon || 0), compactNumber(r.fightsLost || 0)]; while (row.length > 1 && (row[row.length - 1] === 0 || row[row.length - 1] === "" || row[row.length - 1] === null || row[row.length - 1] === undefined)) row.pop(); if (row.length > 1) rows.push(row); } rows.sort((a, b) => { const score = row => Math.abs(row[1] || 0) + Math.abs(row[2] || 0) + Math.abs(row[3] || 0) * 2 + Math.abs(row[4] || 0) * 2; return score(b) - score(a); }); return rows.length ? rows.slice(0, 20) : undefined; } function expandRelationshipsV5(rows) { if (!Array.isArray(rows)) return rows; const out = {}; for (const row of rows) { if (!Array.isArray(row) || !row[0]) continue; out[row[0]] = { affinity: Number(row[1] || 0), fear: Number(row[2] || 0), fightsWon: Number(row[3] || 0), fightsLost: Number(row[4] || 0), lastEvent: "", lastTime: 0 }; } return out; } function v5DefaultTarinaiData(data = {}, worldRef = null) { const key = data.familyKey || data.id || ""; const genetics = typeof normalizeGenetics === "function" ? normalizeGenetics(null, key) : null; const birthPersonality = typeof randomBirthPersonality === "function" ? randomBirthPersonality(key) : null; const adultScale = typeof adultScaleFromGenetics === "function" ? adultScaleFromGenetics(key, data.genetics || genetics) : undefined; const out = { id: "", liveToken: 0, generation: 1, birthTime: 0, impulseVx: 0, impulseVy: 0, state: "idle", dead: false, deathReason: "", favorite: false, hasPaired: false, parents: [], children: [], relationships: {}, loveMochiTimer: 0, fightMochiTimer: 0, zunchiDisease: false, zunchiDiseaseSeverity: 0, sleepDisease: false, explosionDisease: false, explosionDiseaseTimer: 0, fightDisease: false, totalFightLosses: 0, totalFightWins: 0, isZunchiSlave: false, zunchiSlaveLocked: false, formerName: null, zunchiSlaveSpriteVariant: "", zunchiStain: 0, digest: 0, poopCount: 0, oshiriByoZunchiStock: 0, stuckPushpinId: null, itemEffectTimers: { laxative: 0, ammo: 0 }, laxativePoopTimer: 3, powerItemMode: "", sizeItemMode: "", lifeItemMode: "", mercuryLifeSpanBase: null, mercuryLifeMultiplier: 1, mysteryDrugHistory: [], insideNestBoxId: null, sunbathTimer: 0, sunbathCooldown: 0, sunbathSpriteVariant: 1, fallTimer: 0, hurtTimer: 0, fearTimer: 0, }; if (genetics) out.genetics = genetics; if (birthPersonality) { out.birthPersonality = birthPersonality; out.currentPersonality = birthPersonality; } if (adultScale !== undefined) { out.adultScale = adultScale; if ((data.generation || 1) <= 1) out.scale = adultScale; } if (typeof stableChoice === "function") out.goodMode = stableChoice(key, "good-mode", typeof displayNormalSprites === "function" ? displayNormalSprites() : []) || "smile"; if (typeof stableUnit === "function") { out.facing = stableUnit(key, "facing") < 0.5 ? -1 : 1; out.visualFacing = out.facing; out.fallDir = stableUnit(key, "fall-dir") < 0.5 ? -1 : 1; out.zunchiStainSeed = stableUnit(key, "zunchi-stain"); } return out; } function encodeV5TarinaiValue(key, value) { if (key === "type") return codeOrValue(value, V4_SPRITE_TYPES); if (key === "need") return codeOrValue(value, V5_NEEDS); if (key === "state") return codeOrValue(value, V5_STATES); if (key === "birthPersonality" || key === "currentPersonality") return packAxisObject(value, PERSONALITY_ORDER); if (key === "genetics") return packAxisObject(value, GENETIC_ORDER, { lifeSpanMul: 1, attackMul: 1, speedMul: 1, sizeMul: 1 }); if (key === "relationships") return compactRelationshipsV5(value); if (key === "itemEffectTimers") return compactItemEffectTimers(value); if (key === "personalityDaily") return compactPersonalityDaily(value); return compactValue(value, 0, key); } function decodeV5TarinaiValue(key, value) { if (key === "type") return valueOrCode(value, V4_SPRITE_TYPES); if (key === "need") return valueOrCode(value, V5_NEEDS); if (key === "state") return valueOrCode(value, V5_STATES); if (key === "birthPersonality" || key === "currentPersonality") return unpackAxisObject(value, PERSONALITY_ORDER); if (key === "genetics") return unpackAxisObject(value, GENETIC_ORDER, { lifeSpanMul: 1, attackMul: 1, speedMul: 1, sizeMul: 1 }); if (key === "relationships") return expandRelationshipsV5(value); if (key === "itemEffectTimers") return expandItemEffectTimers(value); if (key === "personalityDaily") return expandPersonalityDaily(value); return clonePlain(value); } function encodeV5ItemValue(key, value) { if (key === "toolSize" || key === "machineSlotSize") return codeOrValue(value, V5_SIZES); if (key === "pinState") return codeOrValue(value, V5_PIN_STATES); if (key === "stage") return codeOrValue(value, V5_ZUNCHI_STAGES); if (key === "machineSlotType" || key === "machineLastSlotType" || key === "sourceMachineType") return codeOrValue(value, V4_ITEM_TYPES); return compactValue(value, 0, key); } function decodeV5ItemValue(key, value) { if (key === "toolSize" || key === "machineSlotSize") return valueOrCode(value, V5_SIZES); if (key === "pinState") return valueOrCode(value, V5_PIN_STATES); if (key === "stage") return valueOrCode(value, V5_ZUNCHI_STAGES); if (key === "machineSlotType" || key === "machineLastSlotType" || key === "sourceMachineType") return valueOrCode(value, V4_ITEM_TYPES); return clonePlain(value); } function packSparseV5(data, keys, defaults = null, encode = (key, value) => compactValue(value, 0, key)) { const out = []; for (let i = 0; i < keys.length; i++) { const key = keys[i]; const encoded = encode(key, data?.[key]); if (isEmptyCompactValue(encoded)) continue; if (defaults && approxSame(encoded, encode(key, defaults[key]), key)) continue; out.push(i, encoded); } return out; } function unpackSparseV5(pairs, keys, decode = (key, value) => clonePlain(value)) { const out = {}; if (!Array.isArray(pairs)) return out; for (let i = 0; i < pairs.length - 1; i += 2) { const key = keys[pairs[i]]; if (!key) continue; out[key] = decode(key, pairs[i + 1]); } return out; } function v5ReferencedItemIds(worldRef, tarinaiRows = []) { const ids = new Set(); for (const t of tarinaiRows || []) { if (t?.stuckPushpinId) ids.add(t.stuckPushpinId); if (t?.insideNestBoxId) ids.add(t.insideNestBoxId); } for (const a of worldRef?.ants || []) { if (a?.homeId) ids.add(a.homeId); if (a?.targetId && String(a.targetId).startsWith("item")) ids.add(a.targetId); } for (const it of worldRef?.items || []) { if (it?.projectileSourceId) ids.add(it.projectileSourceId); } return ids; } function v5ItemDefaultsFor(type = "", keepId = false) { const out = itemDefaultsFor(type); if (!keepId) out.id = ""; if (type !== "signboard") out.text = ""; return out; } function packV5World(worldRef) { const world = worldData(worldRef); const mood = worldRef?.colonyMood?.id || world.colonyMood?.id || "relaxed"; world.colonyMood = codeOrValue(mood, V5_MOODS); delete world.frozenTarinai; delete world.family; const defaults = { w: 1000, h: 720, time: 0, day: 1, speed: 1, fieldType: "garden", fieldZoom: 1, cameraX: 0, cameraY: 0, weather: "sunny", deadCount: 0, liveIdNext: 1, liveIdSerial: 0, lastBirthAt: -999, maxGeneration: 1, colonyMood: 0, lastColonyMoodDay: 0 }; return packSparseV5(world, V5_WORLD_KEYS, defaults, (key, value) => key === "colonyMood" ? value : compactValue(value, 0, key)); } function unpackV5World(row) { const world = unpackSparseV5(row, V5_WORLD_KEYS, (key, value) => key === "colonyMood" ? { id: valueOrCode(value, V5_MOODS) || "relaxed" } : clonePlain(value)); return world; } function packV5Tarinai(worldRef, tarinai) { const data = snapshotEntity(worldRef, tarinai); const defaults = v5DefaultTarinaiData(data, worldRef); return packSparseV5(data, V5_TARINAI_KEYS, defaults, encodeV5TarinaiValue); } function unpackV5Tarinai(row) { return unpackSparseV5(row, V5_TARINAI_KEYS, decodeV5TarinaiValue); } function packV5Item(worldRef, item, referencedIds) { const data = snapshotEntity(worldRef, item); const type = data.type || item?.type || ""; const keepId = Boolean(data.id && (referencedIds?.has?.(data.id) || type === "ant_nest" || type === "nest_box" || data.pinState === "lodged")); const defaults = v5ItemDefaultsFor(type, keepId); const extras = packSparseV5(data, V5_ITEM_EXTRA_KEYS, defaults, encodeV5ItemValue); const row = [codeFromList(type, V4_ITEM_TYPES), compactCoordinate(data.x || 0), compactCoordinate(data.y || 0)]; if (extras.length) row.push(extras); return row; } function unpackV5Item(row) { if (!Array.isArray(row)) return null; const type = valueFromList(row[0], V4_ITEM_TYPES); const data = unpackSparseV5(row[3] || [], V5_ITEM_EXTRA_KEYS, decodeV5ItemValue); data.type = type; data.x = Number(row[1] || 0); data.y = Number(row[2] || 0); return data; } function packV5Ant(worldRef, ant) { const data = snapshotEntity(worldRef, ant); const defaults = { r: 6, maxHp: 8, hp: data.maxHp || 8, state: "wander", age: 0, dead: false }; return packSparseV5(data, V5_ANT_KEYS, defaults, (key, value) => compactValue(value, 0, key)); } function unpackV5Ant(row) { return unpackSparseV5(row, V5_ANT_KEYS, (key, value) => clonePlain(value)); } function compactFrozenEntryV5(entry = {}) { const data = entry.data || entry; const row = packSparseV5(data, V5_TARINAI_KEYS, v5DefaultTarinaiData(data), encodeV5TarinaiValue); const out = [entry.freezeId || entry.id || "", compactNumber(entry.frozenAt || 0), compactNumber(entry.worldTime || 0), row]; const losses = compactValue((entry.pendingLosses || []).slice(-8)) || []; if (losses.length) out.push(losses); return out; } function expandFrozenEntryV5(entry = []) { if (!Array.isArray(entry)) return expandFrozenEntry(entry); const data = unpackV5Tarinai(entry[3] || []); return { freezeId: entry[0] || "", frozenAt: entry[1] || 0, worldTime: entry[2] || 0, name: entry[3] || data.name || "tarinai", familyKey: data.familyKey || "", liveId: data.id || "", liveToken: data.liveToken || 0, type: data.type || "smile", data, pendingLosses: clonePlain(entry[4]) || [] }; } function createV5Snapshot(worldRef = global.world) { if (!worldRef) throw new Error("world is not ready"); const tarinaiRows = (worldRef.tarinai || []).filter(Boolean).map(t => snapshotEntity(worldRef, t)); const referencedIds = v5ReferencedItemIds(worldRef, tarinaiRows); const frozen = Array.isArray(worldRef.frozenTarinai) ? worldRef.frozenTarinai : []; return [ V5_SNAPSHOT_VERSION, packV5World(worldRef), (worldRef.tarinai || []).filter(Boolean).map(t => packV5Tarinai(worldRef, t)), (worldRef.items || []).filter(it => it && !it.dead && !["trace", "splat"].includes(it.type)).map(it => packV5Item(worldRef, it, referencedIds)), (worldRef.ants || []).filter(a => a && !a.dead).map(a => packV5Ant(worldRef, a)), frozen.map(compactFrozenEntryV5), ]; } function expandV5Snapshot(snapshot) { if (!Array.isArray(snapshot) || snapshot[0] !== V5_SNAPSHOT_VERSION) return snapshot; const world = unpackV5World(snapshot[1] || []); world.frozenTarinai = (snapshot[5] || []).map(expandFrozenEntryV5); return { version: SNAPSHOT_VERSION, app: "tarinai_colony_game", createdAt: Date.now(), summary: { day: world.day || 1, time: "", fieldType: world.fieldType || "garden", population: Array.isArray(snapshot[2]) ? snapshot[2].length : 0, items: Array.isArray(snapshot[3]) ? snapshot[3].length : 0 }, world, tarinai: (snapshot[2] || []).map(unpackV5Tarinai), items: (snapshot[3] || []).map(unpackV5Item).filter(Boolean), ants: (snapshot[4] || []).map(unpackV5Ant), effects: [], logs: [], selectedRef: null, }; } function compactNumber(value) { if (!Number.isFinite(value)) return 0; const rounded = Math.round(value * 1000) / 1000; return Object.is(rounded, -0) ? 0 : rounded; } function compactCoordinate(value) { if (!Number.isFinite(value)) return 0; const rounded = Math.round(value * 10) / 10; return Object.is(rounded, -0) ? 0 : rounded; } function compactValue(value, depth = 0, keyHint = "") { if (value == null) return value; const type = typeof value; if (type === "number") return COORDINATE_KEYS.has(keyHint) ? compactCoordinate(value) : compactNumber(value); if (type === "string" || type === "boolean") return value; if (type === "function" || type === "symbol" || type === "undefined") return undefined; if (depth > 6) return undefined; if (Array.isArray(value)) return value.map(v => compactValue(v, depth + 1, keyHint)).filter(v => v !== undefined); if (!isPlainObject(value)) return undefined; const out = {}; for (const [key, v] of Object.entries(value)) { if (DEFAULT_SKIP.has(key) || key.startsWith("__")) continue; const c = compactValue(v, depth + 1, key); if (c !== undefined && c !== null && !(Array.isArray(c) && !c.length)) out[key] = c; } return out; } function compactRelationships(rels) { if (!rels || typeof rels !== "object") return undefined; const entries = Object.entries(rels) .map(([id, r]) => [id, compactValue(r)]) .filter(([, r]) => r && (Math.abs(r.affinity || 0) > 0.08 || Math.abs(r.fear || 0) > 0.08 || Math.abs(r.familiarity || 0) > 0.2 || Math.abs(r.fights || 0) > 0)); entries.sort((a, b) => { const score = (r) => Math.abs(r.affinity || 0) + Math.abs(r.fear || 0) + Math.abs(r.familiarity || 0) * 0.25 + Math.abs(r.fights || 0) * 2; return score(b[1]) - score(a[1]); }); const out = {}; for (const [id, r] of entries.slice(0, 12)) out[id] = r; return Object.keys(out).length ? out : undefined; } function compactByKeys(data, keys) { const arr = []; for (let i = 0; i < keys.length; i++) { const key = keys[i]; let value = data?.[key]; if (key === "relationships") value = compactRelationships(value); arr[i] = compactValue(value, 0, key); } while (arr.length && (arr[arr.length - 1] === undefined || arr[arr.length - 1] === null || (Array.isArray(arr[arr.length - 1]) && !arr[arr.length - 1].length))) arr.pop(); return arr; } function expandByKeys(arr, keys) { const out = {}; if (!Array.isArray(arr)) return out; for (let i = 0; i < Math.min(arr.length, keys.length); i++) { const value = arr[i]; if (value === undefined || value === null) continue; out[keys[i]] = clonePlain(value); } return out; } function compactFrozenEntry(entry = {}) { const data = entry.data || entry; return { i: entry.freezeId || entry.id || "", a: compactNumber(entry.frozenAt || 0), t: compactNumber(entry.worldTime || 0), n: entry.name || data.name || "", k: entry.familyKey || data.familyKey || "", d: compactByKeys(data, COMPACT_TARINAI_KEYS), l: compactValue((entry.pendingLosses || []).slice(-8)) || [] }; } function expandFrozenEntry(entry = {}) { const data = expandByKeys(entry.d, COMPACT_TARINAI_KEYS); return { freezeId: entry.i || "", frozenAt: entry.a || 0, worldTime: entry.t || 0, name: entry.n || data.name || "tarinai", familyKey: entry.k || data.familyKey || "", liveId: data.id || "", liveToken: data.liveToken || 0, type: data.type || "smile", data, pendingLosses: clonePlain(entry.l) || [] }; } // v6 hash format: alpha-only, shorter than V5. Individual observation history and names are intentionally omitted. // Gameplay state is preserved through needs, personality, genetics, relationships, family links, item states, machines, ants, and frozen entries. const V6_SNAPSHOT_VERSION = 6; const V6_FIELD_TYPES = ["garden", "cage", "park"]; const V6_WEATHERS = ["sunny", "cloudy", "light_rain"]; const V6_CORE_TARINAI_KEYS = ["familyKey", "id", "liveToken", "x", "y", "adultScale", "scale", "age", "lifeSpan", "hunger", "loneliness", "energy", "stress", "affection", "need", "state", "reproductionTimer", "digest", "currentPersonality", "genetics"]; const V6_EXTRA_TARINAI_KEYS = ["type", "vx", "vy", "impulseVx", "impulseVy", "generation", "birthTime", "parents", "children", "relationships", "dead", "hasPaired", "loveMochiTimer", "fightMochiTimer", "zunchiDisease", "zunchiDiseaseSeverity", "sleepDisease", "explosionDisease", "explosionDiseaseTimer", "fightDisease", "totalFightLosses", "totalFightWins", "isZunchiSlave", "zunchiSlaveLocked", "poopCount", "oshiriByoZunchiStock", "stuckPushpinId", "itemEffectTimers", "laxativePoopTimer", "powerItemMode", "sizeItemMode", "lifeItemMode", "mercuryLifeSpanBase", "mercuryLifeMultiplier", "insideNestBoxId", "sunbathTimer", "sunbathCooldown", "fallTimer", "fearTimer"]; const V6_LOSS_RELATIONS = ["parent", "child", "mate"]; const V6_TARINAI_KEYS = [...V6_CORE_TARINAI_KEYS, ...V6_EXTRA_TARINAI_KEYS]; function v6IdToCode(value) { if (!value) return ""; const m = /^t(\d+)$/.exec(String(value)); return m ? Number(m[1]) : value; } function v6CodeToId(value) { if (value === undefined || value === null || value === "") return ""; return typeof value === "number" ? `t${value}` : String(value); } function v6FamilyMaps(worldRef, frozen = []) { const map = new Map(); const list = []; const add = (value) => { if (!value) return null; const key = String(value); if (!map.has(key)) { map.set(key, map.size); list.push(key); } return map.get(key); }; for (const t of worldRef?.tarinai || []) { if (!t) continue; add(t.familyKey || t.archiveKey || t.id); for (const id of t.parents || []) add(id); for (const id of t.children || []) add(id); } for (const entry of frozen || []) { const data = entry?.data || entry; if (!data) continue; add(entry.familyKey || data.familyKey || data.id); for (const id of data.parents || []) add(id); for (const id of data.children || []) add(id); for (const loss of entry.pendingLosses || []) add(loss?.familyKey); } return { map, list }; } function v6FamilyCode(value, ctx) { if (!value) return ""; if (!ctx?.familyMap) return value; const existing = ctx.familyMap.get(String(value)); if (existing !== undefined) return existing; const next = ctx.familyMap.size; ctx.familyMap.set(String(value), next); ctx.familyList?.push?.(String(value)); return next; } function v6FamilyValue(value, ctx) { if (value === undefined || value === null || value === "") return ""; // V6 intentionally restores compact family keys. Exact historic UUIDs are observation history, not gameplay. return typeof value === "number" ? `f${value + 1}` : String(value); } function v6RelationKey(value) { return v6IdToCode(value); } function v6RelationId(value) { return v6CodeToId(value); } function compactRelationshipsV6(rels) { if (!rels || typeof rels !== "object") return undefined; const rows = []; for (const [id, r] of Object.entries(rels)) { if (!id || !r) continue; const row = [v6RelationKey(id), compactMetric(r.affinity || 0), compactMetric(r.fear || 0), compactNumber(r.fightsWon || 0), compactNumber(r.fightsLost || 0)]; while (row.length > 1 && (row[row.length - 1] === 0 || row[row.length - 1] === "" || row[row.length - 1] === null || row[row.length - 1] === undefined)) row.pop(); if (row.length > 1) rows.push(row); } rows.sort((a, b) => { const score = row => Math.abs(row[1] || 0) + Math.abs(row[2] || 0) + Math.abs(row[3] || 0) * 2 + Math.abs(row[4] || 0) * 2; return score(b) - score(a); }); return rows.length ? rows.slice(0, 20) : undefined; } function expandRelationshipsV6(rows) { if (!Array.isArray(rows)) return rows; const out = {}; for (const row of rows) { if (!Array.isArray(row) || row[0] === undefined || row[0] === null || row[0] === "") continue; out[v6RelationId(row[0])] = { affinity: Number(row[1] || 0), fear: Number(row[2] || 0), fightsWon: Number(row[3] || 0), fightsLost: Number(row[4] || 0), lastEvent: "", lastTime: 0 }; } return out; } function encodeV6TarinaiValue(key, value, ctx) { if (key === "familyKey") return v6FamilyCode(value, ctx); if (key === "id") return v6IdToCode(value); if (key === "type") return codeOrValue(value, V4_SPRITE_TYPES); if (key === "need") return codeOrValue(value, V5_NEEDS); if (key === "state") return codeOrValue(value, V5_STATES); if (key === "currentPersonality") return packAxisObject(value, PERSONALITY_ORDER); if (key === "genetics") return packAxisObject(value, GENETIC_ORDER, { lifeSpanMul: 1, attackMul: 1, speedMul: 1, sizeMul: 1 }); if (key === "parents" || key === "children") return Array.isArray(value) && value.length ? value.map(v => v6FamilyCode(v, ctx)).filter(v => v !== "") : undefined; if (key === "relationships") return compactRelationshipsV6(value); if (key === "itemEffectTimers") return compactItemEffectTimers(value); return compactValue(value, 0, key); } function decodeV6TarinaiValue(key, value, ctx) { if (key === "familyKey") return v6FamilyValue(value, ctx); if (key === "id") return v6CodeToId(value); if (key === "type") return valueOrCode(value, V4_SPRITE_TYPES); if (key === "need") return valueOrCode(value, V5_NEEDS); if (key === "state") return valueOrCode(value, V5_STATES); if (key === "currentPersonality") return unpackAxisObject(value, PERSONALITY_ORDER); if (key === "genetics") return unpackAxisObject(value, GENETIC_ORDER, { lifeSpanMul: 1, attackMul: 1, speedMul: 1, sizeMul: 1 }); if (key === "parents" || key === "children") return Array.isArray(value) ? value.map(v => v6FamilyValue(v, ctx)).filter(Boolean) : []; if (key === "relationships") return expandRelationshipsV6(value); if (key === "itemEffectTimers") return expandItemEffectTimers(value); return clonePlain(value); } function v6DefaultTarinaiData(data = {}) { return { familyKey: "", id: "", liveToken: 0, type: "smile", x: 0, y: 0, vx: 0, vy: 0, impulseVx: 0, impulseVy: 0, adultScale: 0, scale: 0, age: 0, lifeSpan: 0, generation: 1, birthTime: 0, hunger: 0, loneliness: 0, energy: 0, stress: 0, affection: 0, need: "", state: "idle", currentPersonality: {}, genetics: {}, parents: [], children: [], relationships: {}, dead: false, deathReason: "", favorite: false, hasPaired: false, reproductionTimer: 0, loveMochiTimer: 0, fightMochiTimer: 0, zunchiDisease: false, zunchiDiseaseSeverity: 0, sleepDisease: false, explosionDisease: false, explosionDiseaseTimer: 0, fightDisease: false, totalFightLosses: 0, totalFightWins: 0, isZunchiSlave: false, zunchiSlaveLocked: false, zunchiSlaveSpriteVariant: "", zunchiStain: 0, digest: 0, poopCount: 0, oshiriByoZunchiStock: 0, stuckPushpinId: null, itemEffectTimers: { laxative: 0, ammo: 0 }, laxativePoopTimer: 3, powerItemMode: "", sizeItemMode: "", lifeItemMode: "", mercuryLifeSpanBase: null, mercuryLifeMultiplier: 1, insideNestBoxId: null, sunbathTimer: 0, sunbathCooldown: 0, sunbathSpriteVariant: 1, goodMode: "", facing: 0, visualFacing: 0, fallTimer: 0, fallDir: 0, hurtTimer: 0, fearTimer: 0, }; } function packV6Core(data, keys, defaults, encode, ctx) { const row = keys.map(key => { const encoded = encode(key, data?.[key], ctx); if (isEmptyCompactValue(encoded)) return null; const def = encode(key, defaults?.[key], ctx); if (approxSame(encoded, def, key)) return null; return encoded; }); while (row.length && (row[row.length - 1] === null || row[row.length - 1] === undefined || isEmptyCompactValue(row[row.length - 1]))) row.pop(); return row; } function unpackV6Core(row, keys, decode, ctx) { const out = {}; if (!Array.isArray(row)) return out; for (let i = 0; i < Math.min(row.length, keys.length); i++) { const value = row[i]; if (value === null || value === undefined) continue; const key = keys[i]; out[key] = decode(key, value, ctx); } return out; } function packV6CoreTarinai(data, ctx) { const row = V6_CORE_TARINAI_KEYS.map(key => { const encoded = encodeV6TarinaiValue(key, data?.[key], ctx); return isEmptyCompactValue(encoded) ? null : encoded; }); while (row.length && (row[row.length - 1] === null || row[row.length - 1] === undefined || isEmptyCompactValue(row[row.length - 1]))) row.pop(); return row; } function unpackV6CoreTarinai(row, ctx) { const out = {}; if (!Array.isArray(row)) return out; for (let i = 0; i < Math.min(row.length, V6_CORE_TARINAI_KEYS.length); i++) { const value = row[i]; if (value === null || value === undefined) continue; const key = V6_CORE_TARINAI_KEYS[i]; out[key] = decodeV6TarinaiValue(key, value, ctx); } return out; } function packV6ExtrasTarinai(data, defaults, ctx) { const out = []; for (let i = 0; i < V6_EXTRA_TARINAI_KEYS.length; i++) { const key = V6_EXTRA_TARINAI_KEYS[i]; const encoded = encodeV6TarinaiValue(key, data?.[key], ctx); if (isEmptyCompactValue(encoded)) continue; if (defaults && approxSame(encoded, encodeV6TarinaiValue(key, defaults[key], ctx), key)) continue; out.push(i, encoded); } return out; } function unpackV6ExtrasTarinai(row, ctx) { const out = {}; if (!Array.isArray(row)) return out; for (let i = 0; i < row.length - 1; i += 2) { const key = V6_EXTRA_TARINAI_KEYS[row[i]]; if (!key) continue; out[key] = decodeV6TarinaiValue(key, row[i + 1], ctx); } return out; } function packV6Tarinai(worldRef, tarinai, ctx) { const data = snapshotEntity(worldRef, tarinai); delete data.name; delete data.parentNames; delete data.formerName; delete data.birthPersonality; delete data.personalityDaily; delete data.mysteryDrugHistory; delete data.recentChangeCauses; delete data.records; delete data.deathReason; delete data.favorite; delete data.zunchiSlaveSpriteVariant; delete data.zunchiStain; delete data.sunbathSpriteVariant; delete data.goodMode; delete data.facing; delete data.visualFacing; delete data.fallDir; delete data.hurtTimer; const defaults = v6DefaultTarinaiData(data); const core = packV6CoreTarinai(data, ctx); const extras = packV6ExtrasTarinai(data, defaults, ctx); return extras.length ? [core, extras] : core; } function unpackV6Tarinai(row, ctx) { const split = Array.isArray(row) && Array.isArray(row[0]) && Array.isArray(row[1]); const core = split ? row[0] : row; const extras = split ? row[1] : []; const data = unpackV6CoreTarinai(core, ctx); Object.assign(data, unpackV6ExtrasTarinai(extras, ctx)); if (!data.generation) data.generation = 1; if (!data.name) data.name = data.isZunchiSlave || data.zunchiSlaveLocked ? "zunchi_slave" : "tarinai"; if (!data.birthPersonality && data.currentPersonality) data.birthPersonality = clonePlain(data.currentPersonality); return data; } function encodeV6WorldValue(key, value) { if (key === "fieldType") return codeOrValue(value, V6_FIELD_TYPES); if (key === "weather") return codeOrValue(value, V6_WEATHERS); if (key === "colonyMood") return codeOrValue(value, V5_MOODS); return compactValue(value, 0, key); } function decodeV6WorldValue(key, value) { if (key === "fieldType") return valueOrCode(value, V6_FIELD_TYPES); if (key === "weather") return valueOrCode(value, V6_WEATHERS); if (key === "colonyMood") return { id: valueOrCode(value, V5_MOODS) || "relaxed" }; return clonePlain(value); } function packV6World(worldRef) { const world = worldData(worldRef); world.colonyMood = worldRef?.colonyMood?.id || world.colonyMood?.id || "relaxed"; delete world.frozenTarinai; delete world.family; const defaults = { w: 1000, h: 720, time: 0, day: 1, speed: 1, fieldType: "garden", fieldZoom: 1, cameraX: 0, cameraY: 0, weather: "sunny", deadCount: 0, liveIdNext: 1, liveIdSerial: 0, lastBirthAt: -999, maxGeneration: 1, colonyMood: "relaxed", lastColonyMoodDay: 0 }; return packSparseV5(world, V5_WORLD_KEYS, defaults, encodeV6WorldValue); } function unpackV6World(row) { return unpackSparseV5(row, V5_WORLD_KEYS, decodeV6WorldValue); } function packV6Items(worldRef, referencedIds) { const base = []; const extras = []; let index = 0; for (const item of worldRef.items || []) { if (!item || item.dead || ["trace", "splat"].includes(item.type)) continue; const data = snapshotEntity(worldRef, item); const type = data.type || item.type || ""; const keepId = Boolean(data.id && (referencedIds?.has?.(data.id) || type === "ant_nest" || type === "nest_box" || data.pinState === "lodged")); const defaults = v5ItemDefaultsFor(type, keepId); const extra = packSparseV5(data, V5_ITEM_EXTRA_KEYS, defaults, encodeV5ItemValue); base.push(codeFromList(type, V4_ITEM_TYPES), compactCoordinate(data.x || 0), compactCoordinate(data.y || 0)); if (extra.length) extras.push(index, extra); index += 1; } return extras.length ? [base, extras] : [base]; } function unpackV6Items(payload) { const base = Array.isArray(payload?.[0]) ? payload[0] : []; const extraPairs = Array.isArray(payload?.[1]) ? payload[1] : []; const extraByIndex = new Map(); for (let i = 0; i < extraPairs.length - 1; i += 2) extraByIndex.set(Number(extraPairs[i]) || 0, extraPairs[i + 1]); const out = []; for (let i = 0, index = 0; i < base.length - 2; i += 3, index++) { const type = valueFromList(base[i], V4_ITEM_TYPES); if (!type) continue; const data = unpackSparseV5(extraByIndex.get(index) || [], V5_ITEM_EXTRA_KEYS, decodeV5ItemValue); data.type = type; data.x = Number(base[i + 1] || 0); data.y = Number(base[i + 2] || 0); out.push(data); } return out; } function compactLossesV6(losses, ctx) { if (!Array.isArray(losses) || !losses.length) return []; const out = []; for (const loss of losses.slice(-8)) { if (!loss) continue; const rel = codeOrValue(loss.relation || "", V6_LOSS_RELATIONS); const fam = v6FamilyCode(loss.familyKey || "", ctx); const live = v6IdToCode(loss.liveId || ""); const row = [rel]; if (fam !== "") row.push(fam); if (live !== "") row.push(live); out.push(row); } return out; } function expandLossesV6(rows, ctx) { if (!Array.isArray(rows)) return []; return rows.map(row => Array.isArray(row) ? { relation: valueOrCode(row[0], V6_LOSS_RELATIONS) || "unknown", name: "unknown", familyKey: row.length > 1 ? v6FamilyValue(row[1], ctx) : "", liveId: row.length > 2 ? v6CodeToId(row[2]) : "", reason: "鬮ョ蠑ア繝サ繝サ・ス繝サ・サ鬮」雋サ・」・ー郢晢スサ繝サ・。", time: 0, } : null).filter(Boolean); } function compactFrozenEntryV6(entry = {}, ctx) { const data = entry.data || entry; const row = packV6Tarinai(null, data, ctx); const out = [compactNumber(entry.frozenAt || 0), compactNumber(entry.worldTime || 0), row]; const losses = compactLossesV6(entry.pendingLosses || [], ctx); if (losses.length) out.push(losses); return out; } function expandFrozenEntryV6(entry = [], ctx, index = 0) { if (!Array.isArray(entry)) return expandFrozenEntryV5(entry); const data = unpackV6Tarinai(entry[2] || [], ctx); const freezeId = `fz${index + 1}`; return { freezeId, frozenAt: entry[0] || 0, worldTime: entry[1] || 0, name: entry[3] || data.name || "tarinai", familyKey: data.familyKey || "", liveId: data.id || "", liveToken: data.liveToken || 0, type: data.type || "smile", data, pendingLosses: expandLossesV6(entry[3] || [], ctx) }; } function createV6Snapshot(worldRef = global.world) { if (!worldRef) throw new Error("world is not ready"); const frozen = Array.isArray(worldRef.frozenTarinai) ? worldRef.frozenTarinai : []; const maps = v6FamilyMaps(worldRef, frozen); const ctx = { familyMap: maps.map, familyList: maps.list }; const tarinaiRows = (worldRef.tarinai || []).filter(Boolean).map(t => packV6Tarinai(worldRef, t, ctx)); const tarinaiDataRows = (worldRef.tarinai || []).filter(Boolean).map(t => snapshotEntity(worldRef, t)); const referencedIds = v5ReferencedItemIds(worldRef, tarinaiDataRows); return [ V6_SNAPSHOT_VERSION, packV6World(worldRef), tarinaiRows, packV6Items(worldRef, referencedIds), (worldRef.ants || []).filter(a => a && !a.dead).map(a => packV5Ant(worldRef, a)), frozen.map(entry => compactFrozenEntryV6(entry, ctx)), ]; } function expandV6Snapshot(snapshot) { if (!Array.isArray(snapshot) || snapshot[0] !== V6_SNAPSHOT_VERSION) return snapshot; const ctx = {}; const world = unpackV6World(snapshot[1] || []); world.frozenTarinai = (snapshot[5] || []).map((entry, index) => expandFrozenEntryV6(entry, ctx, index)); return { version: SNAPSHOT_VERSION, app: "tarinai_colony_game", createdAt: Date.now(), summary: { day: world.day || 1, time: "", fieldType: world.fieldType || "garden", population: Array.isArray(snapshot[2]) ? snapshot[2].length : 0, items: Array.isArray(snapshot[3]?.[0]) ? Math.floor(snapshot[3][0].length / 3) : 0 }, world, tarinai: (snapshot[2] || []).map(row => unpackV6Tarinai(row, ctx)), items: unpackV6Items(snapshot[3] || []), ants: (snapshot[4] || []).map(unpackV5Ant), effects: [], logs: [], selectedRef: null, }; } // v7 hash format: alpha-only. It keeps individual names and gameplay state, // drops dead individuals from the family graph/save rows, and relies on the save // layer's printable-ASCII base91 transport for a shorter hash string. const V7_SNAPSHOT_VERSION = 7; const V7_CORE_TARINAI_KEYS = ["id", "familyKey", "x", "y", "age", "hunger", "loneliness", "energy", "stress", "affection", "need", "currentPersonality", "genetics", "adultScale", "scale", "lifeSpan", "state", "reproductionTimer", "digest"]; const V7_EXTRA_TARINAI_KEYS = ["type", "liveToken", "vx", "vy", "impulseVx", "impulseVy", "generation", "birthTime", "parents", "children", "relationships", "hasPaired", "loveMochiTimer", "fightMochiTimer", "zunchiDisease", "zunchiDiseaseSeverity", "sleepDisease", "explosionDisease", "explosionDiseaseTimer", "fightDisease", "totalFightLosses", "totalFightWins", "isZunchiSlave", "zunchiSlaveLocked", "poopCount", "oshiriByoZunchiStock", "stuckPushpinId", "itemEffectTimers", "laxativePoopTimer", "powerItemMode", "sizeItemMode", "lifeItemMode", "mercuryLifeSpanBase", "mercuryLifeMultiplier", "insideNestBoxId", "sunbathTimer", "sunbathCooldown", "fallTimer", "fearTimer"]; const V7_NUMERIC_BASE = { adultScale: 1, scale: 1, age: 10, lifeSpan: 1700, hunger: 30, loneliness: 40, energy: 75, stress: 10, affection: 15, reproductionTimer: 30, digest: 0, }; function v7LiveContext(worldRef, frozen = []) { const liveIds = new Set(); const liveFamilyKeys = new Set(); const frozenIds = new Set(); const frozenFamilyKeys = new Set(); for (const t of worldRef?.tarinai || []) { if (!t || t.dead) continue; if (t.id) liveIds.add(t.id); if (t.familyKey) liveFamilyKeys.add(String(t.familyKey)); } for (const entry of frozen || []) { const data = entry?.data || entry; if (!data) continue; if (data.id || entry.liveId) frozenIds.add(data.id || entry.liveId); if (data.familyKey || entry.familyKey) frozenFamilyKeys.add(String(data.familyKey || entry.familyKey)); } return { liveIds, liveFamilyKeys, frozenIds, frozenFamilyKeys }; } function v7FamilyMaps(worldRef, frozen = []) { const live = v7LiveContext(worldRef, frozen); const map = new Map(); const list = []; const add = (value) => { if (!value) return null; const key = String(value); if (!map.has(key)) { map.set(key, map.size); list.push(key); } return map.get(key); }; for (const t of worldRef?.tarinai || []) { if (!t || t.dead) continue; add(t.familyKey || t.archiveKey || t.id); for (const id of t.parents || []) if (live.liveFamilyKeys.has(String(id)) || live.frozenFamilyKeys.has(String(id))) add(id); for (const id of t.children || []) if (live.liveFamilyKeys.has(String(id)) || live.frozenFamilyKeys.has(String(id))) add(id); } for (const entry of frozen || []) { const data = entry?.data || entry; if (!data) continue; add(entry.familyKey || data.familyKey || data.id); for (const id of data.parents || []) if (live.liveFamilyKeys.has(String(id)) || live.frozenFamilyKeys.has(String(id))) add(id); for (const id of data.children || []) if (live.liveFamilyKeys.has(String(id)) || live.frozenFamilyKeys.has(String(id))) add(id); // Pending death losses are gameplay notifications for frozen individuals, not family-chart nodes. for (const loss of entry.pendingLosses || []) add(loss?.familyKey); } return { map, list, live }; } function v7IsKnownFamily(value, ctx) { if (!value) return false; const key = String(value); return Boolean(ctx?.live?.liveFamilyKeys?.has?.(key) || ctx?.live?.frozenFamilyKeys?.has?.(key)); } function v7EncodeRelativeNumber(key, value) { if (!Number.isFinite(value)) return undefined; if (!Object.prototype.hasOwnProperty.call(V7_NUMERIC_BASE, key)) return compactValue(value, 0, key); return compactNumber(value - V7_NUMERIC_BASE[key]); } function v7DecodeRelativeNumber(key, value) { if (!Object.prototype.hasOwnProperty.call(V7_NUMERIC_BASE, key)) return clonePlain(value); return compactNumber(Number(value || 0) + V7_NUMERIC_BASE[key]); } function compactRelationshipsV7(rels, ctx) { if (!rels || typeof rels !== "object") return undefined; const rows = []; const allowed = ctx?.live?.liveIds || null; for (const [id, r] of Object.entries(rels)) { if (!id || !r) continue; if (allowed && !allowed.has(id)) continue; const row = [v6RelationKey(id), compactMetric(r.affinity || 0), compactMetric(r.fear || 0), compactNumber(r.fightsWon || 0), compactNumber(r.fightsLost || 0)]; while (row.length > 1 && (row[row.length - 1] === 0 || row[row.length - 1] === "" || row[row.length - 1] === null || row[row.length - 1] === undefined)) row.pop(); if (row.length > 1) rows.push(row); } rows.sort((a, b) => { const score = row => Math.abs(row[1] || 0) + Math.abs(row[2] || 0) + Math.abs(row[3] || 0) * 2 + Math.abs(row[4] || 0) * 2; return score(b) - score(a); }); return rows.length ? rows.slice(0, 20) : undefined; } function encodeV7TarinaiValue(key, value, ctx) { if (key === "familyKey") return v6FamilyCode(value, ctx); if (key === "id") return v6IdToCode(value); if (key === "name") return value || "tarinai"; if (key === "type") return codeOrValue(value, V4_SPRITE_TYPES); if (key === "need") return codeOrValue(value, V5_NEEDS); if (key === "state") return codeOrValue(value, V5_STATES); if (key === "currentPersonality") return packAxisObject(value, PERSONALITY_ORDER); if (key === "genetics") return packAxisObject(value, GENETIC_ORDER, { lifeSpanMul: 1, attackMul: 1, speedMul: 1, sizeMul: 1 }); if (key === "parents" || key === "children") return Array.isArray(value) && value.length ? value.filter(v => v7IsKnownFamily(v, ctx)).map(v => v6FamilyCode(v, ctx)).filter(v => v !== "") : undefined; if (key === "relationships") return compactRelationshipsV7(value, ctx); if (key === "itemEffectTimers") return compactItemEffectTimers(value); if (Object.prototype.hasOwnProperty.call(V7_NUMERIC_BASE, key)) return v7EncodeRelativeNumber(key, Number(value)); return compactValue(value, 0, key); } function decodeV7TarinaiValue(key, value, ctx) { if (key === "familyKey") return v6FamilyValue(value, ctx); if (key === "id") return v6CodeToId(value); if (key === "name") return value || "tarinai"; if (key === "type") return valueOrCode(value, V4_SPRITE_TYPES); if (key === "need") return valueOrCode(value, V5_NEEDS); if (key === "state") return valueOrCode(value, V5_STATES); if (key === "currentPersonality") return unpackAxisObject(value, PERSONALITY_ORDER); if (key === "genetics") return unpackAxisObject(value, GENETIC_ORDER, { lifeSpanMul: 1, attackMul: 1, speedMul: 1, sizeMul: 1 }); if (key === "parents" || key === "children") return Array.isArray(value) ? value.map(v => v6FamilyValue(v, ctx)).filter(Boolean) : []; if (key === "relationships") return expandRelationshipsV6(value); if (key === "itemEffectTimers") return expandItemEffectTimers(value); if (Object.prototype.hasOwnProperty.call(V7_NUMERIC_BASE, key)) return v7DecodeRelativeNumber(key, value); return clonePlain(value); } function v7DefaultTarinaiData() { const out = v6DefaultTarinaiData({}); out.name = "tarinai"; return out; } function packV7CoreTarinai(data, ctx) { const row = V7_CORE_TARINAI_KEYS.map(key => { const encoded = encodeV7TarinaiValue(key, data?.[key], ctx); return isEmptyCompactValue(encoded) ? null : encoded; }); while (row.length && (row[row.length - 1] === null || row[row.length - 1] === undefined || isEmptyCompactValue(row[row.length - 1]))) row.pop(); return row; } function unpackV7CoreTarinai(row, ctx) { const out = {}; if (!Array.isArray(row)) return out; for (let i = 0; i < Math.min(row.length, V7_CORE_TARINAI_KEYS.length); i++) { const value = row[i]; if (value === null || value === undefined) continue; const key = V7_CORE_TARINAI_KEYS[i]; out[key] = decodeV7TarinaiValue(key, value, ctx); } return out; } function packV7ExtrasTarinai(data, defaults, ctx) { const out = []; for (let i = 0; i < V7_EXTRA_TARINAI_KEYS.length; i++) { const key = V7_EXTRA_TARINAI_KEYS[i]; const encoded = encodeV7TarinaiValue(key, data?.[key], ctx); if (isEmptyCompactValue(encoded)) continue; if (defaults && approxSame(encoded, encodeV7TarinaiValue(key, defaults[key], ctx), key)) continue; out.push(i, encoded); } return out; } function unpackV7ExtrasTarinai(row, ctx) { const out = {}; if (!Array.isArray(row)) return out; for (let i = 0; i < row.length - 1; i += 2) { const key = V7_EXTRA_TARINAI_KEYS[row[i]]; if (!key) continue; out[key] = decodeV7TarinaiValue(key, row[i + 1], ctx); } return out; } function packV7Tarinai(worldRef, tarinai, ctx) { const data = snapshotEntity(worldRef, tarinai); data.parents = Array.isArray(data.parents) ? data.parents.filter(v => v7IsKnownFamily(v, ctx)) : []; data.children = Array.isArray(data.children) ? data.children.filter(v => v7IsKnownFamily(v, ctx)) : []; delete data.parentNames; delete data.formerName; delete data.birthPersonality; delete data.personalityDaily; delete data.mysteryDrugHistory; delete data.recentChangeCauses; delete data.records; delete data.deathReason; delete data.favorite; delete data.dead; delete data.zunchiSlaveSpriteVariant; delete data.zunchiStain; delete data.sunbathSpriteVariant; delete data.goodMode; delete data.facing; delete data.visualFacing; delete data.fallDir; delete data.hurtTimer; const defaults = v7DefaultTarinaiData(); const core = packV7CoreTarinai(data, ctx); const extras = packV7ExtrasTarinai(data, defaults, ctx); return extras.length ? [core, extras] : core; } function unpackV7Tarinai(row, ctx) { const split = Array.isArray(row) && Array.isArray(row[0]) && Array.isArray(row[1]); const core = split ? row[0] : row; const extras = split ? row[1] : []; const data = unpackV7CoreTarinai(core, ctx); Object.assign(data, unpackV7ExtrasTarinai(extras, ctx)); if (!data.generation) data.generation = 1; if (!data.name) data.name = data.isZunchiSlave || data.zunchiSlaveLocked ? "zunchi_slave" : "tarinai"; if (!data.birthPersonality && data.currentPersonality) data.birthPersonality = clonePlain(data.currentPersonality); data.dead = false; return data; } function compactLossesV7(losses, ctx) { return compactLossesV6(losses, ctx); } function compactFrozenEntryV7(entry = {}, ctx) { const data = entry.data || entry; const row = packV7Tarinai(null, data, ctx); const out = [compactNumber(entry.frozenAt || 0), compactNumber(entry.worldTime || 0), row]; const name = data.name || entry.name || ""; const losses = compactLossesV7(entry.pendingLosses || [], ctx); if (name && name !== "tarinai") out.push(name); else if (losses.length) out.push(null); if (losses.length) out.push(losses); return out; } function expandFrozenEntryV7(entry = [], ctx, index = 0) { if (!Array.isArray(entry)) return expandFrozenEntryV6(entry, ctx, index); const data = unpackV7Tarinai(entry[2] || [], ctx); const freezeId = `fz${index + 1}`; return { freezeId, frozenAt: entry[0] || 0, worldTime: entry[1] || 0, name: entry[3] || data.name || "tarinai", familyKey: data.familyKey || "", liveId: data.id || "", liveToken: data.liveToken || 0, type: data.type || "smile", data, pendingLosses: expandLossesV6(entry[4] || [], ctx) }; } function createV7Snapshot(worldRef = global.world) { if (!worldRef) throw new Error("world is not ready"); const frozen = Array.isArray(worldRef.frozenTarinai) ? worldRef.frozenTarinai : []; const maps = v7FamilyMaps(worldRef, frozen); const ctx = { familyMap: maps.map, familyList: maps.list, live: maps.live }; const aliveTarinai = (worldRef.tarinai || []).filter(t => t && !t.dead); const tarinaiRows = aliveTarinai.map(t => packV7Tarinai(worldRef, t, ctx)); const names = aliveTarinai.map(t => t?.name || "tarinai").join("\n"); const tarinaiDataRows = aliveTarinai.map(t => snapshotEntity(worldRef, t)); const referencedIds = v5ReferencedItemIds(worldRef, tarinaiDataRows); return [ V7_SNAPSHOT_VERSION, packV6World(worldRef), names, tarinaiRows, packV6Items(worldRef, referencedIds), (worldRef.ants || []).filter(a => a && !a.dead).map(a => packV5Ant(worldRef, a)), frozen.map(entry => compactFrozenEntryV7(entry, ctx)), ]; } function expandV7Snapshot(snapshot) { if (!Array.isArray(snapshot) || snapshot[0] !== V7_SNAPSHOT_VERSION) return snapshot; const ctx = {}; const world = unpackV6World(snapshot[1] || []); const names = typeof snapshot[2] === "string" ? snapshot[2].split("\n") : (Array.isArray(snapshot[2]) ? snapshot[2] : []); const tarinaiRows = Array.isArray(snapshot[3]) ? snapshot[3] : []; world.frozenTarinai = (snapshot[6] || []).map((entry, index) => expandFrozenEntryV7(entry, ctx, index)); return { version: SNAPSHOT_VERSION, app: "tarinai_colony_game", createdAt: Date.now(), summary: { day: world.day || 1, time: "", fieldType: world.fieldType || "garden", population: tarinaiRows.length, items: Array.isArray(snapshot[4]?.[0]) ? Math.floor(snapshot[4][0].length / 3) : 0 }, world, tarinai: tarinaiRows.map((row, index) => { const data = unpackV7Tarinai(row, ctx); if (names[index]) data.name = names[index]; return data; }), items: unpackV6Items(snapshot[4] || []), ants: (snapshot[5] || []).map(unpackV5Ant), effects: [], logs: [], selectedRef: null, }; } // v8 hash format: alpha-only and intentionally drops legacy import support. // It keeps gameplay state and individual names, removes frozen memories, saves no dead family nodes, // and uses a tiny reset-sentinel for untouched initial colonies. const V8_SNAPSHOT_VERSION = 8; function v8Names(worldRef) { return (worldRef?.tarinai || []).filter(t => t && !t.dead).map(t => t.name || "tarinai").join("\n"); } function v8InitialCandidate(worldRef) { if (!worldRef) return false; if ((worldRef.day || 1) !== 1 || Math.abs(worldRef.time || 0) > 0.08) return false; if ((worldRef.deadCount || 0) !== 0) return false; if ((worldRef.ants || []).some(a => a && !a.dead)) return false; if ((worldRef.effects || []).length) return false; if ((worldRef.frozenTarinai || []).length) return false; const field = (typeof FIELD_TYPES !== "undefined" ? FIELD_TYPES?.[worldRef.fieldType || "garden"] : null) || null; const expectedPop = field?.population ?? (typeof CONFIG !== "undefined" ? CONFIG.initialPopulation : null); const expectedGrass = field?.grass ?? 18; const living = (worldRef.tarinai || []).filter(t => t && !t.dead); if (Number.isFinite(expectedPop) && living.length !== expectedPop) return false; for (const t of living) { if ((t.parents || []).length || (t.children || []).length || Object.keys(t.relationships || {}).length) return false; if (t.state && t.state !== "idle") return false; if (t.isZunchiSlave || t.zunchiSlaveLocked || t.zunchiDisease || t.sleepDisease || t.explosionDisease || t.fightDisease) return false; if (t.stuckPushpinId || t.insideNestBoxId || (t.oshiriByoZunchiStock || 0) > 0) return false; } const counts = {}; for (const it of worldRef.items || []) { if (!it || it.dead || ["trace", "splat"].includes(it.type)) continue; counts[it.type] = (counts[it.type] || 0) + 1; if (!["grass", "stone", "bed"].includes(it.type)) return false; } return (counts.grass || 0) === expectedGrass && (counts.stone || 0) === 1 && (counts.bed || 0) === 2; } function compactFrozenEntryV8(entry = {}, ctx) { const data = clonePlain(entry.data || entry) || {}; delete data.relationships; delete data.records; delete data.recentChangeCauses; delete data.personalityDaily; delete data.mysteryDrugHistory; delete data.parentNames; const row = packV7Tarinai(null, data, ctx); const out = [compactNumber(entry.frozenAt || 0), compactNumber(entry.worldTime || 0), row]; const name = data.name || entry.name || ""; if (name && name !== "tarinai") out.push(name); return out; } function expandFrozenEntryV8(entry = [], ctx, index = 0) { if (!Array.isArray(entry)) return expandFrozenEntryV7(entry, ctx, index); const data = unpackV7Tarinai(entry[2] || [], ctx); if (entry[3]) data.name = entry[3]; const freezeId = `fz${index + 1}`; return { freezeId, frozenAt: entry[0] || 0, worldTime: entry[1] || 0, name: data.name || "tarinai", familyKey: data.familyKey || "", liveId: data.id || "", liveToken: data.liveToken || 0, type: data.type || "smile", data, pendingLosses: [] }; } function createV8Snapshot(worldRef = global.world) { if (!worldRef) throw new Error("world is not ready"); const fieldCode = codeOrValue(worldRef.fieldType || "garden", V6_FIELD_TYPES); if (v8InitialCandidate(worldRef)) { const names = v8Names(worldRef); return [V8_SNAPSHOT_VERSION, 0, fieldCode, names || ""]; } const frozen = Array.isArray(worldRef.frozenTarinai) ? worldRef.frozenTarinai : []; const maps = v7FamilyMaps(worldRef, frozen); const ctx = { familyMap: maps.map, familyList: maps.list, live: maps.live }; const aliveTarinai = (worldRef.tarinai || []).filter(t => t && !t.dead); const tarinaiRows = aliveTarinai.map(t => packV7Tarinai(worldRef, t, ctx)); const names = aliveTarinai.map(t => t?.name || "tarinai").join("\n"); const tarinaiDataRows = aliveTarinai.map(t => snapshotEntity(worldRef, t)); const referencedIds = v5ReferencedItemIds(worldRef, tarinaiDataRows); return [ V8_SNAPSHOT_VERSION, packV6World(worldRef), names, tarinaiRows, packV6Items(worldRef, referencedIds), (worldRef.ants || []).filter(a => a && !a.dead).map(a => packV5Ant(worldRef, a)), frozen.map(entry => compactFrozenEntryV8(entry, ctx)), ]; } function expandV8Snapshot(snapshot) { if (!Array.isArray(snapshot) || snapshot[0] !== V8_SNAPSHOT_VERSION || snapshot[1] === 0) return snapshot; const ctx = {}; const world = unpackV6World(snapshot[1] || []); const names = typeof snapshot[2] === "string" ? snapshot[2].split("\n") : (Array.isArray(snapshot[2]) ? snapshot[2] : []); const tarinaiRows = Array.isArray(snapshot[3]) ? snapshot[3] : []; world.frozenTarinai = (snapshot[6] || []).map((entry, index) => expandFrozenEntryV8(entry, ctx, index)); return { version: SNAPSHOT_VERSION, app: "tarinai_colony_game", createdAt: Date.now(), summary: { day: world.day || 1, time: "", fieldType: world.fieldType || "garden", population: tarinaiRows.length, items: Array.isArray(snapshot[4]?.[0]) ? Math.floor(snapshot[4][0].length / 3) : 0 }, world, tarinai: tarinaiRows.map((row, index) => { const data = unpackV7Tarinai(row, ctx); if (names[index]) data.name = names[index]; return data; }), items: unpackV6Items(snapshot[4] || []), ants: (snapshot[5] || []).map(unpackV5Ant), effects: [], logs: [], selectedRef: null, }; } function restoreV8InitialSnapshot(snapshot, worldRef = global.world) { if (!Array.isArray(snapshot) || snapshot[0] !== V8_SNAPSHOT_VERSION || snapshot[1] !== 0) return false; const fieldType = valueOrCode(snapshot[2], V6_FIELD_TYPES) || "garden"; if (typeof applyFieldLayout === "function") applyFieldLayout(fieldType); if (typeof worldRef.reset === "function") worldRef.reset(null, fieldType); const names = typeof snapshot[3] === "string" ? snapshot[3].split("\n") : []; const living = (worldRef.tarinai || []).filter(t => t && !t.dead); for (let i = 0; i < Math.min(names.length, living.length); i++) if (names[i]) living[i].name = names[i]; worldRef.logs = []; worldRef.selected = null; worldRef.familyTreeDirty = true; worldRef.drawListDirty = true; worldRef.terrainDirty = true; if (typeof worldRef.updateItemCounts === "function") worldRef.updateItemCounts(); if (typeof worldRef.updateEffectCounts === "function") worldRef.updateEffectCounts(); if (typeof worldRef.rebuildSpatial === "function") worldRef.rebuildSpatial(true); if (typeof renderLog === "function") renderLog([]); 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; } // v9 hash format: alpha-only and current-only. // This removes the V8 initial-state sentinel and uses one binary packet for every state. // Game-play state is kept; observation history, dead family nodes, and frozen memories are omitted. const V9_SNAPSHOT_VERSION = 9; const V9_NAME_CHARS = "\u306f\u3046\u3045\u3063"; const V9_BOOLEAN_FLAGS = ["hasPaired", "zunchiDisease", "sleepDisease", "explosionDisease", "fightDisease", "isZunchiSlave", "zunchiSlaveLocked"]; const V9_CORE_NUMERIC_KEYS = ["age", "hunger", "loneliness", "energy", "stress", "affection", "adultScale", "scale", "lifeSpan", "reproductionTimer", "digest"]; const V9_EXTRA_TARINAI_KEYS = ["type", "state", "vx", "vy", "impulseVx", "impulseVy", "generation", "birthTime", "parents", "children", "relationships", "hasPaired", "loveMochiTimer", "fightMochiTimer", "zunchiDiseaseSeverity", "explosionDiseaseTimer", "totalFightLosses", "totalFightWins", "poopCount", "oshiriByoZunchiStock", "stuckPushpinId", "itemEffectTimers", "laxativePoopTimer", "powerItemMode", "sizeItemMode", "lifeItemMode", "mercuryLifeSpanBase", "mercuryLifeMultiplier", "insideNestBoxId", "sunbathTimer", "sunbathCooldown", "fallTimer", "fearTimer"]; const V9_TIMER_KEYS = new Set(["loveMochiTimer", "fightMochiTimer", "explosionDiseaseTimer", "laxativePoopTimer", "sunbathTimer", "sunbathCooldown", "fallTimer", "fearTimer"]); const V9_ITEM_EXTRA_KEYS = ["r", "amount", "age", "toolSize", "foodServingScale", "foodServingsMax", "foodServingsRemaining", "growth", "health", "seedTimer", "eatenAmount", "fertilityBoost", "lifeSpan", "wither", "comfort", "wear", "text", "vx", "vy", "spin", "spinVelocity", "stage", "stageTimer", "fertility", "freshness", "zunchiVariant", "antCount", "antSpawnTimer", "antSpawnCooldown", "antWorkers", "queenSpawnAt", "workerSprite", "fuseTimer", "fuseMax", "dropTimer", "dropMax", "pinState", "pinTargetId", "pinAttachAngle", "pinAttachDistance", "pinOffsetY", "pinFlipX", "machineSlotType", "machineSlotSize", "machineSpawnTimer", "machineLastSlotType", "machineProduced", "machineProducerId", "sourceMachineType", "infiniteServing", "noDecay", "cannonAngle", "cannonFireTimer", "cannonProjectile", "projectileSourceId", "projectileTimer"]; const V9_ANT_KEYS = ["kind", "homeId", "targetId", "targetToken", "x", "y", "vx", "vy", "r", "maxHp", "hp", "state", "age", "seed", "wanderAngle", "nextTurn", "returnTimer", "foundingX", "foundingY", "foundingDelayUntil", "foundingAttempts"]; const V9_WORLD_KEYS = ["w", "h", "time", "fieldType", "weather", "nextWeatherChange", "deadCount", "liveIdSerial", "lastBirthAt", "maxGeneration", "colonyMood", "lastColonyMoodDay"]; function v9Coord(value) { return Math.round((Number(value) || 0) * 10); } function v9Uncoord(value) { return compactCoordinate((Number(value) || 0) / 10); } function v9Num1(value) { return Math.round((Number(value) || 0) * 10); } function v9Unnum1(value) { return compactNumber((Number(value) || 0) / 10); } function v9Diff1(key, value) { return v9Num1((Number(value) || 0) - (V7_NUMERIC_BASE[key] || 0)); } function v9Undiff1(key, value) { return compactNumber((V7_NUMERIC_BASE[key] || 0) + ((Number(value) || 0) / 10)); } function v9PackName(name) { const s = String(name || "tarinai"); if (s === "zunchi_slave") return -1; if (s === "tarinai") return -2; if (s.length > 0 && s.length <= 15 && Array.from(s).every(ch => V9_NAME_CHARS.includes(ch))) { let code = s.length & 15; let shift = 4; for (const ch of s) { code |= (V9_NAME_CHARS.indexOf(ch) & 3) << shift; shift += 2; } return code; } return s; } function v9UnpackName(value) { if (value === -1) return "zunchi_slave"; if (value === -2 || value === undefined || value === null || value === "") return "tarinai"; if (typeof value !== "number") return String(value || "tarinai"); const len = value & 15; let out = ""; for (let i = 0, shift = 4; i < len; i++, shift += 2) out += V9_NAME_CHARS[(value >> shift) & 3] || "\\u306f"; return out || "tarinai"; } function v9LiveLists(worldRef, frozen = []) { const alive = (worldRef?.tarinai || []).filter(t => t && !t.dead); const frozenRows = (frozen || []).map(entry => entry?.data || entry).filter(Boolean); const all = [...alive, ...frozenRows]; const idMap = new Map(); const familyMap = new Map(); all.forEach((t, index) => { if (t?.id) idMap.set(String(t.id), index); if (t?.familyKey) familyMap.set(String(t.familyKey), index); if (t?.archiveKey) familyMap.set(String(t.archiveKey), index); }); return { alive, frozenRows, all, idMap, familyMap, liveCount: alive.length }; } function v9ItemList(worldRef) { const items = (worldRef?.items || []).filter(it => it && !it.dead && !["trace", "splat"].includes(it.type)); const itemMap = new Map(); items.forEach((it, index) => { if (it?.id) itemMap.set(String(it.id), index); }); return { items, itemMap }; } function v9PackTarinaiRef(value, ctx) { if (!value) return undefined; const idx = ctx?.idMap?.get?.(String(value)); return idx === undefined ? undefined : idx + 1; } function v9PackFamilyRef(value, ctx) { if (!value) return undefined; const idx = ctx?.familyMap?.get?.(String(value)); return idx === undefined ? undefined : idx + 1; } function v9PackItemRef(value, ctx) { if (!value) return undefined; const idx = ctx?.itemMap?.get?.(String(value)); return idx === undefined ? undefined : idx + 1; } function v9TarinaiId(index) { return `t${index + 1}`; } function v9FamilyKey(index) { return `f${index + 1}`; } function v9ItemId(index) { return `i${index + 1}`; } function v9AntId(index) { return `a${index + 1}`; } function v9PackAxisDelta(obj, keys, defaults = null, scale = 100) { if (!obj || typeof obj !== "object") return undefined; let mask = 0; const vals = []; for (let i = 0; i < keys.length; i++) { const key = keys[i]; const base = defaults?.[key] ?? 0; const diff = Math.round(((Number(obj[key]) || 0) - base) * scale); if (!diff) continue; mask |= (1 << i); vals.push(diff); } return mask ? [mask, ...vals] : undefined; } function v9UnpackAxisDelta(row, keys, defaults = null, scale = 100) { const out = defaults && typeof defaults === "object" ? clonePlain(defaults) || {} : {}; if (!Array.isArray(row) || !row.length) return out; const mask = Number(row[0]) || 0; let p = 1; for (let i = 0; i < keys.length; i++) { const key = keys[i]; const base = defaults?.[key] ?? 0; out[key] = mask & (1 << i) ? compactNumber(base + ((Number(row[p++]) || 0) / scale)) : base; } return out; } function v9PackRelationships(rels, ctx) { if (!rels || typeof rels !== "object") return undefined; const rows = []; for (const [id, r] of Object.entries(rels)) { const idx = v9PackTarinaiRef(id, ctx); if (idx === undefined || !r) continue; const aff = v9Num1(r.affinity || 0); const fear = v9Num1(r.fear || 0); const wins = Math.round(Number(r.fightsWon || 0)); const losses = Math.round(Number(r.fightsLost || 0)); if (!aff && !fear && !wins && !losses) continue; const row = [idx, aff, fear, wins, losses]; while (row.length > 1 && !row[row.length - 1]) row.pop(); rows.push(row); } rows.sort((a, b) => (Math.abs(b[1] || 0) + Math.abs(b[2] || 0) + (b[3] || 0) * 20 + (b[4] || 0) * 20) - (Math.abs(a[1] || 0) + Math.abs(a[2] || 0) + (a[3] || 0) * 20 + (a[4] || 0) * 20)); return rows.length ? rows.slice(0, 20) : undefined; } function v9UnpackRelationships(rows) { if (!Array.isArray(rows)) return {}; const out = {}; for (const row of rows) { if (!Array.isArray(row) || !Number.isFinite(row[0])) continue; out[v9TarinaiId(row[0] - 1)] = { affinity: v9Unnum1(row[1] || 0), fear: v9Unnum1(row[2] || 0), fightsWon: Number(row[3] || 0), fightsLost: Number(row[4] || 0), lastEvent: "", lastTime: 0 }; } return out; } function v9PackItemEffectTimers(value) { if (!value || typeof value !== "object") return undefined; const row = [v9Num1(value.laxative || 0), v9Num1(value.ammo || 0)]; while (row.length && !row[row.length - 1]) row.pop(); return row.length ? row : undefined; } function v9UnpackItemEffectTimers(row) { return { laxative: Array.isArray(row) ? v9Unnum1(row[0] || 0) : 0, ammo: Array.isArray(row) ? v9Unnum1(row[1] || 0) : 0 }; } function v9EncodeTarinaiExtra(key, value, ctx) { if (value === undefined || value === null || value === "") return undefined; if (key === "type") return codeOrValue(value, V4_SPRITE_TYPES); if (key === "state") return codeOrValue(value, V5_STATES); if (key === "parents" || key === "children") { const arr = Array.isArray(value) ? value.map(v => v9PackFamilyRef(v, ctx)).filter(v => v !== undefined) : []; return arr.length ? arr : undefined; } if (key === "relationships") return v9PackRelationships(value, ctx); if (key === "stuckPushpinId" || key === "insideNestBoxId") return v9PackItemRef(value, ctx); if (key === "itemEffectTimers") return v9PackItemEffectTimers(value); if (key === "powerItemMode") return codeOrValue(value, ["", "protein", "niteropu"]); if (key === "sizeItemMode") return codeOrValue(value, ["", "giant_drug", "dwarf_drug"]); if (key === "lifeItemMode") return codeOrValue(value, ["", "mercury"]); if (V9_TIMER_KEYS.has(key)) return v9Num1(value || 0); if (["vx", "vy", "impulseVx", "impulseVy"].includes(key)) return v9Num1(value || 0); return compactValue(value, 0, key); } function v9DecodeTarinaiExtra(key, value) { if (key === "type") return valueOrCode(value, V4_SPRITE_TYPES); if (key === "state") return valueOrCode(value, V5_STATES); if (key === "parents" || key === "children") return Array.isArray(value) ? value.map(v => v9FamilyKey(v - 1)).filter(Boolean) : []; if (key === "relationships") return v9UnpackRelationships(value); if (key === "stuckPushpinId" || key === "insideNestBoxId") return Number.isFinite(value) ? v9ItemId(value - 1) : null; if (key === "itemEffectTimers") return v9UnpackItemEffectTimers(value); if (key === "powerItemMode") return valueOrCode(value, ["", "protein", "niteropu"]); if (key === "sizeItemMode") return valueOrCode(value, ["", "giant_drug", "dwarf_drug"]); if (key === "lifeItemMode") return valueOrCode(value, ["", "mercury"]); if (V9_TIMER_KEYS.has(key)) return v9Unnum1(value || 0); if (["vx", "vy", "impulseVx", "impulseVy"].includes(key)) return v9Unnum1(value || 0); return clonePlain(value); } function v9PackFlags(data) { let flags = 0; for (let i = 0; i < V9_BOOLEAN_FLAGS.length; i++) if (data?.[V9_BOOLEAN_FLAGS[i]]) flags |= (1 << i); return flags; } function v9ApplyFlags(data, flags) { for (let i = 0; i < V9_BOOLEAN_FLAGS.length; i++) data[V9_BOOLEAN_FLAGS[i]] = Boolean(flags & (1 << i)); } function v9PackTarinai(worldRef, t, ctx) { const data = snapshotEntity(worldRef, t); data.parents = Array.isArray(data.parents) ? data.parents.filter(v => v9PackFamilyRef(v, ctx) !== undefined) : []; data.children = Array.isArray(data.children) ? data.children.filter(v => v9PackFamilyRef(v, ctx) !== undefined) : []; const row = [v9PackName(data.name), v9Coord(data.x), v9Coord(data.y), codeOrValue(data.need || "", V5_NEEDS)]; const numeric = []; for (let i = 0; i < V9_CORE_NUMERIC_KEYS.length; i++) { const key = V9_CORE_NUMERIC_KEYS[i]; const diff = v9Diff1(key, Number(data[key] || 0)); if (diff) numeric.push(i, diff); } if (numeric.length) row.push(numeric); else row.push(null); const personality = v9PackAxisDelta(data.currentPersonality, PERSONALITY_ORDER, null, 100); const genetics = v9PackAxisDelta(data.genetics, GENETIC_ORDER, { lifeSpanMul: 1, attackMul: 1, speedMul: 1, sizeMul: 1 }, 100); const axes = []; if (personality) axes.push(0, personality); if (genetics) axes.push(1, genetics); row.push(axes.length ? axes : null); const flags = v9PackFlags(data); row.push(flags || null); const defaults = v7DefaultTarinaiData(); const extras = []; for (let i = 0; i < V9_EXTRA_TARINAI_KEYS.length; i++) { const key = V9_EXTRA_TARINAI_KEYS[i]; if (V9_BOOLEAN_FLAGS.includes(key)) continue; const encoded = v9EncodeTarinaiExtra(key, data[key], ctx); if (isEmptyCompactValue(encoded)) continue; const def = v9EncodeTarinaiExtra(key, defaults[key], ctx); if (approxSame(encoded, def, key)) continue; extras.push(i, encoded); } row.push(extras.length ? extras : null); while (row.length > 4 && isEmptyCompactValue(row[row.length - 1])) row.pop(); return row; } function v9UnpackTarinai(row, index = 0) { const data = { id: v9TarinaiId(index), familyKey: v9FamilyKey(index), liveToken: index + 1, generation: 1, state: "idle", type: "smile", parents: [], children: [], relationships: {}, itemEffectTimers: { laxative: 0, ammo: 0 } }; if (!Array.isArray(row)) return data; data.name = v9UnpackName(row[0]); data.x = v9Uncoord(row[1] || 0); data.y = v9Uncoord(row[2] || 0); data.need = valueOrCode(row[3], V5_NEEDS) || ""; for (const key of V9_CORE_NUMERIC_KEYS) data[key] = V7_NUMERIC_BASE[key] ?? 0; const numeric = Array.isArray(row[4]) ? row[4] : []; for (let i = 0; i < numeric.length - 1; i += 2) { const key = V9_CORE_NUMERIC_KEYS[numeric[i]]; if (key) data[key] = v9Undiff1(key, numeric[i + 1]); } const axes = Array.isArray(row[5]) ? row[5] : []; for (let i = 0; i < axes.length - 1; i += 2) { if (axes[i] === 0) data.currentPersonality = v9UnpackAxisDelta(axes[i + 1], PERSONALITY_ORDER, null, 100); if (axes[i] === 1) data.genetics = v9UnpackAxisDelta(axes[i + 1], GENETIC_ORDER, { lifeSpanMul: 1, attackMul: 1, speedMul: 1, sizeMul: 1 }, 100); } if (!data.currentPersonality) data.currentPersonality = {}; if (!data.birthPersonality) data.birthPersonality = clonePlain(data.currentPersonality); if (!data.genetics) data.genetics = { lifeSpanMul: 1, attackMul: 1, speedMul: 1, sizeMul: 1 }; v9ApplyFlags(data, Number(row[6] || 0)); const extras = Array.isArray(row[7]) ? row[7] : []; for (let i = 0; i < extras.length - 1; i += 2) { const key = V9_EXTRA_TARINAI_KEYS[extras[i]]; if (!key) continue; data[key] = v9DecodeTarinaiExtra(key, extras[i + 1]); } if (data.isZunchiSlave || data.zunchiSlaveLocked) { data.isZunchiSlave = true; data.zunchiSlaveLocked = true; data.name = "zunchi_slave"; } data.dead = false; return data; } function v9EncodeItemValue(key, value, ctx) { if (value === undefined || value === null || value === "") return undefined; if (key === "toolSize" || key === "machineSlotSize") return codeOrValue(value, V5_SIZES); if (key === "pinState") return codeOrValue(value, V5_PIN_STATES); if (key === "stage") return codeOrValue(value, V5_ZUNCHI_STAGES); if (["machineSlotType", "machineLastSlotType", "sourceMachineType"].includes(key)) return codeOrValue(value, V4_ITEM_TYPES); if (key === "pinTargetId") return v9PackTarinaiRef(value, ctx); if (["machineProducerId", "projectileSourceId"].includes(key)) return v9PackItemRef(value, ctx); if (["vx", "vy", "spinVelocity", "pinAttachAngle", "pinAttachDistance", "pinOffsetY", "cannonAngle"].includes(key)) return v9Num1(value || 0); return compactValue(value, 0, key); } function v9DecodeItemValue(key, value) { if (key === "toolSize" || key === "machineSlotSize") return valueOrCode(value, V5_SIZES); if (key === "pinState") return valueOrCode(value, V5_PIN_STATES); if (key === "stage") return valueOrCode(value, V5_ZUNCHI_STAGES); if (["machineSlotType", "machineLastSlotType", "sourceMachineType"].includes(key)) return valueOrCode(value, V4_ITEM_TYPES); if (key === "pinTargetId") return Number.isFinite(value) ? v9TarinaiId(value - 1) : ""; if (["machineProducerId", "projectileSourceId"].includes(key)) return Number.isFinite(value) ? v9ItemId(value - 1) : ""; if (["vx", "vy", "spinVelocity", "pinAttachAngle", "pinAttachDistance", "pinOffsetY", "cannonAngle"].includes(key)) return v9Unnum1(value || 0); return clonePlain(value); } function v9PackItems(worldRef, ctx) { const base = []; const extras = []; for (let index = 0; index < ctx.items.length; index++) { const item = ctx.items[index]; const data = snapshotEntity(worldRef, item); const type = data.type || item.type || ""; base.push(codeFromList(type, V4_ITEM_TYPES), v9Coord(data.x), v9Coord(data.y)); const defaults = itemDefaultsFor(type); const pairs = []; for (let i = 0; i < V9_ITEM_EXTRA_KEYS.length; i++) { const key = V9_ITEM_EXTRA_KEYS[i]; const encoded = v9EncodeItemValue(key, data[key], ctx); if (isEmptyCompactValue(encoded)) continue; const def = v9EncodeItemValue(key, defaults[key], ctx); if (approxSame(encoded, def, key)) continue; pairs.push(i, encoded); } if (pairs.length) extras.push(index, pairs); } return extras.length ? [base, extras] : [base]; } function v9UnpackItems(payload) { const base = Array.isArray(payload?.[0]) ? payload[0] : []; const extraPairs = Array.isArray(payload?.[1]) ? payload[1] : []; const extraByIndex = new Map(); for (let i = 0; i < extraPairs.length - 1; i += 2) extraByIndex.set(Number(extraPairs[i]) || 0, extraPairs[i + 1]); const out = []; for (let i = 0, index = 0; i < base.length - 2; i += 3, index++) { const type = valueFromList(base[i], V4_ITEM_TYPES); if (!type) continue; const data = { id: v9ItemId(index), type, x: v9Uncoord(base[i + 1] || 0), y: v9Uncoord(base[i + 2] || 0) }; const extras = extraByIndex.get(index) || []; for (let p = 0; p < extras.length - 1; p += 2) { const key = V9_ITEM_EXTRA_KEYS[extras[p]]; if (!key) continue; data[key] = v9DecodeItemValue(key, extras[p + 1]); } out.push(data); } return out; } function v9EncodeAntValue(key, value, ctx) { if (value === undefined || value === null || value === "") return undefined; if (key === "homeId") return v9PackItemRef(value, ctx); if (key === "targetId") { const item = v9PackItemRef(value, ctx); if (item !== undefined) return item; const tarinai = v9PackTarinaiRef(value, ctx); return tarinai !== undefined ? -tarinai : undefined; } if (["x", "y", "foundingX", "foundingY"].includes(key)) return v9Coord(value || 0); if (["vx", "vy", "r", "hp", "maxHp", "age", "seed", "wanderAngle", "nextTurn", "returnTimer", "foundingDelayUntil"].includes(key)) return v9Num1(value || 0); return compactValue(value, 0, key); } function v9DecodeAntValue(key, value) { if (key === "homeId") return Number.isFinite(value) ? v9ItemId(value - 1) : ""; if (key === "targetId") { if (!Number.isFinite(value)) return ""; return value < 0 ? v9TarinaiId((-value) - 1) : v9ItemId(value - 1); } if (["x", "y", "foundingX", "foundingY"].includes(key)) return v9Uncoord(value || 0); if (["vx", "vy", "r", "hp", "maxHp", "age", "seed", "wanderAngle", "nextTurn", "returnTimer", "foundingDelayUntil"].includes(key)) return v9Unnum1(value || 0); return clonePlain(value); } function v9PackAnts(worldRef, ctx) { const rows = []; for (const ant of worldRef.ants || []) { if (!ant || ant.dead) continue; const data = snapshotEntity(worldRef, ant); const pairs = []; const defaults = { kind: "worker", x: 0, y: 0, vx: 0, vy: 0, r: 4, maxHp: 1, hp: 1, state: "wander", age: 0, seed: 0, wanderAngle: 0, nextTurn: 0, returnTimer: 0, foundingAttempts: 0 }; for (let i = 0; i < V9_ANT_KEYS.length; i++) { const key = V9_ANT_KEYS[i]; const encoded = v9EncodeAntValue(key, data[key], ctx); if (isEmptyCompactValue(encoded)) continue; const def = v9EncodeAntValue(key, defaults[key], ctx); if (approxSame(encoded, def, key)) continue; pairs.push(i, encoded); } rows.push(pairs); } return rows; } function v9UnpackAnts(rows) { if (!Array.isArray(rows)) return []; return rows.map((row, index) => { const data = { id: v9AntId(index) }; for (let i = 0; i < row.length - 1; i += 2) { const key = V9_ANT_KEYS[row[i]]; if (!key) continue; data[key] = v9DecodeAntValue(key, row[i + 1]); } return data; }); } function v9EncodeWorldValue(key, value) { if (value === undefined || value === null || value === "") return undefined; if (key === "fieldType") return codeOrValue(value, V6_FIELD_TYPES); if (key === "weather") return codeOrValue(value, V6_WEATHERS); if (key === "colonyMood") return codeOrValue(value, V5_MOODS); if (["w", "h", "time", "nextWeatherChange", "lastBirthAt", "lastColonyMoodDay"].includes(key)) return v9Num1(value || 0); return compactValue(value, 0, key); } function v9DecodeWorldValue(key, value) { if (key === "fieldType") return valueOrCode(value, V6_FIELD_TYPES); if (key === "weather") return valueOrCode(value, V6_WEATHERS); if (key === "colonyMood") return { id: valueOrCode(value, V5_MOODS) || "relaxed" }; if (["w", "h", "time", "nextWeatherChange", "lastBirthAt", "lastColonyMoodDay"].includes(key)) return v9Unnum1(value || 0); return clonePlain(value); } function v9PackWorld(worldRef) { const world = worldData(worldRef); world.colonyMood = worldRef?.colonyMood?.id || world.colonyMood?.id || "relaxed"; delete world.day; const defaults = { w: 1000, h: 720, time: 0, fieldType: "garden", weather: "sunny", deadCount: 0, liveIdSerial: 0, lastBirthAt: -999, maxGeneration: 1, colonyMood: "relaxed", lastColonyMoodDay: 0 }; return packSparseV5(world, V9_WORLD_KEYS, defaults, v9EncodeWorldValue); } function v9UnpackWorld(row) { const world = unpackSparseV5(row, V9_WORLD_KEYS, v9DecodeWorldValue); world.day = 1; if (!world.fieldType) world.fieldType = "garden"; if (!world.colonyMood) world.colonyMood = { id: "relaxed" }; return world; } function v9PackFrozen(entry = {}, ctx) { const data = clonePlain(entry.data || entry) || {}; delete data.relationships; delete data.records; delete data.recentChangeCauses; delete data.personalityDaily; delete data.mysteryDrugHistory; delete data.parentNames; const row = v9PackTarinai(null, data, ctx); const out = [v9Num1(entry.frozenAt || 0), v9Num1(entry.worldTime || 0), row]; return out; } function v9UnpackFrozen(entry = [], index = 0, liveCount = 0) { const data = v9UnpackTarinai(entry[2] || [], liveCount + index); data.id = `ft${index + 1}`; data.familyKey = `ff${index + 1}`; const freezeId = `fz${index + 1}`; return { freezeId, frozenAt: v9Unnum1(entry[0] || 0), worldTime: v9Unnum1(entry[1] || 0), name: data.name || "tarinai", familyKey: data.familyKey || "", liveId: data.id || "", liveToken: data.liveToken || 0, type: data.type || "smile", data, pendingLosses: [] }; } function createV9SnapshotArray(worldRef = global.world) { if (!worldRef) throw new Error("world is not ready"); const frozen = Array.isArray(worldRef.frozenTarinai) ? worldRef.frozenTarinai : []; const live = v9LiveLists(worldRef, frozen); const itemInfo = v9ItemList(worldRef); const ctx = { ...live, ...itemInfo }; return [ V9_SNAPSHOT_VERSION, v9PackWorld(worldRef), live.alive.map(t => v9PackTarinai(worldRef, t, ctx)), v9PackItems(worldRef, ctx), v9PackAnts(worldRef, ctx), frozen.map(entry => v9PackFrozen(entry, ctx)), ]; } function expandV9SnapshotArray(snapshot) { if (!Array.isArray(snapshot) || snapshot[0] !== V9_SNAPSHOT_VERSION) return snapshot; const world = v9UnpackWorld(snapshot[1] || []); const tarinaiRows = Array.isArray(snapshot[2]) ? snapshot[2] : []; world.frozenTarinai = (snapshot[5] || []).map((entry, index) => v9UnpackFrozen(entry, index, tarinaiRows.length)); return { version: SNAPSHOT_VERSION, app: "tarinai_colony_game", createdAt: Date.now(), summary: { day: 1, time: "", fieldType: world.fieldType || "garden", population: tarinaiRows.length, items: Array.isArray(snapshot[3]?.[0]) ? Math.floor(snapshot[3][0].length / 3) : 0 }, world, tarinai: tarinaiRows.map((row, index) => v9UnpackTarinai(row, index)), items: v9UnpackItems(snapshot[3] || []), ants: v9UnpackAnts(snapshot[4] || []), effects: [], logs: [], selectedRef: null, }; } class V9BinaryWriter { constructor() { this.bytes = []; } byte(v) { this.bytes.push(v & 255); } varUint(v) { let n = Math.max(0, Math.floor(Number(v) || 0)); while (n >= 128) { this.byte((n & 127) | 128); n = Math.floor(n / 128); } this.byte(n); } sint(v) { const n = Math.trunc(Number(v) || 0); this.varUint(n < 0 ? ((-n) * 2 - 1) : n * 2); } bytesRaw(bytes) { for (const b of bytes) this.byte(b); } value(value) { if (value === undefined || value === null) { this.byte(0); return; } if (value === false) { this.byte(1); return; } if (value === true) { this.byte(2); return; } if (typeof value === "number") { if (Number.isInteger(value)) { this.byte(value >= 0 ? 3 : 4); value >= 0 ? this.varUint(value) : this.sint(value); return; } this.byte(5); this.sint(Math.round(value * 1000)); return; } if (typeof value === "string") { const bytes = new TextEncoder().encode(value); this.byte(6); this.varUint(bytes.length); this.bytesRaw(bytes); return; } if (Array.isArray(value)) { this.byte(7); this.varUint(value.length); for (const v of value) this.value(v); return; } this.value(JSON.stringify(value)); } finish() { return new Uint8Array(this.bytes); } } class V9BinaryReader { constructor(bytes) { this.bytes = bytes || new Uint8Array(); this.i = 0; } byte() { return this.i < this.bytes.length ? this.bytes[this.i++] : 0; } varUint() { let shift = 0, out = 0, mul = 1; while (this.i < this.bytes.length) { const b = this.byte(); out += (b & 127) * mul; if (!(b & 128)) break; mul *= 128; shift += 7; if (shift > 42) break; } return out; } sint() { const u = this.varUint(); return u & 1 ? -((u + 1) / 2) : u / 2; } value() { const tag = this.byte(); if (tag === 0) return null; if (tag === 1) return false; if (tag === 2) return true; if (tag === 3) return this.varUint(); if (tag === 4) return this.sint(); if (tag === 5) return this.sint() / 1000; if (tag === 6) { const len = this.varUint(); const slice = this.bytes.slice(this.i, this.i + len); this.i += len; return new TextDecoder().decode(slice); } if (tag === 7) { const len = this.varUint(); const arr = []; for (let i = 0; i < len; i++) arr.push(this.value()); return arr; } return null; } } // v10 hash format: current-only compact packet. // It removes the V8 initial-state special case and does not use generic JSON/value arrays for common live data. // Game-play state is kept; observation history, elapsed day count, dead family nodes, and frozen memories are omitted. const V10_SNAPSHOT_VERSION = 10; const V10_NAME_CHARS = V9_NAME_CHARS; const V10_WORLD_KEYS = ["w", "h", "time", "fieldType", "weather", "nextWeatherChange", "deadCount", "lastBirthAt", "maxGeneration", "colonyMood"]; const V10_WORLD_DEFAULTS = { w: 1000, h: 720, time: 0, fieldType: "garden", weather: "sunny", nextWeatherChange: 0, deadCount: 0, lastBirthAt: -999, maxGeneration: 1, colonyMood: "relaxed" }; class V10Writer { constructor() { this.bytes = []; } byte(v) { this.bytes.push((Number(v) || 0) & 255); } u(v) { let n = Math.max(0, Math.floor(Number(v) || 0)); while (n >= 128) { this.byte((n % 128) | 128); n = Math.floor(n / 128); } this.byte(n); } z(v) { const n = Math.trunc(Number(v) || 0); this.u(n < 0 ? ((-n) * 2 - 1) : n * 2); } raw(bytes) { for (const b of bytes || []) this.byte(b); } str(value) { const bytes = new TextEncoder().encode(String(value ?? "")); this.u(bytes.length); this.raw(bytes); } val(value) { if (value === undefined || value === null) { this.byte(0); return; } if (value === false) { this.byte(1); return; } if (value === true) { this.byte(2); return; } if (typeof value === "number") { this.byte(3); this.z(Math.round(value)); return; } if (typeof value === "string") { this.byte(4); this.str(value); return; } if (Array.isArray(value)) { this.byte(5); this.u(value.length); for (const v of value) this.val(v); return; } this.byte(6); this.str(JSON.stringify(value)); } finish() { return new Uint8Array(this.bytes); } } class V10Reader { constructor(bytes) { this.bytes = bytes || new Uint8Array(); this.i = 0; } byte() { return this.i < this.bytes.length ? this.bytes[this.i++] : 0; } u() { let out = 0, mul = 1, guard = 0; while (this.i < this.bytes.length && guard++ < 8) { const b = this.byte(); out += (b & 127) * mul; if (!(b & 128)) break; mul *= 128; } return out; } z() { const u = this.u(); return u & 1 ? -((u + 1) / 2) : u / 2; } str() { const len = this.u(); const slice = this.bytes.slice(this.i, this.i + len); this.i += len; return new TextDecoder().decode(slice); } val() { const tag = this.byte(); if (tag === 0) return null; if (tag === 1) return false; if (tag === 2) return true; if (tag === 3) return this.z(); if (tag === 4) return this.str(); if (tag === 5) { const len = this.u(); const out = []; for (let i = 0; i < len; i++) out.push(this.val()); return out; } if (tag === 6) { try { return JSON.parse(this.str()); } catch (_) { return {}; } } return null; } } function v10PackNameInfo(name) { const s = String(name || "tarinai"); if (s === "tarinai") return [0]; if (s === "zunchi_slave") return [1]; const chars = Array.from(s); if (chars.length > 0 && chars.length <= 24 && chars.every(ch => V10_NAME_CHARS.includes(ch))) { let code = 0; let mul = 1; for (const ch of chars) { code += (V10_NAME_CHARS.indexOf(ch) & 3) * mul; mul *= 4; } return [2, chars.length, code]; } return [3, s]; } function v10WriteName(w, name) { const info = v10PackNameInfo(name); w.u(info[0]); if (info[0] === 2) { w.u(info[1]); w.u(info[2]); } else if (info[0] === 3) w.str(info[1]); } function v10ReadName(r) { const mode = r.u(); if (mode === 0) return "tarinai"; if (mode === 1) return "zunchi_slave"; if (mode === 2) { const len = r.u(); let code = r.u(); let out = ""; for (let i = 0; i < len; i++) { out += V10_NAME_CHARS[code % 4] || "\\u306f"; code = Math.floor(code / 4); } return out || "tarinai"; } if (mode === 3) return r.str() || "tarinai"; return "tarinai"; } function v10WriteAxis(w, obj, keys, defaults = null, scale = 100) { const packed = v9PackAxisDelta(obj, keys, defaults, scale); if (!packed) { w.u(0); return; } const mask = packed[0] || 0; w.u(mask); for (let i = 1; i < packed.length; i++) w.z(packed[i]); } function v10ReadAxis(r, keys, defaults = null, scale = 100) { const mask = r.u(); if (!mask) return defaults && typeof defaults === "object" ? clonePlain(defaults) || {} : {}; const row = [mask]; for (let i = 0; i < keys.length; i++) if (mask & (1 << i)) row.push(r.z()); return v9UnpackAxisDelta(row, keys, defaults, scale); } function v10WriteTarinai(w, t, worldRef, ctx) { const data = snapshotEntity(worldRef, t); data.parents = Array.isArray(data.parents) ? data.parents.filter(v => v9PackFamilyRef(v, ctx) !== undefined) : []; data.children = Array.isArray(data.children) ? data.children.filter(v => v9PackFamilyRef(v, ctx) !== undefined) : []; v10WriteName(w, data.name); w.u(v9Coord(data.x)); w.u(v9Coord(data.y)); w.u((codeFromList(data.need || "", V5_NEEDS) + 1) || 0); let numericMask = 0; const numericValues = []; for (let i = 0; i < V9_CORE_NUMERIC_KEYS.length; i++) { const key = V9_CORE_NUMERIC_KEYS[i]; const diff = v9Diff1(key, Number(data[key] || 0)); if (!diff) continue; numericMask |= (1 << i); numericValues.push(diff); } w.u(numericMask); for (const v of numericValues) w.z(v); v10WriteAxis(w, data.currentPersonality, PERSONALITY_ORDER, null, 100); v10WriteAxis(w, data.genetics, GENETIC_ORDER, { lifeSpanMul: 1, attackMul: 1, speedMul: 1, sizeMul: 1 }, 100); w.u(v9PackFlags(data)); const defaults = v7DefaultTarinaiData(); for (let i = 0; i < V9_EXTRA_TARINAI_KEYS.length; i++) { const key = V9_EXTRA_TARINAI_KEYS[i]; if (V9_BOOLEAN_FLAGS.includes(key)) continue; const encoded = v9EncodeTarinaiExtra(key, data[key], ctx); if (isEmptyCompactValue(encoded)) continue; const def = v9EncodeTarinaiExtra(key, defaults[key], ctx); if (approxSame(encoded, def, key)) continue; w.u(i + 1); w.val(encoded); } w.u(0); } function v10ReadTarinai(r, index = 0) { const data = { id: v9TarinaiId(index), familyKey: v9FamilyKey(index), liveToken: index + 1, generation: 1, state: "idle", type: "smile", parents: [], children: [], relationships: {}, itemEffectTimers: { laxative: 0, ammo: 0 } }; data.name = v10ReadName(r); data.x = v9Uncoord(r.u()); data.y = v9Uncoord(r.u()); const need = r.u(); data.need = need ? (valueFromList(need - 1, V5_NEEDS) || "") : ""; for (const key of V9_CORE_NUMERIC_KEYS) data[key] = V7_NUMERIC_BASE[key] ?? 0; const numericMask = r.u(); for (let i = 0; i < V9_CORE_NUMERIC_KEYS.length; i++) { if (!(numericMask & (1 << i))) continue; const key = V9_CORE_NUMERIC_KEYS[i]; data[key] = v9Undiff1(key, r.z()); } data.currentPersonality = v10ReadAxis(r, PERSONALITY_ORDER, null, 100); data.birthPersonality = clonePlain(data.currentPersonality); data.genetics = v10ReadAxis(r, GENETIC_ORDER, { lifeSpanMul: 1, attackMul: 1, speedMul: 1, sizeMul: 1 }, 100); v9ApplyFlags(data, r.u()); while (true) { const tag = r.u(); if (!tag) break; const key = V9_EXTRA_TARINAI_KEYS[tag - 1]; const value = r.val(); if (!key) continue; data[key] = v9DecodeTarinaiExtra(key, value); } if (data.isZunchiSlave || data.zunchiSlaveLocked) { data.isZunchiSlave = true; data.zunchiSlaveLocked = true; data.name = "zunchi_slave"; } data.dead = false; return data; } function v10WriteWorld(w, worldRef) { const world = worldData(worldRef); world.colonyMood = worldRef?.colonyMood?.id || world.colonyMood?.id || "relaxed"; delete world.day; let mask = 0; const values = []; for (let i = 0; i < V10_WORLD_KEYS.length; i++) { const key = V10_WORLD_KEYS[i]; const raw = key === "colonyMood" ? world.colonyMood : world[key]; const def = V10_WORLD_DEFAULTS[key]; let encoded; if (key === "fieldType") encoded = codeOrValue(raw, V6_FIELD_TYPES); else if (key === "weather") encoded = codeOrValue(raw, V6_WEATHERS); else if (key === "colonyMood") encoded = codeOrValue(raw, V5_MOODS); else if (["w", "h", "time", "nextWeatherChange", "lastBirthAt"].includes(key)) encoded = v9Num1((Number(raw) || 0) - (Number(def) || 0)); else encoded = Math.round(Number(raw ?? def) || 0) - Math.round(Number(def) || 0); if (!encoded) continue; mask |= (1 << i); values.push(encoded); } w.u(mask); for (const v of values) w.z(v); } function v10ReadWorld(r) { const out = { day: 1, fieldType: "garden", colonyMood: { id: "relaxed" } }; const mask = r.u(); for (let i = 0; i < V10_WORLD_KEYS.length; i++) { const key = V10_WORLD_KEYS[i]; const def = V10_WORLD_DEFAULTS[key]; if (!(mask & (1 << i))) { if (key !== "colonyMood") out[key] = def; continue; } const v = r.z(); if (key === "fieldType") out[key] = valueOrCode(v, V6_FIELD_TYPES) || "garden"; else if (key === "weather") out[key] = valueOrCode(v, V6_WEATHERS) || "sunny"; else if (key === "colonyMood") out[key] = { id: valueOrCode(v, V5_MOODS) || "relaxed" }; else if (["w", "h", "time", "nextWeatherChange", "lastBirthAt"].includes(key)) out[key] = v9Unnum1(v9Num1(def) + v); else out[key] = Math.round(Number(def) || 0) + v; } if (!out.colonyMood) out.colonyMood = { id: "relaxed" }; return out; } function v10WriteItems(w, worldRef, ctx) { w.u(ctx.items.length); for (let index = 0; index < ctx.items.length; index++) { const item = ctx.items[index]; const data = snapshotEntity(worldRef, item); const type = data.type || item.type || ""; w.u((codeFromList(type, V4_ITEM_TYPES) + 1) || 0); w.u(v9Coord(data.x)); w.u(v9Coord(data.y)); const defaults = itemDefaultsFor(type); for (let i = 0; i < V9_ITEM_EXTRA_KEYS.length; i++) { const key = V9_ITEM_EXTRA_KEYS[i]; if (key === "antWorkers" || key === "workerSprite") continue; const encoded = v9EncodeItemValue(key, data[key], ctx); if (isEmptyCompactValue(encoded)) continue; const def = v9EncodeItemValue(key, defaults[key], ctx); if (approxSame(encoded, def, key)) continue; w.u(i + 1); w.val(encoded); } w.u(0); } } function v10ReadItems(r) { const count = r.u(); const out = []; for (let index = 0; index < count; index++) { const type = valueFromList(r.u() - 1, V4_ITEM_TYPES); const data = { id: v9ItemId(index), type, x: v9Uncoord(r.u()), y: v9Uncoord(r.u()) }; while (true) { const tag = r.u(); if (!tag) break; const key = V9_ITEM_EXTRA_KEYS[tag - 1]; const value = r.val(); if (!key) continue; data[key] = v9DecodeItemValue(key, value); } if (type) out.push(data); } return out; } function v10WriteAnts(w, worldRef, ctx) { const ants = (worldRef.ants || []).filter(a => a && !a.dead); w.u(ants.length); for (const ant of ants) { const data = snapshotEntity(worldRef, ant); const defaults = { kind: "worker", x: 0, y: 0, vx: 0, vy: 0, r: 4, maxHp: 1, hp: 1, state: "wander", age: 0, seed: 0, wanderAngle: 0, nextTurn: 0, returnTimer: 0, foundingAttempts: 0 }; for (let i = 0; i < V9_ANT_KEYS.length; i++) { const key = V9_ANT_KEYS[i]; const encoded = v9EncodeAntValue(key, data[key], ctx); if (isEmptyCompactValue(encoded)) continue; const def = v9EncodeAntValue(key, defaults[key], ctx); if (approxSame(encoded, def, key)) continue; w.u(i + 1); w.val(encoded); } w.u(0); } } function v10ReadAnts(r) { const count = r.u(); const out = []; for (let index = 0; index < count; index++) { const data = { id: v9AntId(index) }; while (true) { const tag = r.u(); if (!tag) break; const key = V9_ANT_KEYS[tag - 1]; const value = r.val(); if (!key) continue; data[key] = v9DecodeAntValue(key, value); } out.push(data); } return out; } function v10WriteFrozen(w, frozen, ctx) { w.u((frozen || []).length); for (const entry of frozen || []) { w.z(v9Num1(entry?.frozenAt || 0)); w.z(v9Num1(entry?.worldTime || 0)); const data = clonePlain(entry?.data || entry) || {}; delete data.relationships; delete data.records; delete data.recentChangeCauses; delete data.personalityDaily; delete data.mysteryDrugHistory; delete data.parentNames; v10WriteTarinai(w, data, null, ctx); } } function v10ReadFrozen(r, liveCount = 0) { const count = r.u(); const out = []; for (let index = 0; index < count; index++) { const frozenAt = v9Unnum1(r.z()); const worldTime = v9Unnum1(r.z()); const data = v10ReadTarinai(r, liveCount + index); data.id = `ft${index + 1}`; data.familyKey = `ff${index + 1}`; out.push({ freezeId: `fz${index + 1}`, frozenAt, worldTime, name: data.name || "tarinai", familyKey: data.familyKey || "", liveId: data.id || "", liveToken: data.liveToken || 0, type: data.type || "smile", data, pendingLosses: [] }); } return out; } function createV10SnapshotBytes(worldRef = global.world) { if (!worldRef) throw new Error("world is not ready"); const frozen = Array.isArray(worldRef.frozenTarinai) ? worldRef.frozenTarinai : []; const live = v9LiveLists(worldRef, frozen); const itemInfo = v9ItemList(worldRef); const ctx = { ...live, ...itemInfo }; const w = new V10Writer(); w.byte(0x54); w.byte(0x0a); v10WriteWorld(w, worldRef); w.u(live.alive.length); for (const t of live.alive) v10WriteTarinai(w, t, worldRef, ctx); v10WriteItems(w, worldRef, ctx); v10WriteAnts(w, worldRef, ctx); v10WriteFrozen(w, frozen, ctx); return w.finish(); } function decodeV10SnapshotBytes(bytes) { const r = new V10Reader(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || [])); if (r.byte() !== 0x54 || r.byte() !== 0x0a) throw new Error("invalid V10 save packet"); const world = v10ReadWorld(r); const liveCount = r.u(); const tarinai = []; for (let i = 0; i < liveCount; i++) tarinai.push(v10ReadTarinai(r, i)); const items = v10ReadItems(r); const ants = v10ReadAnts(r); world.frozenTarinai = v10ReadFrozen(r, liveCount); return { version: SNAPSHOT_VERSION, app: "tarinai_colony_game", createdAt: Date.now(), summary: { day: 1, time: "", fieldType: world.fieldType || "garden", population: tarinai.length, items: items.length }, world, tarinai, items, ants, effects: [], logs: [], selectedRef: null, }; } // v11 hash format: current-only wrapper. Mode 0 is a semantic reset/delta // packet for initial and modest early-game saves; mode 1 embeds a V10 packet // so complex worlds still preserve the full compact simulation state. const V11_SNAPSHOT_VERSION = 11; const V11_SEMANTIC_ITEM_TYPES = [ "sweet", "love_mochi", "fight_mochi", "water_bowl", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", "zunchi", "genkotsu", "nest_box", "ant_nest", "ball", "signboard", "pushpin", "oshibyo", "duplicator", "cannon", "fence_v", "fence_h", "water" ]; const V11_BASELINE_ITEM_TYPES = new Set(["grass", "stone", "bed", "trace", "splat", "ant_corpse"]); const V11_HAZARD_ITEM_TYPES = new Set(["firecracker"]); const V11_TIMER_TARINAI_KEYS = ["loveMochiTimer", "fightMochiTimer", "explosionDiseaseTimer", "sunbathTimer", "sunbathCooldown", "fallTimer", "fearTimer", "birthRitualTimer", "intimidateTimer", "intimidatedTimer"]; const V11_COMPLEX_STATES = new Set(["fight", "panic", "birth_ritual", "intimidate", "hurt", "defeated", "stuck"]); const V11_MAX_SEMANTIC_OBJECTS = 16; const V11_MAX_SIGN_TEXT = 40; function v11ExpectedPopulation(fieldType) { const field = (typeof FIELD_TYPES !== "undefined" ? FIELD_TYPES?.[fieldType || "garden"] : null) || null; return field?.population ?? (typeof CONFIG !== "undefined" ? CONFIG.initialPopulation : 18); } function v11SemanticTarinaiBlocked(t) { if (!t || t.dead) return false; if (V11_COMPLEX_STATES.has(t.state)) return true; if (t.isZunchiSlave || t.zunchiSlaveLocked || t.zunchiDisease || t.sleepDisease || t.explosionDisease || t.fightDisease) return true; if (t.stuckPushpinId || t.insideNestBoxId || (t.oshiriByoZunchiStock || 0) > 0) return true; if (Object.keys(t.relationships || {}).length) return true; if (t.itemEffectTimers && Object.values(t.itemEffectTimers).some(v => (Number(v) || 0) > 0.2)) return true; return V11_TIMER_TARINAI_KEYS.some(key => (Number(t[key]) || 0) > 0.2); } function v11SemanticItemData(worldRef) { const out = []; for (const it of worldRef?.items || []) { if (!it || it.dead || V11_BASELINE_ITEM_TYPES.has(it.type)) continue; if (V11_HAZARD_ITEM_TYPES.has(it.type)) return null; if (!V11_SEMANTIC_ITEM_TYPES.includes(it.type)) return null; if ((it.fuseTimer || 0) > 0 || (it.dropTimer || 0) > 0 || it.cannonProjectile || it.projectileSourceId) return null; const text = it.type === "signboard" ? String(it.text || "") : ""; if (text.length > V11_MAX_SIGN_TEXT) return null; out.push({ type: it.type, x: Number(it.x) || 0, y: Number(it.y) || 0, text }); if (out.length > V11_MAX_SEMANTIC_OBJECTS) return null; } return out; } function v11SemanticData(worldRef) { if (!worldRef) return null; const alive = (worldRef.tarinai || []).filter(t => t && !t.dead); if ((worldRef.effects || []).some(ef => ef && !ef.dead)) return null; if ((worldRef.ants || []).some(a => a && !a.dead)) return null; if (alive.some(v11SemanticTarinaiBlocked)) return null; const objects = v11SemanticItemData(worldRef); if (!objects) return null; const indexByFamily = new Map(); alive.forEach((t, index) => indexByFamily.set(t.familyKey || t.id || String(index), index)); const people = []; for (const t of alive) { const parents = []; for (const ref of t.parents || []) { if (!indexByFamily.has(ref)) return null; parents.push(indexByFamily.get(ref)); } const name = t.name || "tarinai"; people.push({ name, type: t.type || "smile", generation: Math.max(1, Math.round(t.generation || 1)), parents }); } const frozen = []; for (const entry of worldRef.frozenTarinai || []) { const data = entry?.data || entry || {}; if (data.isZunchiSlave || data.zunchiSlaveLocked || data.zunchiDisease || data.sleepDisease || data.explosionDisease || data.fightDisease) return null; const name = data.name || entry?.name || "tarinai"; frozen.push({ name, type: data.type || entry?.type || "smile", generation: Math.max(1, Math.round(data.generation || 1)) }); } return { fieldType: worldRef.fieldType || "garden", deadCount: Math.max(0, Math.round(worldRef.deadCount || 0)), people, objects, frozen, }; } function v11WriteSemantic(w, data) { w.u((codeFromList(data.fieldType || "garden", V6_FIELD_TYPES) + 1) || 0); w.u(data.deadCount || 0); w.u(data.people.length); for (const p of data.people) { v10WriteName(w, p.name); w.u((codeFromList(p.type || "smile", V4_SPRITE_TYPES) + 1) || 0); w.u(Math.max(1, p.generation || 1)); w.u(p.parents.length); for (const parent of p.parents) w.u(parent + 1); } w.u(data.objects.length); for (const obj of data.objects) { w.u((codeFromList(obj.type, V4_ITEM_TYPES) + 1) || 0); w.u(v9Coord(obj.x)); w.u(v9Coord(obj.y)); if (obj.type === "signboard") w.str(obj.text || ""); } w.u(data.frozen.length); for (const f of data.frozen) { v10WriteName(w, f.name); w.u((codeFromList(f.type || "smile", V4_SPRITE_TYPES) + 1) || 0); w.u(Math.max(1, f.generation || 1)); } } function v11ReadSemantic(r) { const fieldType = valueFromList(r.u() - 1, V6_FIELD_TYPES) || "garden"; const deadCount = r.u(); const people = []; const peopleCount = r.u(); for (let i = 0; i < peopleCount; i++) { const name = v10ReadName(r); const type = valueFromList(r.u() - 1, V4_SPRITE_TYPES) || "smile"; const generation = Math.max(1, r.u() || 1); const parents = []; const parentCount = r.u(); for (let p = 0; p < parentCount; p++) parents.push(r.u() - 1); people.push({ name, type, generation, parents }); } const objects = []; const objectCount = r.u(); for (let i = 0; i < objectCount; i++) { const type = valueFromList(r.u() - 1, V4_ITEM_TYPES) || ""; const x = v9Uncoord(r.u()); const y = v9Uncoord(r.u()); const text = type === "signboard" ? r.str() : ""; if (type) objects.push({ type, x, y, text }); } const frozen = []; const frozenCount = r.u(); for (let i = 0; i < frozenCount; i++) { frozen.push({ name: v10ReadName(r), type: valueFromList(r.u() - 1, V4_SPRITE_TYPES) || "smile", generation: Math.max(1, r.u() || 1), }); } return { __tarinaiV11Semantic: true, version: V11_SNAPSHOT_VERSION, fieldType, deadCount, people, objects, frozen }; } function createV11SnapshotBytes(worldRef = global.world) { const full = createV10SnapshotBytes(worldRef); const semantic = v11SemanticData(worldRef); if (semantic) { const w = new V10Writer(); w.byte(0x54); w.byte(0x0b); w.u(0); v11WriteSemantic(w, semantic); const semanticBytes = w.finish(); if (semanticBytes.length < full.length) return semanticBytes; } const w = new V10Writer(); w.byte(0x54); w.byte(0x0b); w.u(1); w.u(full.length); w.raw(full); return w.finish(); } function decodeV11SnapshotBytes(bytes) { const r = new V10Reader(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || [])); if (r.byte() !== 0x54 || r.byte() !== 0x0b) throw new Error("invalid V11 save packet"); const mode = r.u(); if (mode === 0) return v11ReadSemantic(r); if (mode === 1) { const len = r.u(); return decodeV10SnapshotBytes(r.bytes.slice(r.i, r.i + len)); } throw new Error("unsupported V11 save mode"); } function restoreV11SemanticSnapshot(snapshot, worldRef = global.world) { if (!snapshot?.__tarinaiV11Semantic) return false; const fieldType = snapshot.fieldType || "garden"; if (typeof applyFieldLayout === "function") applyFieldLayout(fieldType); if (typeof worldRef.reset === "function") worldRef.reset(null, fieldType); const people = Array.isArray(snapshot.people) ? snapshot.people : []; const baseline = (worldRef.tarinai || []).filter(t => t && !t.dead); while (baseline.length > people.length) { const t = baseline.pop(); if (typeof worldRef.releaseTarinaiLiveId === "function") worldRef.releaseTarinaiLiveId(t); const index = worldRef.tarinai.indexOf(t); if (index >= 0) worldRef.tarinai.splice(index, 1); } while (baseline.length < people.length) { const p = people[baseline.length] || {}; baseline.push(worldRef.addTarinai({ name: p.name || "tarinai", type: p.type || "smile", generation: Math.max(1, p.generation || 1), x: rand(80, worldRef.w - 80), y: rand(80, worldRef.h - 80), })); } for (let i = 0; i < Math.min(people.length, baseline.length); i++) { const p = people[i] || {}; const t = baseline[i]; t.name = p.name || t.name; t.type = p.type || t.type || "smile"; t.generation = Math.max(1, p.generation || 1); t.parents = []; t.children = []; t.relationships = {}; t.hasPaired = false; t.dead = false; } for (let i = 0; i < Math.min(people.length, baseline.length); i++) { const t = baseline[i]; const refs = []; for (const parentIndex of people[i]?.parents || []) { const parent = baseline[parentIndex]; if (!parent) continue; refs.push(parent.familyKey); parent.children = Array.from(new Set([...(parent.children || []), t.familyKey])); } t.parents = refs; if (refs.length) t.hasPaired = true; } worldRef.deadCount = Math.max(0, Math.round(snapshot.deadCount || 0)); worldRef.items = (worldRef.items || []).filter(it => it && !it.dead); for (const obj of snapshot.objects || []) { if (!obj?.type) continue; const item = new Item(obj.type, Number(obj.x) || 0, Number(obj.y) || 0); if (obj.type === "signboard") item.text = String(obj.text || ""); worldRef.items.push(item); } worldRef.frozenTarinai = (snapshot.frozen || []).map((f, index) => { const data = { id: `ft${index + 1}`, familyKey: `ff${index + 1}`, liveToken: baseline.length + index + 1, name: f.name || "tarinai", type: f.type || "smile", generation: Math.max(1, f.generation || 1), parents: [], children: [], relationships: {}, itemEffectTimers: { laxative: 0, ammo: 0 }, }; return { freezeId: `fz${index + 1}`, frozenAt: 0, worldTime: 0, name: data.name, familyKey: data.familyKey, liveId: data.id, liveToken: data.liveToken, type: data.type, data, pendingLosses: [] }; }); worldRef.logs = []; worldRef.selected = null; worldRef.family = {}; for (const t of baseline) if ((t.parents || []).length || (t.children || []).length) worldRef.recordFamily?.(t); worldRef.familyTreeDirty = true; worldRef.drawListDirty = true; worldRef.terrainDirty = true; if (typeof worldRef.updateItemCounts === "function") worldRef.updateItemCounts(); if (typeof worldRef.updateEffectCounts === "function") worldRef.updateEffectCounts(); if (typeof worldRef.rebuildSpatial === "function") worldRef.rebuildSpatial(true); if (typeof worldRef.markTerrainDirty === "function") worldRef.markTerrainDirty("load"); if (typeof renderLog === "function") renderLog([]); 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; } function createV9SnapshotBytes(worldRef = global.world) { const writer = new V9BinaryWriter(); writer.byte(0x54); // T writer.byte(0x09); writer.value(createV9SnapshotArray(worldRef)); return writer.finish(); } function decodeV9SnapshotBytes(bytes) { const reader = new V9BinaryReader(bytes); if (reader.byte() !== 0x54 || reader.byte() !== 0x09) throw new Error("invalid V9 save packet"); const snapshot = reader.value(); if (!Array.isArray(snapshot) || snapshot[0] !== V9_SNAPSHOT_VERSION) throw new Error("invalid V9 save packet"); return expandV9SnapshotArray(snapshot); } function createCompactSnapshot(worldRef = global.world) { return { __tarinaiCompactBytes: true, version: V11_SNAPSHOT_VERSION, bytes: createV11SnapshotBytes(worldRef) }; } function expandCompactSnapshot(snapshot) { if (snapshot && snapshot.__tarinaiCompactBytes && snapshot.bytes) return decodeV11SnapshotBytes(snapshot.bytes); if (snapshot instanceof Uint8Array) return decodeV11SnapshotBytes(snapshot); return snapshot; } function createSnapshot(worldRef = global.world) { if (!worldRef) throw new Error("world is not ready"); const selectedRef = refFor(worldRef, worldRef.selected); return { version: SNAPSHOT_VERSION, app: "tarinai_colony_game", createdAt: Date.now(), summary: { day: worldRef.day || 1, time: worldRef.clockString ? worldRef.clockString() : "", fieldType: worldRef.fieldType || "garden", population: (worldRef.tarinai || []).filter(t => t && !t.dead).length, items: (worldRef.items || []).filter(it => it && !it.dead).length, }, world: worldData(worldRef), tarinai: (worldRef.tarinai || []).filter(Boolean).map(t => snapshotEntity(worldRef, t)), items: (worldRef.items || []).filter(Boolean).map(it => snapshotEntity(worldRef, it)), ants: (worldRef.ants || []).filter(Boolean).map(a => snapshotEntity(worldRef, a)), effects: (worldRef.effects || []).filter(Boolean).map(ef => snapshotEntity(worldRef, ef)), logs: clonePlain((worldRef.logs || []).slice(0, 100)) || [], selectedRef, }; } function restoreTarinaiData(worldRef, data, opts = {}) { const TarinaiClass = global.Tarinai || (typeof Tarinai !== "undefined" ? Tarinai : null); if (!TarinaiClass) throw new Error("Tarinai is not available"); const t = new TarinaiClass(worldRef, data || {}); applyOwnData(t, data || {}, new Set(["world", "target", "panicTarget", "targetRef"])); t.world = worldRef; if (opts.clearTransient !== false) { t.target = null; t.panicTarget = null; t.sleeping = false; t.insideNestBoxId = ""; } if (typeof t.refreshEffectiveSize === "function") t.refreshEffectiveSize(); return t; } function restoreSnapshot(snapshot, worldRef = global.world) { if (!snapshot || typeof snapshot !== "object") throw new Error("invalid save data"); if (!worldRef) throw new Error("world is not ready"); if (snapshot && snapshot.__tarinaiCompactBytes && snapshot.bytes) return restoreSnapshot(expandCompactSnapshot(snapshot), worldRef); if (snapshot instanceof Uint8Array) return restoreSnapshot(expandCompactSnapshot(snapshot), worldRef); if (snapshot.__tarinaiV11Semantic) return restoreV11SemanticSnapshot(snapshot, worldRef); const data = snapshot.version ? snapshot : JSON.parse(JSON.stringify(snapshot)); const fieldType = data.world?.fieldType || "garden"; if (typeof applyFieldLayout === "function") applyFieldLayout(fieldType); if (typeof worldRef.reset === "function") worldRef.reset(0, fieldType); worldRef.tarinai = []; worldRef.items = []; worldRef.ants = []; worldRef.effects = []; worldRef.logs = Array.isArray(data.logs) ? clonePlain(data.logs) || [] : []; worldRef.selected = null; applyOwnData(worldRef, data.world || {}, new Set(["tarinai", "items", "ants", "effects", "logs", "selected", "liveTarinai", "events", "spatial"])); worldRef.events = global.TarinaiEvents || null; worldRef.liveTarinai = new Map(); worldRef.liveIdFree = []; const items = Array.isArray(data.items) ? data.items : []; for (const saved of items) { if (!saved || !saved.type) continue; const item = new Item(saved.type, Number(saved.x) || 0, Number(saved.y) || 0); applyOwnData(item, saved, new Set(["target", "targetRef", "world"])); if (item.isStructure && global.TarinaiNeedSystem?.hydrateStructure) global.TarinaiNeedSystem.hydrateStructure(item); worldRef.items.push(item); } const tarinaiList = Array.isArray(data.tarinai) ? data.tarinai : []; for (const saved of tarinaiList) { if (!saved) continue; const t = restoreTarinaiData(worldRef, saved, { clearTransient: false }); if (!t.id && typeof worldRef.assignTarinaiLiveId === "function") worldRef.assignTarinaiLiveId(t); worldRef.tarinai.push(t); if (t.id) worldRef.liveTarinai.set(t.id, { token: t.liveToken || 0, target: t }); } const ants = Array.isArray(data.ants) ? data.ants : []; for (const saved of ants) { if (!saved) continue; const ant = new AntActor(worldRef, saved); applyOwnData(ant, saved, new Set(["world", "targetRef"])); ant.world = worldRef; worldRef.ants.push(ant); } const effects = Array.isArray(data.effects) ? data.effects : []; for (const saved of effects) { if (!saved || !saved.type) continue; const ef = new Effect(saved.type, Number(saved.x) || 0, Number(saved.y) || 0, saved); applyOwnData(ef, saved, new Set(["world", "target", "targetRef"])); worldRef.effects.push(ef); } const itemById = new Map(worldRef.items.map(it => [it.id, it])); const antById = new Map(worldRef.ants.map(a => [a.id, a])); const tarinaiById = new Map(worldRef.tarinai.map(t => [t.id, t])); const resolve = (ref) => { if (!ref) return null; if (ref.kind === "item") return itemById.get(ref.id) || null; if (ref.kind === "ant") return antById.get(ref.id) || null; if (ref.kind === "tarinai") return tarinaiById.get(ref.id) || worldRef.tarinai.find(t => t.familyKey === ref.familyKey) || null; return resolveRef(worldRef, ref); }; for (const [i, saved] of tarinaiList.entries()) { const t = worldRef.tarinai[i]; if (!t) continue; t.target = resolve(saved.__targetRef); t.panicTarget = resolve(saved.__panicTargetRef); } for (const [i, saved] of ants.entries()) { const ant = worldRef.ants[i]; if (!ant) continue; ant.targetRef = resolve(saved.__targetRefObject); } worldRef.selected = resolve(data.selectedRef); worldRef.family = clonePlain(data.world?.family) || worldRef.family || {}; worldRef.relationNotices = clonePlain(data.world?.relationNotices) || {}; worldRef.resolvedFightIds = clonePlain(data.world?.resolvedFightIds) || {}; worldRef.eventCounters = clonePlain(data.world?.eventCounters) || {}; const savedMood = data.world?.colonyMood; worldRef.colonyMood = savedMood?.id && typeof worldRef.colonyMoodDefinition === "function" ? worldRef.colonyMoodDefinition(savedMood.id) : (clonePlain(savedMood) || worldRef.colonyMoodDefinition?.("relaxed") || { id: "relaxed", label: "relaxed", effects: { personality: {} } }); worldRef.familyTreeDirty = true; worldRef.drawListDirty = true; worldRef.terrainDirty = true; if (typeof worldRef.updateItemCounts === "function") worldRef.updateItemCounts(); if (typeof worldRef.updateEffectCounts === "function") worldRef.updateEffectCounts(); if (typeof worldRef.rebuildSpatial === "function") worldRef.rebuildSpatial(true); if (typeof worldRef.markTerrainDirty === "function") worldRef.markTerrainDirty("load"); if (typeof worldRef.clampCamera === "function") worldRef.clampCamera(); if (typeof renderLog === "function") renderLog(worldRef.logs || []); if (typeof resetArchiveRenderState === "function") resetArchiveRenderState(); if (typeof renderArchive === "function") renderArchive(); if (typeof renderSelected === "function") renderSelected(); if (typeof renderStats === "function") renderStats(); if (typeof render === "function") render(); if (typeof syncTopButtons === "function") syncTopButtons(); global.TarinaiFreezeSystem?.afterSnapshotRestored?.(worldRef); return true; } global.TarinaiSnapshot = { version: SNAPSHOT_VERSION, clonePlain, copyOwnData, applyOwnData, refFor, resolveRef, worldData, snapshotEntity, createSnapshot, createCompactSnapshot, expandCompactSnapshot, createV4Snapshot, expandV4Snapshot, createV5Snapshot, expandV5Snapshot, createV6Snapshot, expandV6Snapshot, createV7Snapshot, expandV7Snapshot, createV8Snapshot, expandV8Snapshot, createV10SnapshotBytes, decodeV10SnapshotBytes, createV11SnapshotBytes, decodeV11SnapshotBytes, createV9SnapshotBytes, decodeV9SnapshotBytes, createV9SnapshotArray, expandV9SnapshotArray, restoreSnapshot, restoreTarinaiData, }; })(typeof window !== "undefined" ? window : globalThis);