(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: Data.MAPS.village.spawns.start.x, y: Data.MAPS.village.spawns.start.y, facing: "north" }); 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, introSeen: false, playTimeSeconds: 0, settings: { soundEnabled: false, master: 0.8, music: 0.45, effects: 0.7, voice: 0.85, narrationEnabled: true, subtitles: true, 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, // Existing v1 saves predate the cinematic and must continue directly. introSeen: candidate.introSeen !== false, 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), voice: finite(candidate.settings && candidate.settings.voice, 0.85, 0, 1), narrationEnabled: !(candidate.settings && candidate.settings.narrationEnabled === false), subtitles: !(candidate.settings && candidate.settings.subtitles === false), 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 }); }));