Files
org_web/assets/scripts/pages/princess-lima-game.js
gitea-actions fe8acac707
All checks were successful
Build Org Website / build (push) Successful in 39s
Refactor org web platform and remove legacy code
2026-07-30 11:29:20 +01:00

364 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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 controller = {
root: null,
game: null,
scene: null,
state: null,
audio: null,
ui: null,
locked: true,
transitioning: false,
move: { up: false, down: false, left: false, right: 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,
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 || !window.Phaser) return gracefulFailure();
controller.root = root;
controller.state = loadState();
controller.audio = Audio.create(() => controller.state);
controller.ui = UI.create(root, {
getState: () => controller.state,
onLock: (value) => { controller.locked = value; },
onUseTonic: useTonic,
onSetting: updateSetting,
onFullscreen: toggleFullscreen,
onReset: resetSave,
onRespawn: respawn
});
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 params = new URLSearchParams(window.location.search);
const debugRegion = localDebug() && Data.MAPS[params.get("region")] ? params.get("region") : null;
if (debugRegion) {
const firstSpawn = Object.entries(Data.MAPS[debugRegion].spawns)[0];
return State.withPosition(parsed, debugRegion, firstSpawn[0], firstSpawn[1].x, firstSpawn[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) beginAdventure();
});
root.querySelector("[data-lima-credits]").addEventListener("click", () => controller.ui.show(
"credits", "Credits",
"<p>Designed and built for zainezq.com. Original fantasy artwork and locally generated audio. Powered by locally vendored Phaser.</p>"
));
root.querySelector("[data-lima-setup-form]").addEventListener("submit", submitSetup);
root.querySelector("[data-lima-setup-cancel]").addEventListener("click", closeSetup);
root.querySelectorAll("[data-lima-move]").forEach((button) => {
const direction = button.dataset.limaMove;
const on = (event) => { event.preventDefault(); controller.move[direction] = true; focusGame(); };
const off = (event) => { event.preventDefault(); controller.move[direction] = false; };
button.addEventListener("pointerdown", on);
button.addEventListener("pointerup", off);
button.addEventListener("pointercancel", off);
button.addEventListener("pointerleave", off);
});
bindButton("[data-lima-attack]", () => controller.scene && controller.scene.attack(controller.scene.time.now));
bindButton("[data-lima-interact]", () => controller.scene && controller.scene.interact());
bindButton("[data-lima-item]", useTonic);
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();
beginAdventure();
}
function beginAdventure() {
controller.audio.unlock();
controller.root.querySelector("[data-lima-menu]").hidden = true;
controller.root.querySelector("[data-lima-hud]").hidden = false;
controller.root.querySelector("[data-lima-touch]").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 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-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 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";
}
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 = `<strong>The adventure could not start.</strong><span>${message || "A required local game file is unavailable."}</span><a href="/">Exit to Website</a>`;
}
Object.assign(controller, {
ready, travel, reloadRegion, persistPosition, setState, updateHud, status, prompt, boss,
openDialogue, openPanel, gameOver, openEnding, useTonic
});
}());