555 lines
28 KiB
JavaScript
555 lines
28 KiB
JavaScript
"use strict";
|
|
|
|
(function (global) {
|
|
const SaveSchema = global.TarinaiSaveSchema;
|
|
if (!SaveSchema) throw new Error("TarinaiSaveSchema is not available for snapshot_system.js");
|
|
const SNAPSHOT_VERSION = SaveSchema.SNAPSHOT_VERSION;
|
|
const NEED_KEYS = ["food", "sleep", "health", "safety", "social", "fulfill"];
|
|
const DEFAULT_SKIP = new Set(["world", "target", "panicTarget", "targetRef", "targetEntity", "source"]);
|
|
const {
|
|
FIELD_IDS,
|
|
GROUND_IDS,
|
|
WEATHER_IDS,
|
|
MOOD_IDS,
|
|
canonicalMoodId,
|
|
moodValue,
|
|
STATE_IDS,
|
|
POWER_MODE_IDS,
|
|
SIZE_MODE_IDS,
|
|
LIFE_MODE_IDS,
|
|
PIN_STATE_IDS,
|
|
STAGE_IDS,
|
|
ITEM_TYPE_IDS,
|
|
itemTypeIndex,
|
|
enumIndex,
|
|
enumValue,
|
|
} = SaveSchema;
|
|
function q(value, scale = 1, fallback = 0) {
|
|
const n = Number(value);
|
|
return Number.isFinite(n) ? Math.round(n * scale) : fallback;
|
|
}
|
|
function u(value, scale = 1, fallback = 0) {
|
|
const n = Number(value);
|
|
return Number.isFinite(n) ? n / scale : fallback;
|
|
}
|
|
function mortonKeyXY(x = 0, y = 0) {
|
|
const ix = Math.max(0, Math.min(65535, Math.round(Number(x) || 0)));
|
|
const iy = Math.max(0, Math.min(65535, Math.round(Number(y) || 0)));
|
|
let out = 0;
|
|
for (let i = 0; i < 16; i++) out += (((ix >> i) & 1) * (2 ** (2 * i))) + (((iy >> i) & 1) * (2 ** (2 * i + 1)));
|
|
return out;
|
|
}
|
|
function isPlainObject(value) {
|
|
if (!value || typeof value !== "object") return false;
|
|
const proto = Object.getPrototypeOf(value);
|
|
return proto === Object.prototype || proto === null;
|
|
}
|
|
function clonePlain(value, depth = 0) {
|
|
if (value == null) return value;
|
|
const type = typeof value;
|
|
if (type === "number") return Number.isFinite(value) ? value : 0;
|
|
if (type === "string" || type === "boolean") return value;
|
|
if (type === "function" || type === "symbol" || type === "undefined") return undefined;
|
|
if (depth > 8) return undefined;
|
|
if (Array.isArray(value)) {
|
|
const out = [];
|
|
for (const v of value) {
|
|
const c = clonePlain(v, depth + 1);
|
|
if (c !== undefined) out.push(c);
|
|
}
|
|
return out;
|
|
}
|
|
if (!isPlainObject(value)) return undefined;
|
|
const out = {};
|
|
for (const [key, v] of Object.entries(value)) {
|
|
if (DEFAULT_SKIP.has(key)) continue;
|
|
const c = clonePlain(v, depth + 1);
|
|
if (c !== undefined) out[key] = c;
|
|
}
|
|
return out;
|
|
}
|
|
function needsToArray(needs = {}) {
|
|
return NEED_KEYS.map(k => q(needs[k], 1));
|
|
}
|
|
function arrayToNeeds(arr = []) {
|
|
const out = global.createDefaultNeeds();
|
|
NEED_KEYS.forEach((k, i) => { out[k] = u(arr[i], 1); });
|
|
return out;
|
|
}
|
|
function personalityToArray(p = {}) {
|
|
return ["aggression", "openness", "sociability", "neuroticism"].map(k => q((Number(p?.[k]) || 0) + 1, 100, 100));
|
|
}
|
|
function arrayToPersonality(arr = []) {
|
|
const keys = ["aggression", "openness", "sociability", "neuroticism"];
|
|
const out = {};
|
|
keys.forEach((k, i) => { out[k] = u(arr[i], 100, 1) - 1; });
|
|
return out;
|
|
}
|
|
function geneticsToArray(g = {}) {
|
|
const normalized = global.normalizeGenetics(g, "");
|
|
return [
|
|
q(Number(normalized?.lifeSpanMul) || 1, 100, 100),
|
|
q(Number(normalized?.attackMul) || 1, 100, 100),
|
|
q(Number(normalized?.speedMul) || 1, 100, 100),
|
|
q(Number(normalized?.sizeMul) || 1, 100, 100),
|
|
q(Number(normalized?.temperatureOffset) || 0, 10, 0),
|
|
];
|
|
}
|
|
function arrayToGenetics(arr = []) {
|
|
const out = {
|
|
lifeSpanMul: u(arr[0], 100, 1),
|
|
attackMul: u(arr[1], 100, 1),
|
|
speedMul: u(arr[2], 100, 1),
|
|
sizeMul: u(arr[3], 100, 1),
|
|
temperatureOffset: u(arr[4], 10, 0),
|
|
};
|
|
return global.normalizeGenetics(out, "");
|
|
}
|
|
function flagPack(t = {}) {
|
|
let flags = 0;
|
|
if (t.favorite) flags |= 1 << 0;
|
|
if (t.zunchiDisease) flags |= 1 << 1;
|
|
if (t.sleepDisease) flags |= 1 << 2;
|
|
if (t.explosionDisease) flags |= 1 << 3;
|
|
if (t.fightDisease) flags |= 1 << 4;
|
|
if (t.isZunchiSlave) flags |= 1 << 5;
|
|
if (t.zunchiSlaveLocked) flags |= 1 << 6;
|
|
if (t.birthRitualLeader) flags |= 1 << 7;
|
|
if (t.hasPaired) flags |= 1 << 8;
|
|
if (t.isTarinaiChampion) flags |= 1 << 9;
|
|
return flags;
|
|
}
|
|
function flagApply(t, flags = 0) {
|
|
t.favorite = !!(flags & (1 << 0));
|
|
t.zunchiDisease = !!(flags & (1 << 1));
|
|
t.sleepDisease = !!(flags & (1 << 2));
|
|
t.explosionDisease = !!(flags & (1 << 3));
|
|
t.fightDisease = !!(flags & (1 << 4));
|
|
t.isZunchiSlave = !!(flags & (1 << 5));
|
|
t.zunchiSlaveLocked = !!(flags & (1 << 6));
|
|
t.birthRitualLeader = !!(flags & (1 << 7));
|
|
t.hasPaired = !!(flags & (1 << 8));
|
|
t.isTarinaiChampion = !!(flags & (1 << 9));
|
|
if (t.isZunchiSlave) t.zunchiSlaveLocked = true;
|
|
}
|
|
function compactWorld(worldRef) {
|
|
const config = typeof CONFIG !== "undefined" ? CONFIG : (global.CONFIG || {});
|
|
const dayLength = Math.max(1, Number(config.dayLength || 120));
|
|
const worldTick10 = q((Math.max(1, worldRef?.day || 1) - 1) * dayLength + (Number(worldRef?.time) || 0), 10);
|
|
return [
|
|
enumIndex(FIELD_IDS, worldRef?.fieldType || "garden"),
|
|
enumIndex(GROUND_IDS, worldRef?.groundType || "soil"),
|
|
enumIndex(MOOD_IDS, canonicalMoodId?.(worldRef?.colonyMood?.id || worldRef?.colonyMood || "relaxed") || "relaxed"),
|
|
worldTick10,
|
|
enumIndex(WEATHER_IDS, worldRef?.weather || "sunny"),
|
|
q(worldRef?.lastBirthAt, 10, -9990),
|
|
q(worldRef?.maxGeneration, 1, 1),
|
|
q(worldRef?.deadCount, 1, 0),
|
|
worldRef?.worldSeed || global.TarinaiSeedFactory.ensureWorldSeed(worldRef) || "",
|
|
q(worldRef?.birthSerial, 1, 0),
|
|
q(worldRef?.tarinaiPopulationLimit, 1, 0),
|
|
q(worldRef?.objectLimit, 1, 0),
|
|
];
|
|
}
|
|
function applyCompactWorld(worldRef, arr = []) {
|
|
const config = typeof CONFIG !== "undefined" ? CONFIG : (global.CONFIG || {});
|
|
const dayLength = Math.max(1, Number(config.dayLength || 120));
|
|
const totalTime = u(arr[3], 10, 0);
|
|
worldRef.fieldType = enumValue(FIELD_IDS, arr[0], "garden");
|
|
worldRef.weather = enumValue(WEATHER_IDS, arr[4], "sunny");
|
|
worldRef.worldSeed = String(arr[8] || "") || global.TarinaiSeedFactory.createWorldSeed() || `w${Date.now().toString(36)}`;
|
|
worldRef.birthSerial = Math.max(0, u(arr[9], 1, 0));
|
|
worldRef.tarinaiPopulationLimit = Math.max(0, u(arr[10], 1, 0));
|
|
worldRef.objectLimit = Math.max(0, u(arr[11], 1, 0));
|
|
worldRef.day = Math.max(1, Math.floor(totalTime / dayLength) + 1);
|
|
worldRef.time = totalTime - (worldRef.day - 1) * dayLength;
|
|
worldRef.nextWeatherChange = rand(CONFIG.weatherChangeMin, CONFIG.weatherChangeMax);
|
|
worldRef.deadCount = u(arr[7], 1, 0);
|
|
worldRef.liveIdNext = 1;
|
|
worldRef.liveIdSerial = 1;
|
|
worldRef.lastBirthAt = u(arr[5], 10, -999);
|
|
worldRef.maxGeneration = Math.max(1, u(arr[6], 1, 1));
|
|
const moodId = moodValue?.(arr[2], "relaxed") || enumValue(MOOD_IDS, arr[2], "relaxed");
|
|
worldRef.colonyMood = worldRef.colonyMoodDefinition?.(moodId) || { id: moodId, label: moodId, effects: { personality: {} } };
|
|
worldRef.setGroundType?.(enumValue(GROUND_IDS, arr[1], "soil"), { silent: true, force: true });
|
|
}
|
|
function compactRelationships(t, tarinaiIndex) {
|
|
const out = [];
|
|
for (const [id, rel] of Object.entries(t.relationships || {})) {
|
|
const idx = tarinaiIndex.get(id);
|
|
if (!Number.isInteger(idx) || idx < 0) continue;
|
|
const affinity = Number(rel?.affinity) || 0;
|
|
const fear = Number(rel?.fear) || 0;
|
|
const wins = Math.min(15, Math.max(0, Math.round(Number(rel?.fightsWon) || 0)));
|
|
const losses = Math.min(15, Math.max(0, Math.round(Number(rel?.fightsLost) || 0)));
|
|
if (Math.abs(affinity) < 5 && fear < 5 && wins + losses < 1) continue;
|
|
out.push([idx, q(affinity, 2), q(fear, 2), wins | (losses << 4)]);
|
|
}
|
|
return out;
|
|
}
|
|
function expandRelationships(rows = [], tarinaiList = [], worldRef = null) {
|
|
const out = {};
|
|
for (const row of rows || []) {
|
|
if (!Array.isArray(row)) continue;
|
|
const other = tarinaiList[row[0]];
|
|
if (!other?.id) continue;
|
|
const fight = Number(row[3]) || 0;
|
|
out[other.id] = {
|
|
affinity: u(row[1], 2),
|
|
fear: u(row[2], 2),
|
|
fightsWon: fight & 15,
|
|
fightsLost: (fight >> 4) & 15,
|
|
lastEvent: fight ? "fight" : "",
|
|
lastTime: worldRef?.time || 0,
|
|
};
|
|
}
|
|
return out;
|
|
}
|
|
function compactTarinai(t, idx, familyIndex, tarinaiIndex, itemIndex) {
|
|
const parentIdx = (t.parents || []).map(id => familyIndex.get(id)).filter(Number.isInteger);
|
|
const timers = [
|
|
q(t.reproductionTimer, 10), q(t.fightCooldown, 10), q(t.loveMochiTimer, 10), q(t.fightMochiTimer, 10),
|
|
q(t.zunchiDiseaseSeverity, 10), q(t.zunchiStain, 10), q(t.itemEffectTimers?.laxative, 10), q(t.itemEffectTimers?.ammo, 10), q(t.mercuryLifeMultiplier, 100, 100)
|
|
];
|
|
const modes = [enumIndex(POWER_MODE_IDS, t.powerItemMode || ""), enumIndex(SIZE_MODE_IDS, t.sizeItemMode || ""), enumIndex(LIFE_MODE_IDS, t.lifeItemMode || "")];
|
|
const pinIdx = itemIndex.get(t.stuckPushpinId) ?? -1;
|
|
const nestIdx = itemIndex.get(t.insideNestBoxId) ?? -1;
|
|
const state = t.state === "sleep" ? "sleep" : "idle";
|
|
let targetIdx = -1;
|
|
if (state === "sleep") {
|
|
const targetId = t.sleepSession?.targetId || t.pendingGrassBedWakeId || t.target?.id || "";
|
|
targetIdx = itemIndex.get(targetId) ?? itemIndex.get(t.target?.id) ?? -1;
|
|
}
|
|
return [
|
|
flagPack(t), t.birthSeed || t.familyKey || t.id || `s${idx}`, q(t.x, 1), q(t.y, 1),
|
|
q(t.age, 10), q(t.generation, 1, 1), q(t.hunger, 1), q(t.energy, 1), q(t.circadianSleepPressure, 1),
|
|
needsToArray(t.needs || t.needRaw || {}), personalityToArray(t.currentPersonality),
|
|
parentIdx, compactRelationships(t, tarinaiIndex), timers, modes, pinIdx, nestIdx, [enumIndex(STATE_IDS, state), targetIdx],
|
|
geneticsToArray(t.genetics), [q(t.totalFightWins, 1), q(t.totalFightLosses, 1), q(t.tarinaiChampionSince, 10)]
|
|
];
|
|
}
|
|
function expandTarinai(row, index, worldRef) {
|
|
const TarinaiClass = global.Tarinai || (typeof Tarinai !== "undefined" ? Tarinai : null);
|
|
if (!TarinaiClass) throw new Error("Tarinai is not available");
|
|
const needs = arrayToNeeds(row[9]);
|
|
const age = u(row[4], 10);
|
|
const birthSeed = String(row[1] || `s${index}`);
|
|
const profile = global.TarinaiSeedFactory.birthProfile(birthSeed) || {};
|
|
const opts = {
|
|
familyKey: birthSeed,
|
|
birthSeed,
|
|
birthSerial: global.TarinaiSeedFactory.serialFromBirthSeed(birthSeed) || 0,
|
|
id: `st${index}`,
|
|
liveToken: 1,
|
|
currentPersonality: arrayToPersonality(row[10]),
|
|
genetics: Array.isArray(row[18]) && row[18].length ? arrayToGenetics(row[18]) : profile.genetics,
|
|
x: u(row[2], 1), y: u(row[3], 1), vx: 0, vy: 0,
|
|
age, birthTime: (worldRef?.time || 0) - age, generation: Math.max(1, u(row[5], 1, 1)),
|
|
hunger: u(row[6], 1), loneliness: Number(needs.social) || 0, energy: u(row[7], 1), circadianSleepPressure: u(row[8], 1),
|
|
needs, state: "idle",
|
|
isZunchiSlave: Boolean((Number(row[0]) || 0) & (1 << 5)),
|
|
isTarinaiChampion: Boolean((Number(row[0]) || 0) & (1 << 9)),
|
|
totalFightWins: Array.isArray(row[19]) ? u(row[19][0], 1) : 0,
|
|
totalFightLosses: Array.isArray(row[19]) ? u(row[19][1], 1) : 0,
|
|
tarinaiChampionSince: Array.isArray(row[19]) ? u(row[19][2], 10) : 0,
|
|
};
|
|
const t = new TarinaiClass(worldRef, opts);
|
|
t.familyKey = birthSeed;
|
|
t.birthSeed = birthSeed;
|
|
t.id = `st${index}`;
|
|
t.liveToken = 1;
|
|
t.needRaw = { ...needs };
|
|
t.needDisplay = { ...needs };
|
|
t.previousNeeds = { ...needs };
|
|
t.needRawBase = { ...needs };
|
|
flagApply(t, Number(row[0]) || 0);
|
|
t.__savedParentIdx = Array.isArray(row[11]) ? row[11] : [];
|
|
t.__savedRelationships = Array.isArray(row[12]) ? row[12] : [];
|
|
const timers = Array.isArray(row[13]) ? row[13] : [];
|
|
t.reproductionTimer = u(timers[0], 10); t.fightCooldown = u(timers[1], 10); t.loveMochiTimer = u(timers[2], 10); t.fightMochiTimer = u(timers[3], 10);
|
|
t.zunchiDiseaseSeverity = u(timers[4], 10); t.zunchiStain = u(timers[5], 10);
|
|
t.itemEffectTimers = { ...(t.itemEffectTimers || {}), laxative: u(timers[6], 10), ammo: u(timers[7], 10) };
|
|
t.mercuryLifeMultiplier = u(timers[8], 100, t.mercuryLifeMultiplier || 1);
|
|
const modes = Array.isArray(row[14]) ? row[14] : [];
|
|
t.powerItemMode = enumValue(POWER_MODE_IDS, modes[0], "");
|
|
t.sizeItemMode = enumValue(SIZE_MODE_IDS, modes[1], "");
|
|
t.lifeItemMode = enumValue(LIFE_MODE_IDS, modes[2], "");
|
|
t.__savedPinIdx = row[15]; t.__savedNestIdx = row[16]; t.__savedStateInfo = row[17];
|
|
if (typeof t.refreshEffectiveSize === "function") t.refreshEffectiveSize();
|
|
return t;
|
|
}
|
|
function itemExtra(item, tarinaiIndex = new Map(), itemIndex = new Map()) {
|
|
const type = item?.type || "";
|
|
if (item?.isStructure) {
|
|
const owner = tarinaiIndex.get(item.ownerId) ?? -1;
|
|
const carried = item.carriedById ? (tarinaiIndex.get(item.carriedById) ?? -1) : -1;
|
|
const consumed = item.singleUseConsumedById ? (tarinaiIndex.get(item.singleUseConsumedById) ?? -1) : -1;
|
|
return [q(item.hp, 10), q(item.maxHp, 10), owner, carried, item.singleUsePending ? 1 : 0, consumed, q(item.createdAt, 10)];
|
|
}
|
|
if (type === "grass") return [q(item.growth, 100), q(item.health, 100), q(item.fertilityBoost, 100), item.manualGrass || item.placedByPlayer ? 1 : 0];
|
|
if (type === "zunchi") return [enumIndex(STAGE_IDS, item.stage || "fresh"), q(item.freshness, 100), q(item.fertility, 100)];
|
|
if (type === "signboard") return [item.text || ""];
|
|
if (type === "robot_cleaner") return [Math.max(0, Math.min(0x3f, Number(item.robotCleanerMask ?? 3) | 0)), item.robotCleanerHighSpeed ? 1 : 0];
|
|
if (global.TarinaiPhysicsBodySystem.isPhysicsType(type)) {
|
|
const packed = global.TarinaiPhysicsBodySystem.compactItemExtra(item, tarinaiIndex, itemIndex);
|
|
if (packed) return packed;
|
|
}
|
|
if (global.TarinaiPhysicsBodySystem.isPhysicsType(type)) return { pf: 2, missing: true };
|
|
if (type === "duplicator") return [itemTypeIndex(item.storedFoodType || "", -1)];
|
|
if (type === "ball" || type === "balloon") return [q(item.vx, 10), q(item.vy, 10)];
|
|
if (type === "ant_nest") return [q(item.antCount, 1), q(item.queenSpawnAt, 10)];
|
|
if (type === "ant_corpse") return [q(item.decayTimer, 10)];
|
|
if (type === "firecracker" || type === "flame_firecracker") return [q(item.fuseTimer, 10)];
|
|
if (type === "sticky_bomb") return [q(item.fuseTimer, 10), tarinaiIndex.get(item.stickyBombCarrierId) ?? -1];
|
|
if (type === "fire") return [q(item.flameLife, 10)];
|
|
if (type === "nest_box") return [q(item.burnTimer, 10), q(item.nestFireEvacuationTimer, 10), item.burning ? 1 : 0, item.nestFireEvacuated ? 1 : 0];
|
|
if (global.isPinType(type)) return [enumIndex(PIN_STATE_IDS, item.pinState || "loose"), tarinaiIndex.get(item.pinTargetId) ?? -1, q(item.pinAttachAngle, 1000), q(item.pinAttachDistance, 10), q(item.pinOffsetY, 10)];
|
|
if (type === "fan") return [
|
|
q(item.angle, 1000, q(global.defaultItemAngle(type), 1000)),
|
|
item.fanSwingOn ? 1 : 0,
|
|
q(item.fanSwingCenter ?? item.angle ?? 0, 1000),
|
|
q(item.fanSwingRange ?? (35 * Math.PI / 180), 1000),
|
|
q(item.fanSwingSpeed ?? (48 * Math.PI / 180), 1000),
|
|
q(item.fanSwingPhase || 0, 1000),
|
|
q(item.fanBodyAngle ?? item.angle ?? 0, 1000),
|
|
];
|
|
if (global.isRotatableItemType(type)) return [q(item.angle, 1000, q(global.defaultItemAngle(type), 1000)), type === "gate_fence" && item.gateOpen ? 1 : 0];
|
|
if (global.isServingFoodType(type)) return [q(item.foodServingsRemaining ?? item.amount, 10), q(item.foodServingsMax, 10)];
|
|
return [];
|
|
}
|
|
function compactItem(item, index, tarinaiIndex = new Map(), itemIndex = new Map()) {
|
|
const typeId = itemTypeIndex(item?.type || "", -1);
|
|
if (typeId < 0 || item?.type === "trace") return null;
|
|
return [typeId, q(item?.x, 1), q(item?.y, 1), q(item?.amount ?? item?.hp, 10), itemExtra(item, tarinaiIndex, itemIndex)];
|
|
}
|
|
function applyItemExtra(item, extra, tarinaiList = [], itemList = []) {
|
|
if (!Array.isArray(extra)) return;
|
|
const type = item.type || "";
|
|
if (item.isStructure) {
|
|
item.hp = u(extra[0], 10, item.hp || 1);
|
|
item.maxHp = u(extra[1], 10, item.maxHp || item.hp || 1);
|
|
item.amount = Math.max(0.001, item.hp || item.amount || 1);
|
|
item.ownerId = tarinaiList[extra[2]]?.id || "";
|
|
item.carriedById = tarinaiList[extra[3]]?.id || "";
|
|
item.singleUsePending = !!extra[4];
|
|
item.singleUseConsumedById = tarinaiList[extra[5]]?.id || "";
|
|
item.createdAt = Number.isFinite(Number(extra[6])) ? u(extra[6], 10, item.createdAt || NaN) : (Number.isFinite(Number(item.createdAt)) ? item.createdAt : NaN);
|
|
if (type === "plushie") {
|
|
item.onHead = Boolean(item.carriedById);
|
|
}
|
|
const def = global.StructureRegistry?.get?.(type);
|
|
if (def) {
|
|
item.label = def.label;
|
|
item.roles = clonePlain(def.roles) || item.roles || {};
|
|
item.needEffects = clonePlain(def.needEffects) || item.needEffects || {};
|
|
}
|
|
return;
|
|
}
|
|
if (type === "grass") {
|
|
item.growth = u(extra[0], 100, item.growth || 0); item.health = u(extra[1], 100, item.health || 1); item.fertilityBoost = u(extra[2], 100, 0); item.manualGrass = !!extra[3]; item.placedByPlayer = !!extra[3];
|
|
global.TarinaiGrass.normalize(item);
|
|
} else if (type === "zunchi") {
|
|
item.stage = enumValue(STAGE_IDS, extra[0], "fresh"); item.freshness = u(extra[1], 100, item.freshness || 1); item.fertility = u(extra[2], 100, item.fertility || 1);
|
|
} else if (type === "signboard") {
|
|
item.text = String(extra[0] || "");
|
|
} else if (type === "robot_cleaner") {
|
|
const mask = Number(extra[0]);
|
|
item.robotCleanerMask = Number.isFinite(mask) ? Math.max(0, Math.min(0x3f, mask | 0)) : (global.TarinaiItemDynamicToolSystem?.robotCleanerDefaultMask?.() ?? 3);
|
|
item.robotCleanerHighSpeed = Boolean(extra[1]);
|
|
item.robotCleanerSpeed = global.TarinaiItemDynamicToolSystem?.robotCleanerEffectiveSpeed?.(item) ?? (item.robotCleanerHighSpeed ? 120 : 60);
|
|
item.robotCleanerTargetId = "";
|
|
item.robotCleanerTargetType = "";
|
|
} else if (global.TarinaiPhysicsBodySystem.applyCompactExtra(item, extra, tarinaiList, itemList)) {
|
|
return;
|
|
} else if (global.TarinaiPhysicsBodySystem.isPhysicsType(type)) {
|
|
// Physics items only accept the normalized pf:2 payload from this schema.
|
|
// Old array payloads are intentionally unsupported.
|
|
global.TarinaiPhysicsBodySystem.invalidateItem(item, "physics-extra-missing");
|
|
return;
|
|
} else if (type === "duplicator") {
|
|
item.storedFoodType = enumValue(ITEM_TYPE_IDS, extra[0], ""); item.storedFoodLabel = global.toolLabel ? global.toolLabel(item.storedFoodType) : item.storedFoodType; if (item.roles) item.roles.food = Boolean(item.storedFoodType);
|
|
} else if (type === "ball" || type === "balloon") {
|
|
item.vx = u(extra[0], 10); item.vy = u(extra[1], 10);
|
|
} else if (type === "ant_nest") {
|
|
item.antCount = u(extra[0], 1); item.antWorkers = []; item.queenSpawnAt = u(extra[1], 10);
|
|
} else if (type === "ant_corpse") {
|
|
item.decayTimer = u(extra[0], 10);
|
|
} else if (type === "firecracker" || type === "flame_firecracker") {
|
|
item.fuseTimer = u(extra[0], 10); item.fuseMax = Math.max(item.fuseMax || 5, item.fuseTimer || 0);
|
|
} else if (type === "sticky_bomb") {
|
|
item.fuseTimer = u(extra[0], 10, item.fuseTimer || 12); item.fuseMax = Math.max(item.fuseMax || 12, item.fuseTimer || 0); item.stickyBombCarrierId = tarinaiList[extra[1]]?.id || "";
|
|
} else if (type === "fire") {
|
|
item.flameLife = u(extra[0], 10, item.flameLife || 6);
|
|
} else if (type === "nest_box") {
|
|
item.burnTimer = u(extra[0], 10, 0);
|
|
item.nestFireEvacuationTimer = u(extra[1], 10, 0);
|
|
item.burning = Boolean(extra[2]) || item.burnTimer > 0.02 || item.nestFireEvacuationTimer > 0.02;
|
|
item.nestFireEvacuated = Boolean(extra[3]);
|
|
if (item.burning) {
|
|
item.flameWaterCooldown = 0;
|
|
item.nestFireEffectCooldown = 0;
|
|
}
|
|
} else if (global.isPinType(type)) {
|
|
item.pinState = enumValue(PIN_STATE_IDS, extra[0], "loose"); item.pinTargetId = tarinaiList[extra[1]]?.id || ""; item.pinAttachAngle = u(extra[2], 1000); item.pinAttachDistance = u(extra[3], 10); item.pinOffsetY = u(extra[4], 10);
|
|
} else if (type === "fan") {
|
|
const fallback = global.defaultItemAngle(type);
|
|
item.angle = global.normalizedItemAngle(u(extra[0], 1000, fallback), fallback);
|
|
item.fanSwingOn = Boolean(extra[1]);
|
|
item.fanSwingCenter = global.normalizedItemAngle(u(extra[2], 1000, item.angle), item.angle);
|
|
item.fanSwingRange = Math.max(0, Math.min(150 * Math.PI / 180, u(extra[3], 1000, 35 * Math.PI / 180)));
|
|
item.fanSwingSpeed = Math.max(0, Math.min(360 * Math.PI / 180, u(extra[4], 1000, 48 * Math.PI / 180)));
|
|
item.fanSwingPhase = u(extra[5], 1000, item.fanSwingPhase || 0);
|
|
item.fanBodyAngle = global.normalizedItemAngle(u(extra[6], 1000, item.angle), item.angle);
|
|
} else if (global.isRotatableItemType(type)) {
|
|
const fallback = global.defaultItemAngle(type);
|
|
item.angle = global.normalizedItemAngle(u(extra[0], 1000, fallback), fallback);
|
|
if (type === "gate_fence") item.gateOpen = !!extra[1];
|
|
} else if (global.isServingFoodType(type)) {
|
|
item.foodServingsRemaining = u(extra[0], 10, item.amount || 1); item.foodServingsMax = u(extra[1], 10, item.foodServingsRemaining || item.amount || 1); item.amount = item.foodServingsRemaining; global.updateServingFoodVisualSize(item);
|
|
}
|
|
}
|
|
function createSnapshot(worldRef = global.world) {
|
|
if (!worldRef) throw new Error("world is not ready");
|
|
const tarinaiLive = (worldRef.tarinai || [])
|
|
.filter(t => t && !t.dead)
|
|
.slice()
|
|
.sort((a, b) => mortonKeyXY(a.x, a.y) - mortonKeyXY(b.x, b.y) || String(a.id || "").localeCompare(String(b.id || "")));
|
|
const tarinaiIndex = new Map();
|
|
const familyIndex = new Map();
|
|
tarinaiLive.forEach((t, i) => { if (t.id) tarinaiIndex.set(t.id, i); if (t.familyKey) familyIndex.set(t.familyKey, i); });
|
|
const itemRecords = [];
|
|
for (const it of (worldRef.items || [])) {
|
|
if (!it || it.dead || it.type === "trace") continue;
|
|
const typeId = itemTypeIndex(it.type || "", -1);
|
|
if (typeId < 0) continue;
|
|
itemRecords.push({ item: it, sortTypeId: typeId, sortX: q(it.x, 1), sortY: q(it.y, 1) });
|
|
}
|
|
itemRecords.sort((a, b) => (a.sortTypeId || 0) - (b.sortTypeId || 0) || mortonKeyXY(a.sortX, a.sortY) - mortonKeyXY(b.sortX, b.sortY) || String(a.item?.id || "").localeCompare(String(b.item?.id || "")));
|
|
const itemIndex = new Map();
|
|
itemRecords.forEach((rec, i) => { if (rec.item?.id) itemIndex.set(rec.item.id, i); });
|
|
const itemRows = itemRecords.map((rec, i) => compactItem(rec.item, i, tarinaiIndex, itemIndex)).filter(Boolean);
|
|
return {
|
|
v: SNAPSHOT_VERSION,
|
|
a: "tj1",
|
|
m: [worldRef.day || 1, tarinaiLive.length, worldRef.fieldType || "garden"],
|
|
w: compactWorld(worldRef),
|
|
t: tarinaiLive.map((t, i) => compactTarinai(t, i, familyIndex, tarinaiIndex, itemIndex)),
|
|
i: itemRows,
|
|
};
|
|
}
|
|
function restoreSnapshot(snapshot, worldRef = global.world) {
|
|
if (!snapshot || typeof snapshot !== "object" || snapshot.v !== SNAPSHOT_VERSION || snapshot.a !== "tj1") throw new Error("invalid light save data");
|
|
if (!worldRef) throw new Error("world is not ready");
|
|
const fieldType = enumValue(FIELD_IDS, snapshot.w?.[0], "garden");
|
|
if (typeof global.applyFieldLayout === "function") global.applyFieldLayout(fieldType);
|
|
if (typeof worldRef.reset === "function") worldRef.reset(0, fieldType);
|
|
worldRef.tarinai = [];
|
|
worldRef.items = [];
|
|
worldRef.ants = [];
|
|
worldRef.effects = [];
|
|
worldRef.logs = [];
|
|
worldRef.selected = null;
|
|
worldRef.events = global.TarinaiEvents || null;
|
|
worldRef.liveTarinai = new Map();
|
|
applyCompactWorld(worldRef, snapshot.w || []);
|
|
|
|
const tarRows = Array.isArray(snapshot.t) ? snapshot.t : [];
|
|
for (let idx = 0; idx < tarRows.length; idx++) {
|
|
const t = expandTarinai(tarRows[idx] || [], idx, worldRef);
|
|
worldRef.tarinai.push(t);
|
|
if (t.id) worldRef.liveTarinai.set(t.id, { token: t.liveToken || 1, target: t });
|
|
}
|
|
for (const t of worldRef.tarinai) {
|
|
t.parents = (t.__savedParentIdx || []).map(i => worldRef.tarinai[i]?.familyKey).filter(Boolean);
|
|
t.children = [];
|
|
}
|
|
for (const child of worldRef.tarinai) {
|
|
for (const parentIdx of child.__savedParentIdx || []) {
|
|
const parent = worldRef.tarinai[parentIdx];
|
|
if (parent?.familyKey && child.familyKey && !parent.children.includes(child.familyKey)) parent.children.push(child.familyKey);
|
|
}
|
|
}
|
|
const liveByFamilyKey = new Map((worldRef.tarinai || []).map(t => [t.familyKey, t]).filter(([id]) => id));
|
|
for (const t of worldRef.tarinai) {
|
|
t.parentNames = (t.parents || []).map(id => liveByFamilyKey.get(id)?.name || "").filter(Boolean);
|
|
delete t.__savedParentIdx;
|
|
}
|
|
const itemRows = Array.isArray(snapshot.i) ? snapshot.i : [];
|
|
for (let idx = 0; idx < itemRows.length; idx++) {
|
|
const row = itemRows[idx] || [];
|
|
const type = enumValue(ITEM_TYPE_IDS, row[0], "");
|
|
if (!type) continue;
|
|
const extra = (Array.isArray(row[4]) || (row[4] && typeof row[4] === "object")) ? row[4] : [];
|
|
const owner = global.StructureRegistry?.get?.(type) ? worldRef.tarinai[extra[2]] || null : null;
|
|
const item = global.StructureRegistry?.get?.(type)
|
|
? global.StructureRegistry.create(type, owner, u(row[1], 1), u(row[2], 1), worldRef)
|
|
: new Item(type, u(row[1], 1), u(row[2], 1));
|
|
item.id = `li${idx}`;
|
|
item.world = worldRef;
|
|
item.amount = u(row[3], 10, item.amount || 1);
|
|
item.__savedExtra = extra;
|
|
worldRef.items.push(item);
|
|
}
|
|
for (const item of worldRef.items) {
|
|
applyItemExtra(item, item.__savedExtra || [], worldRef.tarinai, worldRef.items);
|
|
delete item.__savedExtra;
|
|
}
|
|
for (const t of worldRef.tarinai) {
|
|
if (Number.isInteger(t.__savedPinIdx) && t.__savedPinIdx >= 0) t.stuckPushpinId = worldRef.items[t.__savedPinIdx]?.id || null;
|
|
if (Number.isInteger(t.__savedNestIdx) && t.__savedNestIdx >= 0) t.insideNestBoxId = worldRef.items[t.__savedNestIdx]?.id || null;
|
|
const stateInfo = Array.isArray(t.__savedStateInfo) ? t.__savedStateInfo : [];
|
|
const state = enumValue(STATE_IDS, stateInfo[0], "idle");
|
|
if (state === "sleep") {
|
|
const sleepTarget = worldRef.items[stateInfo[1]] || null;
|
|
t.target = sleepTarget;
|
|
t.sleeping = true;
|
|
t.state = "sleep";
|
|
t.sleepSession = { startedAt: worldRef.time || 0, minDuration: 6, targetEnergy: 86, maxDuration: 36, targetId: sleepTarget?.id || null, consumesGrassBedOnWake: sleepTarget?.type === "grass_bed" };
|
|
if (sleepTarget?.type === "grass_bed") {
|
|
t.pendingGrassBedWakeId = sleepTarget.id;
|
|
sleepTarget.singleUsePending = true;
|
|
sleepTarget.singleUseConsumedById = t.id;
|
|
}
|
|
} else {
|
|
t.state = "idle";
|
|
t.sleeping = false;
|
|
t.target = null;
|
|
t.sleepSession = null;
|
|
}
|
|
t.relationships = expandRelationships(t.__savedRelationships || [], worldRef.tarinai, worldRef);
|
|
delete t.__savedPinIdx; delete t.__savedNestIdx; delete t.__savedRelationships; delete t.__savedStateInfo;
|
|
}
|
|
const updateNeedsRuntime = global.TarinaiNeedsRuntime?.updateNeeds;
|
|
if (typeof updateNeedsRuntime === "function") {
|
|
for (const t of worldRef.tarinai) updateNeedsRuntime(t, worldRef, 0);
|
|
}
|
|
worldRef.liveIdNext = Math.max(1, worldRef.tarinai.length + 1);
|
|
worldRef.liveIdSerial = Math.max(1, worldRef.tarinai.length + 1);
|
|
worldRef.birthSerial = Math.max(Number(worldRef.birthSerial) || 0, ...worldRef.tarinai.map(t => Number(t.birthSerial) || 0));
|
|
worldRef.family = {};
|
|
worldRef.familyVersion = (worldRef.familyVersion || 0) + 1;
|
|
for (const t of worldRef.tarinai) worldRef.recordFamily?.(t);
|
|
worldRef.familyCleanVersion = null;
|
|
worldRef.familyTreeDirty = true;
|
|
worldRef.drawListDirty = true;
|
|
worldRef.terrainDirty = true;
|
|
worldRef.relationNotices = {};
|
|
worldRef.resolvedFightIds = {};
|
|
worldRef.eventCounters = {};
|
|
worldRef.updateItemCounts?.();
|
|
worldRef.enforceGrassLimit?.("load-grass-limit");
|
|
worldRef.compactItems?.();
|
|
worldRef.updateItemCounts?.();
|
|
worldRef.updateEffectCounts?.();
|
|
worldRef.rebuildSpatial?.(true);
|
|
worldRef.markTerrainDirty?.("load");
|
|
worldRef.clampCamera?.();
|
|
return { world: worldRef, snapshotVersion: SNAPSHOT_VERSION };
|
|
}
|
|
|
|
global.TarinaiSnapshot = {
|
|
version: SNAPSHOT_VERSION,
|
|
createSnapshot,
|
|
restoreSnapshot,
|
|
};
|
|
})(typeof window !== "undefined" ? window : globalThis);
|