asdfg
This commit is contained in:
parent
f6d7412744
commit
091afc2b5c
77 changed files with 4405 additions and 8223 deletions
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue