tarinai/js/snapshot_system.js
2026-06-23 11:48:11 +09:00

3040 lines
141 KiB
JavaScript

"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);