Files
org_web/assets/scripts/pages/princess-lima-v2-game.js
gitea-actions 563d4b63cc
All checks were successful
Build Org Website / build (push) Successful in 37s
Tune Princess Lima v2 boss balance and Sun Crystal aid
2026-07-30 15:36:59 +01:00

279 lines
14 KiB
JavaScript
Executable File
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.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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[char])); }
Object.assign(controller, {
ready, showMenu, hideMenu, beginAdventure, showHud, updateHud, setState, save, travel,
prompt, status, boss, toggleFullscreen, fatal, frameDialogue, root: null
});
}());