(function () { "use strict"; const Data = window.PrincessLimaData; const State = window.PrincessLimaState; const Systems = window.PrincessLimaSystems; const Scenes = window.PrincessLimaScenes; const UI = window.PrincessLimaUI; const Audio = window.PrincessLimaAudio; const Intro = window.PrincessLimaIntro; const controller = { root: null, game: null, scene: null, state: null, audio: null, intro: null, ui: null, locked: true, transitioning: false, lastSaveAt: 0, systemReducedMotion: window.matchMedia("(prefers-reduced-motion: reduce)").matches, getState: () => controller.state, reducedMotion: () => controller.state && controller.state.settings.reducedMotion !== null ? controller.state.settings.reducedMotion : controller.systemReducedMotion, debugCollision: false, devWarn: (message) => { if (["localhost", "127.0.0.1"].includes(window.location.hostname)) console.warn(`[Princess Lima RPG] ${message}`); } }; document.addEventListener("DOMContentLoaded", init, { once: true }); function init() { const root = document.querySelector("[data-princess-lima-rpg]"); if (!root || !Data || !State || !Systems || !Scenes || !UI || !Audio || !Intro || !window.PrincessLimaMaps || !window.Phaser) return gracefulFailure(); controller.root = root; controller.state = loadState(); controller.audio = Audio.create(() => controller.state); controller.debugCollision = localDebug(); controller.ui = UI.create(root, { getState: () => controller.state, onLock: (value) => { controller.locked = value; }, onUseTonic: useTonic, onSetting: updateSetting, onFullscreen: toggleFullscreen, onReplayIntro: () => { controller.ui.close(); startIntro(true); }, onReset: resetSave, onRespawn: respawn }); controller.intro = Intro.create(root, { onLock: (value) => { controller.locked = value; }, onAudioUnlock: () => controller.audio.unlock(), onComplete: completeIntro, playVoice: (key) => controller.audio.playVoice(key), stopVoice: () => controller.audio.stopVoice(), playEffect: (key, volume) => controller.audio.play(key, volume), narrationEnabled: () => !controller.state || controller.state.settings.narrationEnabled, subtitlesEnabled: () => !controller.state || controller.state.settings.subtitles, soundEnabled: () => Boolean(controller.state && controller.state.settings.soundEnabled), toggleSound, status }); bindInterface(); startEngine(); window.addEventListener("error", (event) => { controller.devWarn(event.message); status("The game recovered from an unexpected problem. Open the pause menu if controls do not respond."); }); document.addEventListener("visibilitychange", () => { if (document.hidden) controller.audio.suspend(); else controller.audio.resume(); }); } function loadState() { try { const raw = localStorage.getItem(State.STORAGE_KEY); if (!raw) return null; const parsed = State.parse(raw); if (!parsed) { queueStatus("The old save was invalid, so it was ignored safely."); return null; } const debugRegion = localRegionPreview(); if (debugRegion) { const map = Data.MAPS[debugRegion]; const requestedSpawn = new URLSearchParams(window.location.search).get("spawn"); const spawn = map.spawns[requestedSpawn] ? [requestedSpawn, map.spawns[requestedSpawn]] : Object.entries(map.spawns)[0]; return State.withPosition(parsed, debugRegion, spawn[0], spawn[1].x, spawn[1].y, "south"); } const safe = Systems.nearestSafeSpawn(parsed, parsed.region, parsed.position.x, parsed.position.y); return State.withPosition(parsed, safe.region, safe.spawn, safe.x, safe.y, parsed.position.facing); } catch (_error) { queueStatus("Local saving is unavailable. You can still play this session."); return null; } } function startEngine() { try { const classes = Scenes.createSceneClasses(controller); controller.game = new Phaser.Game({ type: Phaser.AUTO, width: Data.WIDTH, height: Data.HEIGHT, parent: "princess-lima-game", pixelArt: true, roundPixels: true, backgroundColor: "#0a0b12", physics: { default: "arcade", arcade: { debug: localDebug(), gravity: { x: 0, y: 0 } } }, scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH, width: Data.WIDTH, height: Data.HEIGHT }, scene: classes, render: { antialias: false, pixelArt: true } }); } catch (error) { controller.devWarn(error.message); gracefulFailure("The game engine could not start. You can return safely to the website."); } } function ready() { controller.root.dataset.ready = "true"; const continueButton = controller.root.querySelector("[data-lima-continue]"); continueButton.disabled = !controller.state; continueButton.textContent = controller.state ? `Continue · Chapter ${controller.state.chapter}` : "Continue"; controller.root.querySelector("[data-lima-loading]").hidden = true; controller.root.querySelector("[data-lima-menu]").hidden = false; flushQueuedStatus(); } function bindInterface() { const root = controller.root; root.querySelector("[data-lima-new]").addEventListener("click", () => openSetup()); root.querySelector("[data-lima-continue]").addEventListener("click", () => { if (controller.state) { controller.audio.unlock(); if (controller.state.introSeen) beginAdventure(); else startIntro(false); } }); root.querySelector("[data-lima-replay-intro]").addEventListener("click", () => startIntro(true)); root.querySelector("[data-lima-menu-settings]").addEventListener("click", () => { if (controller.state) openPanel("settings"); else controller.ui.show("settings-help", "Settings", "

Create a traveller to save audio and accessibility preferences. The introduction always includes subtitles and can be muted or skipped.

"); }); root.querySelector("[data-lima-credits]").addEventListener("click", () => controller.ui.show( "credits", "Credits", "

Designed and built for zainezq.com. Original fantasy artwork and locally generated audio. Powered by locally vendored Phaser.

" )); root.querySelector("[data-lima-setup-form]").addEventListener("submit", submitSetup); root.querySelector("[data-lima-setup-cancel]").addEventListener("click", closeSetup); bindButton("[data-lima-pause]", () => openPanel("pause")); bindButton("[data-lima-inventory]", () => openPanel("inventory")); bindButton("[data-lima-quests]", () => openPanel("quests")); bindButton("[data-lima-sound]", toggleSound); bindButton("[data-lima-fullscreen]", toggleFullscreen); document.addEventListener("fullscreenchange", updateFullscreenButton); } function bindButton(selector, handler) { controller.root.querySelectorAll(selector).forEach((button) => button.addEventListener("click", handler)); } function openSetup() { controller.root.querySelector("[data-lima-menu]").hidden = true; controller.root.querySelector("[data-lima-setup]").hidden = false; controller.root.querySelector("[data-lima-setup] input[name=name]").focus(); } function closeSetup() { controller.root.querySelector("[data-lima-setup]").hidden = true; controller.root.querySelector("[data-lima-menu]").hidden = false; } function submitSetup(event) { event.preventDefault(); const form = event.currentTarget; const name = State.validName(form.elements.name.value); const appearance = form.elements.appearance.value; const error = controller.root.querySelector("[data-lima-setup-error]"); if (!name || !State.APPEARANCES.includes(appearance)) { error.textContent = "Enter a name from 1 to 20 characters and choose an appearance."; return; } controller.state = State.fresh(name, appearance); save(); closeSetup(); startIntro(false); } function beginAdventure() { controller.audio.unlock(); controller.root.querySelector("[data-lima-menu]").hidden = true; controller.root.querySelector("[data-lima-intro]").hidden = true; controller.root.querySelector("[data-lima-hud]").hidden = false; controller.root.querySelector("[data-lima-status-stack]").hidden = false; controller.locked = false; controller.game.scene.start("LimaWorld", { region: controller.state.region }); focusGame(); } function startIntro(replay) { controller.audio.unlock(); controller.root.querySelector("[data-lima-menu]").hidden = true; controller.root.querySelector("[data-lima-hud]").hidden = true; controller.root.querySelector("[data-lima-status-stack]").hidden = true; controller.intro.start({ replay }); } function completeIntro(result) { if (controller.state && !result.replay) { controller.state.introSeen = true; controller.state = State.normalize(controller.state); save(); beginAdventure(); return; } controller.locked = true; controller.root.querySelector("[data-lima-menu]").hidden = false; controller.root.querySelector("[data-lima-menu] button").focus(); } function travel(region, spawn) { if (controller.transitioning || !Data.MAPS[region]) return; controller.transitioning = true; persistPosition(false); const point = Data.MAPS[region].spawns[spawn] || Object.values(Data.MAPS[region].spawns)[0]; let next = State.withPosition(controller.state, region, spawn, point.x, point.y, "south"); next = State.withCheckpoint(next, region, spawn, point.x, point.y); setState(next, `Travelling to ${Data.MAPS[region].name}…`); controller.audio.play("door"); try { controller.scene.scene.restart({ region }); } catch (error) { controller.devWarn(`Transition recovered: ${error.message}`); controller.transitioning = false; controller.game.scene.start("LimaWorld", { region }); } } function reloadRegion() { if (controller.scene) controller.scene.scene.restart({ region: controller.state.region }); } function persistPosition(checkpoint) { if (!controller.scene || !controller.scene.player || !controller.state) return; const safe = Systems.nearestSafeSpawn(controller.state, controller.scene.regionId, controller.scene.player.x, controller.scene.player.y); let next = State.withPosition(controller.state, safe.region, safe.spawn, safe.x, safe.y, controller.scene.facing); if (checkpoint) next = State.withCheckpoint(next, safe.region, safe.spawn, safe.x, safe.y); controller.state = next; save(); } function setState(next, message) { const normalized = State.normalize(next); if (!normalized) return; controller.state = normalized; save(); updateHud(); controller.audio.apply(); if (message) status(message); } function save() { if (!controller.state) return; try { localStorage.setItem(State.STORAGE_KEY, JSON.stringify(controller.state)); } catch (_error) { /* Session play remains available. */ } } function resetSave() { try { localStorage.removeItem(State.STORAGE_KEY); } catch (_error) { /* Nothing else to clear. */ } controller.state = null; window.location.reload(); } function respawn() { controller.state = Systems.respawn(controller.state); save(); controller.ui.close(); controller.locked = false; controller.game.scene.start("LimaWorld", { region: controller.state.region }); } function useTonic() { if (!controller.state) return; const result = Systems.useItem(controller.state, "healing_tonic"); if (!result.used) return status(controller.state.health >= controller.state.maxHealth ? "Health is already full." : "No Healing Tonics remain."); setState(result.state, `Healing Tonic restores ${result.amount} health.`); } function updateSetting(key, value) { if (!controller.state || !(key in controller.state.settings)) return; controller.state.settings[key] = value; setState(controller.state); controller.root.classList.toggle("is-high-contrast", controller.state.settings.highContrast); } function toggleSound() { if (!controller.state) return; controller.audio.unlock(); controller.state.settings.soundEnabled = !controller.state.settings.soundEnabled; setState(controller.state, controller.state.settings.soundEnabled ? "Audio enabled." : "Audio muted."); } async function toggleFullscreen() { const shell = controller.root; try { if (!document.fullscreenElement) await shell.requestFullscreen(); else await document.exitFullscreen(); } catch (_error) { status("Fullscreen is not available in this browser."); } } function updateFullscreenButton() { const button = controller.root.querySelector("[data-lima-fullscreen]"); const active = Boolean(document.fullscreenElement); button.setAttribute("aria-pressed", String(active)); button.textContent = active ? "Exit Fullscreen" : "Fullscreen"; if (!active) focusGame(); } function openDialogue(id, done) { controller.ui.openDialogue(id, done); } function openPanel(kind) { if (controller.state) controller.ui.openPanel(kind, controller.state); } function gameOver() { controller.locked = true; controller.ui.gameOver(controller.state); } function openEnding() { controller.locked = true; controller.ui.ending(controller.state); } function updateHud() { if (!controller.state || !controller.root) return; text("[data-lima-player]", controller.state.player.name); text("[data-lima-health]", `${Math.ceil(controller.state.health)} / ${controller.state.maxHealth}`); text("[data-lima-region]", Data.MAPS[controller.state.region].name); text("[data-lima-chapter]", `Chapter ${controller.state.chapter}`); text("[data-lima-objective]", Systems.currentObjective(controller.state)); text("[data-lima-tonics]", `Tonic ×${Systems.quantity(controller.state, "healing_tonic")}`); text("[data-lima-weapon]", controller.state.equipment.weapon ? Data.ITEMS[controller.state.equipment.weapon].name : "Unarmed"); text("[data-lima-armour]", controller.state.equipment.armour ? Data.ITEMS[controller.state.equipment.armour].name : "None"); text("[data-lima-selected-item]", `Healing Tonic ×${Systems.quantity(controller.state, "healing_tonic")}`); text("[data-lima-currency]", String(Systems.quantity(controller.state, "moon_coin"))); text("[data-lima-sound]", controller.state.settings.soundEnabled ? "Sound On" : "Sound Muted"); const healthBar = controller.root.querySelector(".lima-hud__healthbar i"); if (healthBar) healthBar.style.width = `${controller.state.health / controller.state.maxHealth * 100}%`; controller.root.classList.toggle("is-high-contrast", controller.state.settings.highContrast); } function cycleItem() { status("Healing Tonic selected. Press Q to use it."); } function prompt(message) { text("[data-lima-prompt]", message || "Explore the road ahead."); } function status(message) { text("[data-lima-status]", message); } function boss(name, health, maximum) { const hud = controller.root.querySelector("[data-lima-boss]"); hud.hidden = false; text("[data-lima-boss-name]", name); hud.querySelector("i").style.width = `${Math.max(0, health / maximum) * 100}%`; } function text(selector, value) { const node = controller.root && controller.root.querySelector(selector); if (node) node.textContent = value; } function focusGame() { const game = controller.root.querySelector("[data-lima-game]"); if (game) game.focus(); } function localDebug() { const params = new URLSearchParams(window.location.search); return ["localhost", "127.0.0.1"].includes(window.location.hostname) && params.get("collisionDebug") === "1"; } function localRegionPreview() { if (!["localhost", "127.0.0.1"].includes(window.location.hostname)) return null; const region = new URLSearchParams(window.location.search).get("region"); return Data.MAPS[region] ? region : null; } let queuedStatus = ""; function queueStatus(message) { queuedStatus = message; } function flushQueuedStatus() { if (queuedStatus) status(queuedStatus); } function gracefulFailure(message) { const loading = document.querySelector("[data-lima-loading]"); if (loading) loading.innerHTML = `The adventure could not start.${message || "A required local game file is unavailable."}Exit to Website`; } Object.assign(controller, { ready, travel, reloadRegion, persistPosition, setState, updateHud, status, prompt, boss, openDialogue, openPanel, gameOver, openEnding, useTonic, toggleFullscreen, toggleSound, cycleItem }); }());