Refactor org web platform and remove legacy code
All checks were successful
Build Org Website / build (push) Successful in 39s
All checks were successful
Build Org Website / build (push) Successful in 39s
This commit is contained in:
189
assets/scripts/pages/princess-lima-state.js
Normal file
189
assets/scripts/pages/princess-lima-state.js
Normal file
@@ -0,0 +1,189 @@
|
||||
(function (root, factory) {
|
||||
"use strict";
|
||||
const data = root.PrincessLimaData || (typeof require === "function" ? require("./princess-lima-data.js") : null);
|
||||
const api = factory(data);
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
root.PrincessLimaState = api;
|
||||
}(typeof globalThis !== "undefined" ? globalThis : this, function (Data) {
|
||||
"use strict";
|
||||
|
||||
const VERSION = 1;
|
||||
const STORAGE_KEY = "zxh_princess_lima_rpg_v1";
|
||||
const APPEARANCES = Object.freeze(["azure", "ember", "pine"]);
|
||||
const FACES = Object.freeze(["north", "south", "east", "west"]);
|
||||
const START = Object.freeze({ region: "village", spawn: "start", x: 160, y: 570, facing: "east" });
|
||||
|
||||
function validName(value) {
|
||||
const name = String(value || "").trim().replace(/\s+/g, " ");
|
||||
return name.length >= 1 && name.length <= 20 ? name : 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(Data.QUEST_IDS.map((id) => [id, { status: "locked", count: 0, rewarded: false }]));
|
||||
}
|
||||
|
||||
function fresh(name, appearance) {
|
||||
return {
|
||||
version: VERSION,
|
||||
player: { name: validName(name) || "", appearance: APPEARANCES.includes(appearance) ? appearance : "azure" },
|
||||
health: 100,
|
||||
maxHealth: 100,
|
||||
attack: 1,
|
||||
defence: 0,
|
||||
region: START.region,
|
||||
position: { spawn: START.spawn, x: START.x, y: START.y, facing: START.facing },
|
||||
checkpoint: { region: START.region, spawn: START.spawn, x: START.x, y: START.y },
|
||||
chapter: 1,
|
||||
story: "arrival",
|
||||
quests: questDefaults(),
|
||||
inventory: [{ id: "healing_tonic", quantity: 2 }],
|
||||
equipment: { weapon: null, armour: null, boots: null, charm: null },
|
||||
flags: {},
|
||||
solvedPuzzles: [],
|
||||
defeatedBosses: [],
|
||||
openedChests: [],
|
||||
unlockedRoutes: [],
|
||||
rescued: false,
|
||||
playTimeSeconds: 0,
|
||||
settings: {
|
||||
soundEnabled: false,
|
||||
master: 0.8,
|
||||
music: 0.45,
|
||||
effects: 0.7,
|
||||
reducedMotion: null,
|
||||
screenShake: true,
|
||||
highContrast: false,
|
||||
textSpeed: "normal"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePosition(regionId, candidate, fallback) {
|
||||
const region = Data.MAPS[regionId] || Data.MAPS.village;
|
||||
const named = region.spawns[candidate && candidate.spawn] || region.spawns[fallback.spawn] || Object.values(region.spawns)[0];
|
||||
return {
|
||||
spawn: String(candidate && candidate.spawn || fallback.spawn).slice(0, 32),
|
||||
x: finite(candidate && candidate.x, named.x, 24, Data.WIDTH - 24),
|
||||
y: finite(candidate && candidate.y, named.y, 24, Data.HEIGHT - 24),
|
||||
facing: candidate && FACES.includes(candidate.facing) ? candidate.facing : fallback.facing || "south"
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeInventory(value) {
|
||||
const quantities = new Map();
|
||||
if (Array.isArray(value)) value.forEach((entry) => {
|
||||
const item = entry && Data.ITEMS[entry.id];
|
||||
if (!item) return;
|
||||
const cap = item.stack || 1;
|
||||
quantities.set(entry.id, Math.min(cap, (quantities.get(entry.id) || 0) + Math.floor(finite(entry.quantity, 1, 1, cap))));
|
||||
});
|
||||
return Array.from(quantities, ([id, quantity]) => ({ id, quantity }));
|
||||
}
|
||||
|
||||
function normalizeQuests(value) {
|
||||
const quests = questDefaults();
|
||||
Data.QUEST_IDS.forEach((id) => {
|
||||
const source = value && value[id];
|
||||
if (!source) return;
|
||||
quests[id] = {
|
||||
status: ["locked", "active", "complete"].includes(source.status) ? source.status : "locked",
|
||||
count: Math.floor(finite(source.count, 0, 0, 99)),
|
||||
rewarded: source.rewarded === true
|
||||
};
|
||||
});
|
||||
return quests;
|
||||
}
|
||||
|
||||
function normalize(candidate) {
|
||||
if (!candidate || candidate.version !== VERSION) return null;
|
||||
const name = validName(candidate.player && candidate.player.name);
|
||||
const appearance = candidate.player && APPEARANCES.includes(candidate.player.appearance) ? candidate.player.appearance : null;
|
||||
if (!name || !appearance) return null;
|
||||
const region = Data.MAP_IDS.includes(candidate.region) ? candidate.region : START.region;
|
||||
const position = normalizePosition(region, candidate.position, START);
|
||||
const checkpointRegion = candidate.checkpoint && Data.MAP_IDS.includes(candidate.checkpoint.region)
|
||||
? candidate.checkpoint.region : START.region;
|
||||
const checkpointPosition = normalizePosition(checkpointRegion, candidate.checkpoint, START);
|
||||
const inventory = normalizeInventory(candidate.inventory);
|
||||
const has = (id) => inventory.some((entry) => entry.id === id);
|
||||
const equipment = {
|
||||
weapon: has("tempered_sword") ? "tempered_sword" : has("village_sword") ? "village_sword" : null,
|
||||
armour: has("buckler") ? "buckler" : null,
|
||||
boots: has("trail_boots") ? "trail_boots" : null,
|
||||
charm: has("forest_charm") ? "forest_charm" : null
|
||||
};
|
||||
const attack = equipment.weapon ? Data.ITEMS[equipment.weapon].attack : 1;
|
||||
const defence = equipment.armour ? Data.ITEMS[equipment.armour].defence : 0;
|
||||
const maxHealth = finite(candidate.maxHealth, 100, 100, 160);
|
||||
return {
|
||||
version: VERSION,
|
||||
player: { name, appearance },
|
||||
health: finite(candidate.health, maxHealth, 0, maxHealth),
|
||||
maxHealth,
|
||||
attack,
|
||||
defence,
|
||||
region,
|
||||
position,
|
||||
checkpoint: { region: checkpointRegion, spawn: checkpointPosition.spawn, x: checkpointPosition.x, y: checkpointPosition.y },
|
||||
chapter: Math.floor(finite(candidate.chapter, 1, 1, 4)),
|
||||
story: String(candidate.story || "arrival").slice(0, 48),
|
||||
quests: normalizeQuests(candidate.quests),
|
||||
inventory,
|
||||
equipment,
|
||||
flags: candidate.flags && typeof candidate.flags === "object" && !Array.isArray(candidate.flags)
|
||||
? Object.fromEntries(Object.entries(candidate.flags).filter(([key, val]) => /^[a-z0-9_-]{1,48}$/.test(key) && typeof val === "boolean").slice(0, 96))
|
||||
: {},
|
||||
solvedPuzzles: unique(candidate.solvedPuzzles, ["forest_stones", "ruin_braziers", "bridge_winches", "shadow_wards", "sun_pedestals"]),
|
||||
defeatedBosses: unique(candidate.defeatedBosses, ["briar_wolf", "stone_guardian", "captain", "malrec"]),
|
||||
openedChests: Array.isArray(candidate.openedChests) ? Array.from(new Set(candidate.openedChests.filter((id) => typeof id === "string"))).slice(0, 64) : [],
|
||||
unlockedRoutes: unique(candidate.unlockedRoutes, ["village_defended", "guide_found", "ruins_complete", "briar_defeated", "bridge_repaired", "guardian_defeated", "emblem_found", "wards_broken", "malrec_defeated"]),
|
||||
rescued: candidate.rescued === true,
|
||||
playTimeSeconds: Math.floor(finite(candidate.playTimeSeconds, 0, 0, 999999)),
|
||||
settings: {
|
||||
soundEnabled: candidate.settings && candidate.settings.soundEnabled === true,
|
||||
master: finite(candidate.settings && candidate.settings.master, 0.8, 0, 1),
|
||||
music: finite(candidate.settings && candidate.settings.music, 0.45, 0, 1),
|
||||
effects: finite(candidate.settings && candidate.settings.effects, 0.7, 0, 1),
|
||||
reducedMotion: candidate.settings && typeof candidate.settings.reducedMotion === "boolean" ? candidate.settings.reducedMotion : null,
|
||||
screenShake: !(candidate.settings && candidate.settings.screenShake === false),
|
||||
highContrast: candidate.settings && candidate.settings.highContrast === true,
|
||||
textSpeed: candidate.settings && ["slow", "normal", "fast", "instant"].includes(candidate.settings.textSpeed) ? candidate.settings.textSpeed : "normal"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function parse(raw) {
|
||||
try { return raw ? normalize(JSON.parse(raw)) : null; } catch (_error) { return null; }
|
||||
}
|
||||
|
||||
function withPosition(state, region, spawn, x, y, facing) {
|
||||
const next = normalize(state);
|
||||
if (!next || !Data.MAPS[region]) return next;
|
||||
next.region = region;
|
||||
next.position = normalizePosition(region, { spawn, x, y, facing }, START);
|
||||
return normalize(next);
|
||||
}
|
||||
|
||||
function withCheckpoint(state, region, spawn, x, y) {
|
||||
const next = normalize(state);
|
||||
if (!next || !Data.MAPS[region]) return next;
|
||||
const point = normalizePosition(region, { spawn, x, y, facing: "south" }, START);
|
||||
next.checkpoint = { region, spawn: point.spawn, x: point.x, y: point.y };
|
||||
return normalize(next);
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
VERSION, STORAGE_KEY, APPEARANCES, FACES, START,
|
||||
validName, fresh, normalize, parse, normalizeInventory, normalizeQuests,
|
||||
normalizePosition, withPosition, withCheckpoint
|
||||
});
|
||||
}));
|
||||
Reference in New Issue
Block a user