(function () { "use strict"; const Data = window.PrincessLimaV2Data; const State = window.PrincessLimaV2State; const Scenes = window.PrincessLimaV2Scenes; const UI = window.PrincessLimaV2UI; const Audio = window.PrincessLimaAudio; const controller = { root: null, game: null, scene: null, state: null, audio: null, ui: null, inputLock: null, uiTokens: new Map(), lastSaveAt: 0, systemReducedMotion: window.matchMedia("(prefers-reduced-motion: reduce)").matches, reducedMotion() { return this.state && typeof this.state.settings.reducedMotion === "boolean" ? this.state.settings.reducedMotion : this.systemReducedMotion; } }; document.addEventListener("DOMContentLoaded", init, { once: true }); function init() { const root = document.querySelector("[data-princess-lima-rpg]"); if (!root || !Data || !State || !Scenes || !UI || !Audio || !window.Phaser || !window.PrincessLimaV2Maps) return; controller.root = root; const loaded = State.load(localStorage); controller.state = loaded.state; controller.audio = Audio.create(() => controller.state); controller.inputLock = window.PrincessLimaV2Systems.createInputLock((locked) => root.classList.toggle("is-input-locked", locked)); controller.ui = UI.create(root, { getState: () => controller.state, setState, applyEffects: (effects) => setState(State.applyEffects(controller.state, effects)), recordDialogue, lock, beginDialogue: (_id, target) => controller.scene && controller.scene.beginDialogue(target), frameDialogue: (node) => controller.scene && controller.scene.frameDialogue(node), endDialogue: () => controller.scene && controller.scene.endDialogue(), focusGame, useTonic, setting: updateSetting, replayIntro, respawn, returnToMenu }); bindPage(); startEngine(); if (loaded.migrated) queueMicrotask(() => status("Legacy progress imported safely. Your original v1 save remains untouched.")); window.addEventListener("error", (event) => status(`The game recovered from a problem: ${event.message}`)); document.addEventListener("visibilitychange", () => { if (document.hidden) { controller.inputLock.acquire("hidden-tab"); controller.audio.suspend(); } else { controller.inputLock.releaseReason("hidden-tab"); controller.audio.resume(); } }); window.PrincessLimaV2Controller = controller; } function startEngine() { try { controller.game = new Phaser.Game({ type: Phaser.AUTO, width: Data.WIDTH, height: Data.HEIGHT, parent: "princess-lima-game", pixelArt: true, roundPixels: true, backgroundColor: "#080a12", physics: { default: "arcade", arcade: { gravity: { x: 0, y: 0 }, debug: location.search.includes("collisionDebug=1") } }, scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH, width: Data.WIDTH, height: Data.HEIGHT }, render: { antialias: false, pixelArt: true, powerPreference: "high-performance" }, scene: Scenes.createSceneClasses(controller) }); } catch (error) { fatal(error.message); } } function bindPage() { const root = controller.root; root.querySelector("[data-lima-new]").addEventListener("click", openSetup); root.querySelector("[data-lima-continue]").addEventListener("click", () => { if (!controller.state) return; controller.audio.unlock(); controller.state.introSeen ? beginAdventure() : startIntro(false); }); root.querySelector("[data-lima-replay-intro]").addEventListener("click", replayIntro); root.querySelector("[data-lima-menu-settings]").addEventListener("click", () => controller.state ? controller.ui.openPanel("settings") : status("Create a traveller first; all accessibility settings remain available during play.")); root.querySelector("[data-lima-credits]").addEventListener("click", () => controller.ui.openPanel("credits")); root.querySelector("[data-lima-setup-form]").addEventListener("submit", submitSetup); root.querySelector("[data-lima-setup-cancel]").addEventListener("click", closeSetup); root.querySelector("[data-lima-pause]").addEventListener("click", () => controller.ui.openPanel("pause")); root.querySelector("[data-lima-inventory]").addEventListener("click", () => controller.ui.openPanel("inventory")); root.querySelector("[data-lima-quests]").addEventListener("click", () => controller.ui.openPanel("quests")); root.querySelector("[data-lima-sound]").addEventListener("click", toggleSound); root.querySelector("[data-lima-fullscreen]").addEventListener("click", toggleFullscreen); document.addEventListener("fullscreenchange", updateFullscreenButton); } function ready() { controller.root.dataset.ready = "true"; controller.root.querySelector("[data-lima-loading]").hidden = true; const button = controller.root.querySelector("[data-lima-continue]"); button.disabled = !controller.state; button.textContent = controller.state ? `Continue · Chapter ${controller.state.chapter}` : "Continue"; } function showMenu() { controller.inputLock.clear(); controller.root.querySelector("[data-lima-menu]").hidden = false; controller.root.querySelector("[data-lima-hud]").hidden = true; controller.root.querySelector("[data-lima-status-stack]").hidden = true; requestAnimationFrame(() => controller.root.querySelector("[data-lima-menu] button").focus()); } function hideMenu() { controller.root.querySelector("[data-lima-menu]").hidden = true; } function openSetup() { hideMenu(); controller.root.querySelector("[data-lima-setup]").hidden = false; controller.root.querySelector("[data-lima-setup] input").focus(); } function closeSetup() { controller.root.querySelector("[data-lima-setup]").hidden = true; showMenu(); } function submitSetup(event) { event.preventDefault(); const form = event.currentTarget; const name = State.validName(form.elements.name.value); const appearance = form.elements.appearance.value; if (!name || !State.APPEARANCES.includes(appearance)) { controller.root.querySelector("[data-lima-setup-error]").textContent = "Enter a name from 1 to 20 characters and choose a cloak."; return; } controller.state = State.fresh(name, appearance); controller.audio.unlock(); save(); controller.root.querySelector("[data-lima-setup]").hidden = true; startIntro(false); } function startIntro(replay) { hideMenu(); controller.root.querySelector("[data-lima-hud]").hidden = true; controller.game.scene.start("LimaIntro", { replay }); } function replayIntro() { controller.ui.close(); startIntro(true); } function beginAdventure(sourceScene) { hideMenu(); controller.audio.unlock(); const region = controller.state.region || "village"; if (sourceScene && sourceScene.scene) sourceScene.scene.start("LimaWorld", { region }); else controller.game.scene.start("LimaWorld", { region }); focusGame(); } function showHud() { controller.root.querySelector("[data-lima-hud]").hidden = false; controller.root.querySelector("[data-lima-status-stack]").hidden = false; } function updateHud() { if (!controller.state) return; const state = controller.state; controller.root.querySelector("[data-lima-player]").textContent = state.player.name; controller.root.querySelector("[data-lima-chapter]").textContent = `Chapter ${state.chapter}`; controller.root.querySelector("[data-lima-health]").textContent = `${Math.ceil(state.health)} / ${state.maxHealth}`; controller.root.querySelector(".lima-hud__healthbar i").style.width = `${state.health / state.maxHealth * 100}%`; controller.root.querySelector("[data-lima-weapon]").textContent = state.equipment.weapon ? Data.ITEMS[state.equipment.weapon].name : "Unarmed"; controller.root.querySelector("[data-lima-armour]").textContent = state.equipment.armour ? Data.ITEMS[state.equipment.armour].name : "None"; controller.root.querySelector("[data-lima-selected-item]").textContent = `Healing Tonic ×${State.quantity(state, "healing_tonic")}`; controller.root.querySelector("[data-lima-currency]").textContent = State.quantity(state, "moon_coin"); controller.root.querySelector("[data-lima-region]").textContent = Data.MAP_NAMES[state.region]; controller.root.querySelector("[data-lima-objective]").textContent = window.PrincessLimaV2Systems.currentObjective(state); controller.root.classList.toggle("is-high-contrast", state.settings.highContrast); } function setState(next, message) { const normalized = State.normalize(next); if (!normalized) return; controller.state = normalized; save(); updateHud(); if (message) status(message); } function recordDialogue(id) { if (!controller.state.dialogueHistory.includes(id)) controller.state.dialogueHistory.push(id); save(); } function save() { if (!controller.state) return; try { localStorage.setItem(State.STORAGE_KEY, JSON.stringify(State.normalize(controller.state))); controller.lastSaveAt = Date.now(); controller.root.dataset.saved = "true"; setTimeout(() => { if (controller.root) controller.root.dataset.saved = "false"; }, 900); } catch (_error) { status("Saving is unavailable in this browser session."); } } function travel(region, spawn) { const safe = State.safeSpawn(region, spawn); controller.state.region = region; controller.state.position = Object.assign({}, safe, { facing: "south" }); controller.state.checkpoint = safe; save(); controller.audio.play("door"); controller.game.scene.start("LimaWorld", { region }); } function respawn() { const checkpoint = controller.state.checkpoint; controller.state.region = checkpoint.region; controller.state.position = Object.assign({}, checkpoint, { facing: "south" }); controller.state.health = Math.max(Math.ceil(controller.state.maxHealth * 0.6), 1); save(); controller.game.scene.start("LimaWorld", { region: checkpoint.region }); } function useTonic() { const result = State.useTonic(controller.state); if (!result.used) return status(controller.state.health >= controller.state.maxHealth ? "Health is already full." : "No Healing Tonics remain."); setState(result.state, "Healing Tonic used."); controller.audio.play("pickup"); } function updateSetting(key, value) { if (!controller.state || !(key in controller.state.settings)) return; controller.state.settings[key] = value; controller.state = State.normalize(controller.state); save(); controller.audio.apply(); updateHud(); if (["effectsQuality", "particles", "lighting"].includes(key) && controller.scene) status("This visual setting applies fully after the next region transition."); } function toggleSound() { if (!controller.state) return; controller.state.settings.soundEnabled = !controller.state.settings.soundEnabled; controller.audio.unlock(); controller.audio.apply(); save(); const button = controller.root.querySelector("[data-lima-sound]"); button.textContent = controller.state.settings.soundEnabled ? "Sound On" : "Sound Muted"; button.setAttribute("aria-pressed", String(controller.state.settings.soundEnabled)); } function toggleFullscreen() { if (document.fullscreenElement) document.exitFullscreen(); else controller.root.requestFullscreen().catch(() => status("Fullscreen is unavailable in this browser.")); } function updateFullscreenButton() { const button = controller.root.querySelector("[data-lima-fullscreen]"); button.setAttribute("aria-pressed", String(Boolean(document.fullscreenElement))); button.textContent = document.fullscreenElement ? "Exit Fullscreen" : "Fullscreen"; } function lock(value, reason) { if (value) { if (!controller.uiTokens.has(reason)) controller.uiTokens.set(reason, controller.inputLock.acquire(reason)); } else if (controller.uiTokens.has(reason)) { controller.inputLock.release(controller.uiTokens.get(reason)); controller.uiTokens.delete(reason); } else controller.inputLock.releaseReason(reason); } function frameDialogue(node) { if (controller.scene) controller.scene.frameDialogue(node); } function focusGame() { const game = controller.root.querySelector("[data-lima-game]"); if (game) game.focus({ preventScroll: true }); } function prompt(text) { controller.root.querySelector("[data-lima-prompt]").textContent = text || "Explore · speak · protect · discover"; } function status(message) { const element = controller.root && controller.root.querySelector("[data-lima-status]"); if (element) element.textContent = message; } function boss(name, health, maxHealth, phase) { const container = controller.root.querySelector("[data-lima-boss]"); container.hidden = !name; if (!name) return; container.querySelector("[data-lima-boss-name]").textContent = `${name} · Phase ${phase}`; container.querySelector("i").style.width = `${Math.max(0, health) / maxHealth * 100}%`; } function returnToMenu() { controller.ui.close(); controller.game.scene.start("LimaTitle"); } function fatal(message) { if (!controller.root) return; controller.root.querySelector("[data-lima-loading]").innerHTML = `The road could not open.${escapeHtml(message)}Return to Play`; } function escapeHtml(value) { return String(value).replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char])); } Object.assign(controller, { ready, showMenu, hideMenu, beginAdventure, showHud, updateHud, setState, save, travel, prompt, status, boss, toggleFullscreen, fatal, frameDialogue, root: null }); }());