Refine Princess Lima for v2 presentation
All checks were successful
Build Org Website / build (push) Successful in 1m2s
All checks were successful
Build Org Website / build (push) Successful in 1m2s
This commit is contained in:
278
assets/scripts/pages/princess-lima-v2-game.js
Normal file
278
assets/scripts/pages/princess-lima-v2-game.js
Normal file
@@ -0,0 +1,278 @@
|
||||
(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 = `<strong>The road could not open.</strong><span>${escapeHtml(message)}</span><a href="/play/play.html">Return to Play</a>`;
|
||||
}
|
||||
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
|
||||
});
|
||||
}());
|
||||
Reference in New Issue
Block a user