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:
363
assets/scripts/pages/princess-lima-game.js
Normal file
363
assets/scripts/pages/princess-lima-game.js
Normal file
@@ -0,0 +1,363 @@
|
||||
(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
|
||||
});
|
||||
}());
|
||||
Reference in New Issue
Block a user