This commit is contained in:
33333-33333 2026-07-29 20:52:11 +09:00
commit 091afc2b5c
77 changed files with 4405 additions and 8223 deletions

View file

@ -79,7 +79,7 @@
function cloneSnapshotPlain(value, depth = 0) {
if (value == null) return value;
const type = typeof value;
if (type === "number") return Number.isFinite(value) ? value : 0;
if (type === "number") return Number.isFinite(value) ? value : null;
if (type === "string" || type === "boolean") return value;
if (type === "function" || type === "symbol" || type === "undefined") return undefined;
if (depth > 8) return undefined;
@ -100,6 +100,158 @@
}
return out;
}
function cloneEntityState(entity, skip = []) {
const blocked = new Set(["world", "id", ...skip]);
const out = {};
for (const [key, value] of Object.entries(entity || {})) {
if (blocked.has(key) || DEFAULT_SKIP.has(key)) continue;
const cloned = cloneSnapshotPlain(value);
if (cloned !== undefined) out[key] = cloned;
}
return out;
}
function compactSnapshotExtension(worldRef, tarinaiLive, itemRecords) {
const tarIds = tarinaiLive.map(t => String(t?.id || ""));
const itemIds = itemRecords.map(rec => String(rec?.item?.id || ""));
const tarStates = tarinaiLive.map(t => {
// SV-11 is intentionally not serialized: actionCooldowns remain session-local.
const state = cloneEntityState(t, [
"liveToken", "familyKey", "birthSeed", "birthSerial", "parents", "parentNames", "children",
"actionCooldowns", "relationships", "target", "panicTarget", "targetRef", "targetEntity", "source",
]);
state.__targetId = String(t?.target?.id || t?.targetId || "");
state.__panicTargetId = String(t?.panicTarget?.id || "");
state.__relationships = cloneSnapshotPlain(t?.relationships || {}) || {};
return state;
});
const itemStates = itemRecords.map(rec => cloneEntityState(rec?.item, ["type", "targetRef", "source"]));
const antStates = (worldRef?.ants || []).filter(a => a && !a.dead).map(a => {
const state = cloneEntityState(a, ["targetRef", "source"]);
state.id = String(a.id || "");
return state;
});
return {
ids: { t: tarIds, i: itemIds },
w: {
nextWeatherChange: finiteOrNull(worldRef?.nextWeatherChange),
effects: cloneSnapshotPlain((worldRef?.effects || []).slice(-256)) || [],
logs: cloneSnapshotPlain((worldRef?.logs || []).slice(-512)) || [],
relationNotices: cloneSnapshotPlain(worldRef?.relationNotices || {}) || {},
resolvedFightIds: cloneSnapshotPlain(worldRef?.resolvedFightIds || {}) || {},
eventCounters: cloneSnapshotPlain(worldRef?.eventCounters || {}) || {},
},
t: tarStates,
i: itemStates,
a: antStates,
r: cloneSnapshotPlain((worldRef?.residues || []).filter(r => r && !r.dead && Number(r.amount || 0) > 0)) || [],
};
}
const ID_KEYED_SNAPSHOT_MAPS = new Set(["__relationships", "relationships"]);
function snapshotFieldContainsEntityId(key = "") {
const name = String(key || "");
return name === "__targetId" || name === "__panicTargetId" || /(?:Id|Ids)$/.test(name);
}
function remapSnapshotValue(value, idMap, depth = 0, fieldName = "") {
if (depth > 12 || value == null) return value;
if (typeof value === "string") {
return snapshotFieldContainsEntityId(fieldName) ? (idMap.get(value) || value) : value;
}
if (Array.isArray(value)) return value.map(v => remapSnapshotValue(v, idMap, depth + 1, fieldName));
if (!isPlainObject(value)) return value;
const out = {};
const remapKeys = ID_KEYED_SNAPSHOT_MAPS.has(String(fieldName || ""));
for (const [key, child] of Object.entries(value)) {
const mappedKey = remapKeys ? (idMap.get(key) || key) : key;
out[mappedKey] = remapSnapshotValue(child, idMap, depth + 1, key);
}
return out;
}
function applySnapshotExtension(extension, worldRef) {
if (!extension || typeof extension !== "object") return false;
const oldTarIds = Array.isArray(extension.ids?.t) ? extension.ids.t : [];
const oldItemIds = Array.isArray(extension.ids?.i) ? extension.ids.i : [];
const idMap = new Map();
oldTarIds.forEach((id, index) => { if (id && worldRef.tarinai?.[index]?.id) idMap.set(String(id), worldRef.tarinai[index].id); });
oldItemIds.forEach((id, index) => { if (id && worldRef.items?.[index]?.id) idMap.set(String(id), worldRef.items[index].id); });
const tarStates = Array.isArray(extension.t) ? extension.t : [];
for (let index = 0; index < worldRef.tarinai.length; index++) {
const t = worldRef.tarinai[index];
const raw = isPlainObject(tarStates[index]) ? tarStates[index] : null;
if (!t || !raw) continue;
const state = remapSnapshotValue(raw, idMap);
const targetId = String(state.__targetId || "");
const panicTargetId = String(state.__panicTargetId || "");
const relationships = isPlainObject(state.__relationships) ? state.__relationships : null;
delete state.__targetId;
delete state.__panicTargetId;
delete state.__relationships;
delete state.actionCooldowns;
Object.assign(t, state);
// Explicitly preserve the requested exception.
t.actionCooldowns = {};
if (relationships) t.relationships = relationships;
t.target = targetId ? (worldRef.tarinai.find(v => v?.id === targetId) || worldRef.items.find(v => v?.id === targetId) || null) : null;
t.panicTarget = panicTargetId ? (worldRef.tarinai.find(v => v?.id === panicTargetId) || worldRef.items.find(v => v?.id === panicTargetId) || null) : null;
if (t.sleepSession && typeof t.sleepSession === "object" && t.sleepSession.targetId) {
t.sleepSession.targetId = idMap.get(String(t.sleepSession.targetId)) || String(t.sleepSession.targetId);
}
if (typeof t.refreshEffectiveSize === "function") {
const savedEnergy = Number(t.energy);
t.refreshEffectiveSize();
if (Number.isFinite(savedEnergy)) t.energy = Math.max(0, Math.min(Number(t.maxEnergy) || 240, savedEnergy));
}
}
const itemStates = Array.isArray(extension.i) ? extension.i : [];
for (let index = 0; index < worldRef.items.length; index++) {
const item = worldRef.items[index];
const raw = isPlainObject(itemStates[index]) ? itemStates[index] : null;
if (!item || !raw) continue;
Object.assign(item, remapSnapshotValue(raw, idMap));
item.world = worldRef;
if (item.type === "grass") {
if (!Number.isFinite(Number(item.grassStage))) item.grassStage = global.grassStageFromGrowth?.(item.growth, item.amount);
global.TarinaiGrass?.normalize?.(item);
}
}
worldRef.residues = [];
worldRef.residueSerial = 0;
for (const row of (Array.isArray(extension.r) ? extension.r : [])) {
if (!row || typeof row !== "object") continue;
worldRef.addResidue?.(String(row.type || "trace"), Number(row.x) || 0, Number(row.y) || 0, remapSnapshotValue(row, idMap));
}
worldRef.rebuildResidueIndex?.();
worldRef.ants = [];
const antRows = Array.isArray(extension.a) ? extension.a : [];
const AntClass = global.AntActor || (typeof AntActor !== "undefined" ? AntActor : null);
if (AntClass) {
for (const row of antRows) {
if (!isPlainObject(row)) continue;
const state = remapSnapshotValue(row, idMap);
const ant = new AntClass(worldRef, state);
Object.assign(ant, state);
ant.world = worldRef;
ant.targetRef = ant.targetId ? (worldRef.tarinai.find(t => t?.id === ant.targetId) || null) : null;
worldRef.ants.push(ant);
}
}
const worldState = isPlainObject(extension.w) ? remapSnapshotValue(extension.w, idMap) : {};
if (Number.isFinite(Number(worldState.nextWeatherChange))) worldRef.nextWeatherChange = Number(worldState.nextWeatherChange);
worldRef.effects = Array.isArray(worldState.effects) ? worldState.effects : [];
worldRef.logs = Array.isArray(worldState.logs) ? worldState.logs : [];
worldRef.relationNotices = isPlainObject(worldState.relationNotices) ? worldState.relationNotices : {};
worldRef.resolvedFightIds = isPlainObject(worldState.resolvedFightIds) ? worldState.resolvedFightIds : {};
worldRef.eventCounters = isPlainObject(worldState.eventCounters) ? worldState.eventCounters : {};
return true;
}
function needsToArray(needs = {}) {
return NEED_KEYS.map(k => q(needs[k], 1));
}
@ -186,14 +338,14 @@
q(worldRef?.birthSerial, 1, 0),
q(worldRef?.tarinaiPopulationLimit, 1, 0),
q(worldRef?.objectLimit, 1, 0),
[...new Set((worldRef?.achievementNaturalSlaveGenerations || []).map(value => Math.max(1, Math.floor(Number(value) || 1))))].sort((a, b) => a - b),
[...new Set((worldRef?.achievementNaturalKingGenerations || []).map(value => Math.max(1, Math.floor(Number(value) || 1))))].sort((a, b) => a - b),
q(worldRef?.achievementPlayerPlacementCount, 1, 0),
[], // legacy achievement slot 12
[], // legacy achievement slot 13
0, // legacy achievement slot 14
q(worldRef?.achievementLastInterventionAt, 10, 0),
q(worldRef?.achievementNoDeathStartAt, 10, -10),
q(worldRef?.achievementDirectFeedCount, 1, 0),
0, // legacy achievement slot 17
elapsedDays,
q(worldRef?.achievementManualTarinaiAddedCount, 1, 0),
0, // legacy achievement slot 19
];
}
function applyCompactWorld(worldRef, arr = [], meta = []) {
@ -218,13 +370,8 @@
worldRef.birthSerial = Math.max(0, u(arr[9], 1, 0));
worldRef.tarinaiPopulationLimit = worldRef.normalizeColonyLimit?.(u(arr[10], 1, 0)) ?? Math.min(300, Math.max(0, u(arr[10], 1, 0)));
worldRef.objectLimit = worldRef.normalizeColonyLimit?.(u(arr[11], 1, 0)) ?? Math.min(300, Math.max(0, u(arr[11], 1, 0)));
worldRef.achievementNaturalSlaveGenerations = Array.isArray(arr[12]) ? [...new Set(arr[12].map(value => Math.max(1, Math.floor(Number(value) || 1))))].sort((a, b) => a - b) : [];
worldRef.achievementNaturalKingGenerations = Array.isArray(arr[13]) ? [...new Set(arr[13].map(value => Math.max(1, Math.floor(Number(value) || 1))))].sort((a, b) => a - b) : [];
worldRef.achievementPlayerPlacementCount = Math.max(0, u(arr[14], 1, 0));
worldRef.achievementLastInterventionAt = Math.max(0, u(arr[15], 10, totalTime));
worldRef.achievementNoDeathStartAt = u(arr[16], 10, -1);
worldRef.achievementDirectFeedCount = Math.max(0, u(arr[17], 1, 0));
worldRef.achievementManualTarinaiAddedCount = Math.min(100, Math.max(0, u(arr[19], 1, 0)));
worldRef.time = Math.max(0, totalTime);
worldRef.elapsedDays = encodedElapsedDays;
worldRef.day = worldRef.elapsedDays + 1;
@ -239,6 +386,7 @@
worldRef.setGroundType?.(enumValue(GROUND_IDS, arr[1], "soil"), { silent: true, force: true });
}
function finiteOrNull(value) {
if (value === null || value === undefined || value === "") return null;
const n = Number(value);
return Number.isFinite(n) ? n : null;
}
@ -246,13 +394,12 @@
const isUnlocked = id => Boolean(global.TarinaiAchievements?.isUnlocked?.(id));
const needSlaveGenerations = !isUnlocked("natural_zunchi_slave_5_generations");
const needKingGenerations = !isUnlocked("natural_tarinai_king_3_generations");
const needDirectFeed = !isUnlocked("direct_feed_33");
const needRobotClean = !isUnlocked("robot_cleaner_100");
const needEnemyFriend = !isUnlocked("enemy_enemy_friend");
const needSelfSufficient = !isUnlocked("self_sufficient");
const needIdleObserver = !isUnlocked("idle_observer_5_minutes");
const needSafeColony = !isUnlocked("safe_colony_25_5_minutes");
const needHappyColony = !isUnlocked("colony_happy");
const needEliteFew = !isUnlocked("elite_few");
const needOverprotective = !isUnlocked("overprotective");
const needSauna = !isUnlocked("sauna_cold_plunge");
const needQuickDelete = !isUnlocked("unplanned_city_30");
@ -260,23 +407,26 @@
const needPlayerPlacedMetadata = needQuickDelete || needMinimalist;
const metadata = {
w: [
needSlaveGenerations ? [...new Set((worldRef?.achievementNaturalSlaveGenerations || []).map(value => Math.max(1, Math.floor(Number(value) || 1))))].sort((a, b) => a - b) : [],
needKingGenerations ? [...new Set((worldRef?.achievementNaturalKingGenerations || []).map(value => Math.max(1, Math.floor(Number(value) || 1))))].sort((a, b) => a - b) : [],
needDirectFeed ? Math.max(0, Math.floor(Number(worldRef?.achievementDirectFeedCount || 0) || 0)) : 0,
needRobotClean ? Math.max(0, Math.floor(Number(worldRef?.achievementRobotCleanCount || 0) || 0)) : 0,
[], // legacy lineage slot
[], // legacy lineage slot
0, // legacy direct-feed slot
0, // legacy robot-clean slot; old saves are still imported below
needEnemyFriend ? Math.max(0, Math.floor(Number(worldRef?.achievementEnemyAntKills || 0) || 0)) : 0,
needEnemyFriend ? finiteOrNull(worldRef?.achievementLastTarinaiDamageAt) : null,
null, // legacy last-damage slot
needSelfSufficient ? finiteOrNull(worldRef?.achievementSelfSufficientStartAt) : -1,
needSelfSufficient ? finiteOrNull(worldRef?.achievementLastDirectFeedAt) : null,
null, // legacy last-direct-feed slot
needIdleObserver ? finiteOrNull(worldRef?.achievementLastInterventionAt) : 0,
needSafeColony ? finiteOrNull(worldRef?.achievementNoDeathStartAt) : -1,
needHappyColony ? finiteOrNull(worldRef?.achievementHappyStartAt) : -1,
needEliteFew ? finiteOrNull(worldRef?.achievementEliteFewStartAt) : -1,
],
t: tarinaiLive.map(t => [
needOverprotective ? Math.max(0, Math.floor(Number(t?._achievementDirectFeedCount || 0) || 0)) : 0,
needOverprotective ? Math.max(0, Math.floor(Number(t?._achievementDirectTreatmentCount || 0) || 0)) : 0,
null,
needSauna ? finiteOrNull(t?._achievementSaunaHotAt) : null,
needSlaveGenerations ? Math.max(0, Math.floor(Number(t?._achievementNaturalSlaveLineageDepth || 0) || 0)) : 0,
needKingGenerations ? Math.max(0, Math.floor(Number(t?._achievementNaturalKingLineageDepth || 0) || 0)) : 0,
]),
i: itemRecords.map(rec => [
needPlayerPlacedMetadata && rec?.item?._achievementPlayerPlaced ? 1 : 0,
@ -292,26 +442,24 @@
function applyAchievementMetadata(worldRef, metadata = null) {
if (!worldRef || !metadata || typeof metadata !== "object") return false;
const w = Array.isArray(metadata.w) ? metadata.w : [];
worldRef.achievementNaturalSlaveGenerations = Array.isArray(w[0]) ? [...new Set(w[0].map(value => Math.max(1, Math.floor(Number(value) || 1))))].sort((a, b) => a - b) : (worldRef.achievementNaturalSlaveGenerations || []);
worldRef.achievementNaturalKingGenerations = Array.isArray(w[1]) ? [...new Set(w[1].map(value => Math.max(1, Math.floor(Number(value) || 1))))].sort((a, b) => a - b) : (worldRef.achievementNaturalKingGenerations || []);
worldRef.achievementDirectFeedCount = Math.max(0, Math.floor(Number(w[2] || 0) || 0));
worldRef.achievementRobotCleanCount = Math.max(0, Math.floor(Number(w[3] || 0) || 0));
const legacyRobotCleanCount = Math.max(0, Math.floor(Number(w[3] || 0) || 0));
global.TarinaiAchievements?.importWorldAchievementProgress?.({ robotCleanCount: legacyRobotCleanCount, source: "snapshot-metadata" });
worldRef.achievementEnemyAntKills = Math.max(0, Math.floor(Number(w[4] || 0) || 0));
worldRef.achievementLastTarinaiDamageAt = w[5] != null && Number.isFinite(Number(w[5])) ? Number(w[5]) : -Infinity;
worldRef.achievementSelfSufficientStartAt = w[6] != null && Number.isFinite(Number(w[6])) ? Number(w[6]) : -1;
worldRef.achievementLastDirectFeedAt = w[7] != null && Number.isFinite(Number(w[7])) ? Number(w[7]) : -Infinity;
worldRef.achievementLastInterventionAt = w[8] != null && Number.isFinite(Number(w[8])) ? Number(w[8]) : Math.max(0, Number(worldRef.time || 0) || 0);
worldRef.achievementNoDeathStartAt = w[9] != null && Number.isFinite(Number(w[9])) ? Number(w[9]) : -1;
worldRef.achievementHappyStartAt = w[10] != null && Number.isFinite(Number(w[10])) ? Number(w[10]) : -1;
worldRef.achievementEliteFewStartAt = w[11] != null && Number.isFinite(Number(w[11])) ? Number(w[11]) : -1;
const tarRows = Array.isArray(metadata.t) ? metadata.t : [];
(worldRef.tarinai || []).forEach((t, index) => {
const row = Array.isArray(tarRows[index]) ? tarRows[index] : [];
t._achievementDirectFeedCount = Math.max(0, Math.floor(Number(row[0] || 0) || 0));
t._achievementDirectTreatmentCount = Math.max(0, Math.floor(Number(row[1] || 0) || 0));
t._achievementFightMochiAt = null;
t._achievementFightMochiSerial = 0;
t._achievementFightMochiWorld = null;
t._achievementSaunaHotAt = row[3] != null && Number.isFinite(Number(row[3])) ? Number(row[3]) : null;
t._achievementNaturalSlaveLineageDepth = Math.max(0, Math.floor(Number(row[4] || 0) || 0));
t._achievementNaturalKingLineageDepth = Math.max(0, Math.floor(Number(row[5] || 0) || 0));
});
const itemRows = Array.isArray(metadata.i) ? metadata.i : [];
(worldRef.items || []).forEach((item, index) => {
@ -333,8 +481,7 @@
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)]);
out.push([idx, q(affinity, 2), q(fear, 2), wins | (losses << 4), String(rel?.lastEvent || ""), q(rel?.lastTime, 10)]);
}
return out;
}
@ -350,8 +497,8 @@
fear: u(row[2], 2),
fightsWon: fight & 15,
fightsLost: (fight >> 4) & 15,
lastEvent: fight ? "fight" : "",
lastTime: worldRef?.time || 0,
lastEvent: String(row[4] || ""),
lastTime: u(row[5], 10, worldRef?.time || 0),
};
}
return out;
@ -365,7 +512,7 @@
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";
const state = STATE_IDS.includes(String(t.state || "")) ? String(t.state) : "idle";
let targetIdx = -1;
if (state === "sleep") {
const targetId = t.sleepSession?.targetId || t.pendingGrassBedWakeId || t.target?.id || "";
@ -427,7 +574,9 @@
t.sizeItemMode = enumValue(SIZE_MODE_IDS, modes[1], "");
t.lifeItemMode = enumValue(LIFE_MODE_IDS, modes[2], "");
t.__savedPinIdx = row[15]; t.__savedNestIdx = row[16]; t.__savedStateInfo = row[17];
const savedEnergy = u(row[7], 1);
if (typeof t.refreshEffectiveSize === "function") t.refreshEffectiveSize();
t.energy = Math.max(0, Math.min(Number(t.maxEnergy) || 240, savedEnergy));
return t;
}
function itemExtra(item, tarinaiIndex = new Map(), itemIndex = new Map()) {
@ -451,8 +600,8 @@
]);
return base;
}
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 === "grass") return [q(item.growth, 100), q(item.health, 100), q(item.fertilityBoost, 100), item.manualGrass || item.placedByPlayer ? 1 : 0, q(item.grassStage, 1), item.burning ? 1 : 0, q(item.burnTimer, 10)];
if (type === "zunchi") return [enumIndex(STAGE_IDS, item.stage || "fresh"), q(item.freshness, 100), q(item.fertility, 100), q(item.stageTimer, 10), q(item.age, 10), item.burning ? 1 : 0, q(item.burnTimer, 10)];
if (type === "signboard") return [item.text || ""];
if (type === "robot_cleaner") return [Math.max(0, Math.min(0x7f, Number(item.robotCleanerMask ?? 3) | 0)), item.robotCleanerHighSpeed ? 1 : 0];
if (type === "circuit_board") return [JSON.stringify(global.TarinaiCircuitBoardSystem?.serializeConfig?.(item) || item.circuitConfig || {})];
@ -565,9 +714,12 @@
}
if (type === "grass") {
item.growth = u(extra[0], 100, item.growth || 0); item.health = u(extra[1], 100, item.health || 1); item.fertilityBoost = u(extra[2], 100, 0); item.manualGrass = !!extra[3]; item.placedByPlayer = !!extra[3];
item.grassStage = u(extra[4], 1, global.grassStageFromGrowth?.(item.growth, item.amount) || 0);
item.burning = Boolean(extra[5]); item.burnTimer = u(extra[6], 10, 0);
global.TarinaiGrass.normalize(item);
} else if (type === "zunchi") {
item.stage = enumValue(STAGE_IDS, extra[0], "fresh"); item.freshness = u(extra[1], 100, item.freshness || 1); item.fertility = u(extra[2], 100, item.fertility || 1);
item.stageTimer = u(extra[3], 10, 0); item.age = u(extra[4], 10, item.age || 0); item.burning = Boolean(extra[5]); item.burnTimer = u(extra[6], 10, 0);
} else if (type === "signboard") {
item.text = String(extra[0] || "");
} else if (type === "robot_cleaner") {
@ -673,6 +825,7 @@
i: itemRows,
r: residueRows,
g: compactAchievementMetadata(worldRef, tarinaiLive, itemRecords, options),
x: compactSnapshotExtension(worldRef, tarinaiLive, itemRecords),
};
}
function restoreSnapshot(snapshot, worldRef = global.world) {
@ -763,11 +916,11 @@
if (typeof updateNeedsRuntime === "function") {
for (const t of worldRef.tarinai) updateNeedsRuntime(t, worldRef, 0);
}
const restoredExtendedState = applySnapshotExtension(snapshot.x || null, worldRef);
// Velocity is not serialized. Start restored bodies from a neutral physical
// state, but do not grant a timed collision/damage immunity period.
// Legacy snapshots without extended state start from a neutral physical state.
delete worldRef._restoreSettleUntil;
for (const t of worldRef.tarinai) {
if (!restoredExtendedState) for (const t of worldRef.tarinai) {
if (!t || t.dead) continue;
t.vx = 0;
t.vy = 0;
@ -790,9 +943,11 @@
worldRef.familyTreeDirty = true;
worldRef.drawListDirty = true;
worldRef.terrainDirty = true;
worldRef.relationNotices = {};
worldRef.resolvedFightIds = {};
worldRef.eventCounters = {};
if (!restoredExtendedState) {
worldRef.relationNotices = {};
worldRef.resolvedFightIds = {};
worldRef.eventCounters = {};
}
worldRef.rebuildTarinaiCountCache?.("restore");
worldRef.updateItemCounts?.();
worldRef.enforceGrassLimit?.("load-grass-limit");
@ -814,4 +969,10 @@
createSnapshot,
restoreSnapshot,
};
if (global.__TARINAI_TEST__) {
global.TarinaiSnapshotTestHooks = Object.freeze({
compactAchievementMetadata,
applyAchievementMetadata,
});
}
})(typeof window !== "undefined" ? window : globalThis);