Refactor org platform navigation and content flows
All checks were successful
Build Org Website / build (push) Successful in 38s
All checks were successful
Build Org Website / build (push) Successful in 38s
This commit is contained in:
@@ -1,26 +1,24 @@
|
||||
(function (root, factory) {
|
||||
"use strict";
|
||||
|
||||
const api = factory();
|
||||
const data = root.ArchiveWorldData || (typeof require === "function" ? require("./archive-world-data.js") : null);
|
||||
const api = factory(data);
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
root.ArchiveWorldState = api;
|
||||
}(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
||||
}(typeof globalThis !== "undefined" ? globalThis : this, function (Data) {
|
||||
"use strict";
|
||||
|
||||
const VERSION = 1;
|
||||
const STORAGE_KEY = "zxh_archive_world_v1";
|
||||
const VERSION = 2;
|
||||
const STORAGE_KEY = "zxh_archive_world_v2";
|
||||
const LEGACY_KEY = "zxh_archive_world_v1";
|
||||
const PALETTES = Object.freeze(["brass", "moss", "berry"]);
|
||||
const LANDMARK_IDS = Object.freeze([
|
||||
"gate",
|
||||
"library",
|
||||
"study",
|
||||
"inn",
|
||||
"workshop",
|
||||
"playroom",
|
||||
"museum",
|
||||
"garden"
|
||||
]);
|
||||
const SPAWN = Object.freeze({ x: 640, y: 830 });
|
||||
const FACES = Object.freeze(["north", "south", "east", "west"]);
|
||||
const SPAWN = Object.freeze({ area: "town", spawn: "gate", x: 724, y: 965 });
|
||||
const LANDMARK_IDS = Data ? Data.LANDMARK_IDS : Object.freeze(["gate", "library", "study", "inn", "workshop", "playroom", "museum", "garden"]);
|
||||
const QUEST_IDS = Data ? Data.QUEST_IDS : Object.freeze(LANDMARK_IDS.concat("lost_letter"));
|
||||
const ITEM_IDS = Data ? Data.ITEM_IDS : Object.freeze(["living_bookmark", "lantern", "archive_key", "swiftstep_boots", "ink_vial", "memory_fragment", "garden_seed", "workshop_cog", "museum_lens", "recall_stew", "lore_page", "archive_sigil"]);
|
||||
const AREA_IDS = Data ? Data.AREA_IDS : Object.freeze(["town", "vault", "workshop", "playroom", "boss"]);
|
||||
const BOSS_IDS = Object.freeze(["sentinel", "redactor"]);
|
||||
const LEVELS = Object.freeze([0, 80, 200, 380, 620]);
|
||||
|
||||
function validateName(value) {
|
||||
const name = String(value || "").trim().replace(/\s+/g, " ");
|
||||
@@ -31,21 +29,92 @@
|
||||
return PALETTES.includes(value) ? value : null;
|
||||
}
|
||||
|
||||
function finite(value, fallback, min, max) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? Math.min(max, Math.max(min, number)) : fallback;
|
||||
}
|
||||
|
||||
function unique(values, allowed) {
|
||||
return Array.isArray(values) ? Array.from(new Set(values.filter((id) => allowed.includes(id)))) : [];
|
||||
}
|
||||
|
||||
function questDefaults() {
|
||||
return Object.fromEntries(QUEST_IDS.map((id) => [id, { status: "locked", step: 0, count: 0 }]));
|
||||
}
|
||||
|
||||
function fresh(name, palette) {
|
||||
return {
|
||||
version: VERSION,
|
||||
name: validateName(name) || "",
|
||||
palette: validatePalette(palette) || "brass",
|
||||
health: 100,
|
||||
maxHealth: 100,
|
||||
level: 1,
|
||||
xp: 0,
|
||||
attack: 1,
|
||||
position: { area: SPAWN.area, spawn: SPAWN.spawn, x: SPAWN.x, y: SPAWN.y, facing: "north" },
|
||||
checkpoint: { area: SPAWN.area, spawn: SPAWN.spawn, x: SPAWN.x, y: SPAWN.y },
|
||||
discovered: [],
|
||||
complete: false,
|
||||
position: { x: SPAWN.x, y: SPAWN.y },
|
||||
soundEnabled: false
|
||||
story: { stage: "arrival", ending: false, midpointSeen: false },
|
||||
quests: questDefaults(),
|
||||
inventory: [{ id: "living_bookmark", quantity: 1 }, { id: "ink_vial", quantity: 2 }],
|
||||
selectedItem: "ink_vial",
|
||||
equipment: { bookmark: true, boots: false, lens: false },
|
||||
sigils: [],
|
||||
defeatedBosses: [],
|
||||
openedChests: [],
|
||||
solvedPuzzles: [],
|
||||
npcFlags: {},
|
||||
settings: {
|
||||
soundEnabled: false,
|
||||
ambience: 0.35,
|
||||
music: 0.3,
|
||||
effects: 0.65,
|
||||
assist: false,
|
||||
reducedMotion: null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function clamp(value, min, max, fallback) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? Math.min(max, Math.max(min, number)) : fallback;
|
||||
function normalizePosition(candidate, fallback) {
|
||||
const area = candidate && AREA_IDS.includes(candidate.area) ? candidate.area : fallback.area;
|
||||
const areaData = Data && Data.AREAS[area];
|
||||
const defaultSpawn = areaData ? areaData.spawn : { x: fallback.x, y: fallback.y };
|
||||
const width = areaData ? areaData.width : 1448;
|
||||
const height = areaData ? areaData.height : 1086;
|
||||
return {
|
||||
area,
|
||||
spawn: String(candidate && candidate.spawn || fallback.spawn || "entrance").slice(0, 30),
|
||||
x: finite(candidate && candidate.x, defaultSpawn.x, 24, width - 24),
|
||||
y: finite(candidate && candidate.y, defaultSpawn.y, 24, height - 24),
|
||||
facing: candidate && FACES.includes(candidate.facing) ? candidate.facing : "north"
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeInventory(value) {
|
||||
const quantities = new Map();
|
||||
if (Array.isArray(value)) value.forEach((entry) => {
|
||||
if (!entry || !ITEM_IDS.includes(entry.id)) return;
|
||||
const cap = Data && Data.ITEMS[entry.id] && Data.ITEMS[entry.id].stack || 1;
|
||||
quantities.set(entry.id, Math.min(cap, (quantities.get(entry.id) || 0) + Math.floor(finite(entry.quantity, 1, 1, cap))));
|
||||
});
|
||||
if (!quantities.has("living_bookmark")) quantities.set("living_bookmark", 1);
|
||||
return Array.from(quantities, ([id, quantity]) => ({ id, quantity }));
|
||||
}
|
||||
|
||||
function normalizeQuests(value) {
|
||||
const defaults = questDefaults();
|
||||
QUEST_IDS.forEach((id) => {
|
||||
const source = value && value[id];
|
||||
if (!source) return;
|
||||
defaults[id] = {
|
||||
status: ["locked", "active", "complete"].includes(source.status) ? source.status : "locked",
|
||||
step: Math.floor(finite(source.step, 0, 0, 20)),
|
||||
count: Math.floor(finite(source.count, 0, 0, 99))
|
||||
};
|
||||
});
|
||||
return defaults;
|
||||
}
|
||||
|
||||
function normalize(candidate) {
|
||||
@@ -54,61 +123,115 @@
|
||||
const palette = validatePalette(candidate.palette);
|
||||
if (!name || !palette) return null;
|
||||
|
||||
const discovered = Array.isArray(candidate.discovered)
|
||||
? Array.from(new Set(candidate.discovered.filter((id) => LANDMARK_IDS.includes(id))))
|
||||
: [];
|
||||
const discovered = unique(candidate.discovered, LANDMARK_IDS);
|
||||
const sigils = unique(candidate.sigils, LANDMARK_IDS);
|
||||
const xp = Math.floor(finite(candidate.xp, 0, 0, 99999));
|
||||
const level = levelForXp(xp);
|
||||
const expectedMax = 100 + (level >= 2 ? 20 : 0) + (level >= 4 ? 20 : 0);
|
||||
const maxHealth = finite(candidate.maxHealth, expectedMax, expectedMax, expectedMax);
|
||||
const position = normalizePosition(candidate.position, SPAWN);
|
||||
const checkpoint = normalizePosition(candidate.checkpoint, SPAWN);
|
||||
const inventory = normalizeInventory(candidate.inventory);
|
||||
const selected = ITEM_IDS.includes(candidate.selectedItem) ? candidate.selectedItem : "ink_vial";
|
||||
|
||||
return {
|
||||
version: VERSION,
|
||||
name,
|
||||
palette,
|
||||
health: finite(candidate.health, maxHealth, 0, maxHealth),
|
||||
maxHealth,
|
||||
level,
|
||||
xp,
|
||||
attack: sigils.includes("workshop") ? 2 : 1,
|
||||
position,
|
||||
checkpoint: { area: checkpoint.area, spawn: checkpoint.spawn, x: checkpoint.x, y: checkpoint.y },
|
||||
discovered,
|
||||
complete: LANDMARK_IDS.every((id) => discovered.includes(id)),
|
||||
position: {
|
||||
x: clamp(candidate.position && candidate.position.x, 36, 1244, SPAWN.x),
|
||||
y: clamp(candidate.position && candidate.position.y, 36, 924, SPAWN.y)
|
||||
story: {
|
||||
stage: ["arrival", "first_act", "midpoint", "escalation", "vault", "postgame"].includes(candidate.story && candidate.story.stage)
|
||||
? candidate.story.stage : "arrival",
|
||||
ending: candidate.story && candidate.story.ending === true,
|
||||
midpointSeen: candidate.story && candidate.story.midpointSeen === true
|
||||
},
|
||||
soundEnabled: candidate.soundEnabled === true
|
||||
quests: normalizeQuests(candidate.quests),
|
||||
inventory,
|
||||
selectedItem: selected,
|
||||
equipment: {
|
||||
bookmark: true,
|
||||
boots: candidate.equipment && candidate.equipment.boots === true,
|
||||
lens: candidate.equipment && candidate.equipment.lens === true
|
||||
},
|
||||
sigils,
|
||||
defeatedBosses: unique(candidate.defeatedBosses, BOSS_IDS),
|
||||
openedChests: Array.isArray(candidate.openedChests) ? Array.from(new Set(candidate.openedChests.filter((id) => typeof id === "string"))).slice(0, 64) : [],
|
||||
solvedPuzzles: unique(candidate.solvedPuzzles, QUEST_IDS),
|
||||
npcFlags: candidate.npcFlags && typeof candidate.npcFlags === "object" && !Array.isArray(candidate.npcFlags)
|
||||
? Object.fromEntries(Object.entries(candidate.npcFlags).filter(([key, value]) => /^[a-z0-9_-]{1,40}$/.test(key) && ["string", "number", "boolean"].includes(typeof value)).slice(0, 64))
|
||||
: {},
|
||||
settings: {
|
||||
soundEnabled: candidate.settings && candidate.settings.soundEnabled === true,
|
||||
ambience: finite(candidate.settings && candidate.settings.ambience, 0.35, 0, 1),
|
||||
music: finite(candidate.settings && candidate.settings.music, 0.3, 0, 1),
|
||||
effects: finite(candidate.settings && candidate.settings.effects, 0.65, 0, 1),
|
||||
assist: candidate.settings && candidate.settings.assist === true,
|
||||
reducedMotion: candidate.settings && typeof candidate.settings.reducedMotion === "boolean" ? candidate.settings.reducedMotion : null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function migrateV1(candidate) {
|
||||
if (!candidate || candidate.version !== 1) return null;
|
||||
const name = validateName(candidate.name);
|
||||
const palette = validatePalette(candidate.palette);
|
||||
if (!name || !palette) return null;
|
||||
const migrated = fresh(name, palette);
|
||||
migrated.discovered = unique(candidate.discovered, LANDMARK_IDS);
|
||||
migrated.complete = LANDMARK_IDS.every((id) => migrated.discovered.includes(id));
|
||||
migrated.position = normalizePosition({
|
||||
area: "town", spawn: "gate",
|
||||
x: candidate.position && candidate.position.x,
|
||||
y: candidate.position && candidate.position.y,
|
||||
facing: "north"
|
||||
}, SPAWN);
|
||||
migrated.checkpoint = { area: "town", spawn: "gate", x: SPAWN.x, y: SPAWN.y };
|
||||
migrated.settings.soundEnabled = candidate.soundEnabled === true;
|
||||
return normalize(migrated);
|
||||
}
|
||||
|
||||
function parse(raw) {
|
||||
if (!raw) return null;
|
||||
try {
|
||||
return normalize(JSON.parse(raw));
|
||||
const candidate = JSON.parse(raw);
|
||||
return candidate.version === 1 ? migrateV1(candidate) : normalize(candidate);
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function discover(state, landmarkId) {
|
||||
function levelForXp(xp) {
|
||||
let level = 1;
|
||||
LEVELS.forEach((threshold, index) => { if (xp >= threshold) level = index + 1; });
|
||||
return Math.min(5, level);
|
||||
}
|
||||
|
||||
function withPosition(state, area, x, y, facing, spawn) {
|
||||
const current = normalize(state);
|
||||
if (!current || !LANDMARK_IDS.includes(landmarkId)) return current;
|
||||
if (!current.discovered.includes(landmarkId)) current.discovered.push(landmarkId);
|
||||
current.complete = LANDMARK_IDS.every((id) => current.discovered.includes(id));
|
||||
if (!current) return null;
|
||||
current.position = normalizePosition({ area, x, y, facing, spawn }, current.position);
|
||||
return current;
|
||||
}
|
||||
|
||||
function withPosition(state, x, y) {
|
||||
function withCheckpoint(state, area, spawn, x, y) {
|
||||
const current = normalize(state);
|
||||
if (!current) return null;
|
||||
current.position.x = clamp(x, 36, 1244, SPAWN.x);
|
||||
current.position.y = clamp(y, 36, 924, SPAWN.y);
|
||||
const next = normalizePosition({ area, spawn, x, y }, SPAWN);
|
||||
current.checkpoint = { area: next.area, spawn: next.spawn, x: next.x, y: next.y };
|
||||
return current;
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
VERSION,
|
||||
STORAGE_KEY,
|
||||
PALETTES,
|
||||
LANDMARK_IDS,
|
||||
SPAWN,
|
||||
validateName,
|
||||
validatePalette,
|
||||
fresh,
|
||||
normalize,
|
||||
parse,
|
||||
discover,
|
||||
withPosition
|
||||
VERSION, STORAGE_KEY, LEGACY_KEY, PALETTES, LANDMARK_IDS, QUEST_IDS, ITEM_IDS, AREA_IDS, BOSS_IDS, LEVELS, SPAWN,
|
||||
validateName, validatePalette, fresh, normalize, parse, migrateV1, normalizePosition, normalizeInventory,
|
||||
normalizeQuests, levelForXp, withPosition, withCheckpoint
|
||||
});
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user