Files
org_web/assets/scripts/pages/princess-lima-ui.js
gitea-actions a0c78929b3
All checks were successful
Build Org Website / build (push) Successful in 50s
Refactor platform UI and data flow across core pages
2026-07-30 12:22:44 +01:00

194 lines
11 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 (root) {
"use strict";
const Data = root.PrincessLimaData;
const Systems = root.PrincessLimaSystems;
function create(container, actions) {
const overlay = container.querySelector("[data-lima-overlay]");
const panel = container.querySelector("[data-lima-panel]");
const title = container.querySelector("[data-lima-panel-title]");
const body = container.querySelector("[data-lima-panel-body]");
const closeButton = container.querySelector("[data-lima-panel-close]");
let returnFocus = null;
let dialogue = null;
let dialogueIndex = 0;
function lock(value) {
actions.onLock(value);
overlay.hidden = !value;
container.classList.toggle("is-overlay-open", value);
}
function show(kind, heading, html, closable) {
returnFocus = document.activeElement;
panel.dataset.kind = kind;
title.textContent = heading;
body.innerHTML = html;
closeButton.hidden = closable === false;
lock(true);
const target = body.querySelector("button, input, select") || closeButton;
target.focus();
}
function close() {
if (dialogue) return advanceDialogue();
lock(false);
body.innerHTML = "";
if (returnFocus && document.contains(returnFocus)) returnFocus.focus();
else container.querySelector("[data-lima-game]").focus();
}
function openDialogue(id, done) {
const npc = Data.NPCS[id];
if (!npc) return;
dialogue = { id, lines: npc.dialogue.slice(), done };
dialogueIndex = 0;
renderDialogue();
}
function renderDialogue() {
const npc = Data.NPCS[dialogue.id];
const line = dialogue.lines[dialogueIndex];
show("dialogue", npc.name, `
<div class="lima-dialogue">
<div class="lima-dialogue__portrait lima-sprite lima-sprite--${npc.frame}" aria-hidden="true"></div>
<p data-lima-dialogue-text>${escapeHtml(line)}</p>
</div>
<button type="button" class="lima-primary" data-lima-advance>${dialogueIndex + 1 < dialogue.lines.length ? "Continue" : "Finish"}</button>
`, false);
body.querySelector("[data-lima-advance]").addEventListener("click", advanceDialogue, { once: true });
}
function advanceDialogue() {
if (!dialogue) return;
dialogueIndex += 1;
if (dialogueIndex < dialogue.lines.length) return renderDialogue();
const done = dialogue.done;
dialogue = null;
lock(false);
body.innerHTML = "";
container.querySelector("[data-lima-game]").focus();
if (done) done();
}
function openPanel(kind, state) {
if (kind === "inventory") {
const entries = state.inventory.length ? state.inventory.map((entry) => {
const item = Data.ITEMS[entry.id];
const equipped = Object.values(state.equipment).includes(entry.id) ? " · Equipped" : "";
return `<li><strong>${escapeHtml(item.name)}</strong><span>×${entry.quantity}${equipped}</span><p>${escapeHtml(item.description)}</p></li>`;
}).join("") : "<li>No items yet.</li>";
show("inventory", "Inventory", `<ul class="lima-list">${entries}</ul><button type="button" data-lima-use-tonic>Use Healing Tonic</button>`);
const use = body.querySelector("[data-lima-use-tonic]");
use.addEventListener("click", () => { actions.onUseTonic(); openPanel("inventory", actions.getState()); });
} else if (kind === "quests") {
const quests = Data.QUEST_IDS.filter((id) => state.quests[id].status !== "locked").map((id) => {
const quest = Data.QUESTS[id];
const progress = state.quests[id];
return `<li class="${progress.status === "complete" ? "is-complete" : ""}">
<strong>${quest.main ? "Main · " : "Optional · "}${escapeHtml(quest.title)}</strong>
<span>${progress.status === "complete" ? "Complete" : `${progress.count} / ${quest.target}`}</span>
<p>${escapeHtml(quest.description)} · ${escapeHtml(quest.region)}</p>
</li>`;
}).join("");
show("quests", "Quest Log", `<p class="lima-panel-lead">${escapeHtml(Systems.currentObjective(state))}</p><ul class="lima-list">${quests || "<li>No quests yet.</li>"}</ul>`);
} else if (kind === "settings") {
show("settings", "Settings", `
<label class="lima-setting"><span>Master volume</span><input type="range" min="0" max="1" step="0.05" value="${state.settings.master}" data-setting="master"></label>
<label class="lima-setting"><span>Music</span><input type="range" min="0" max="1" step="0.05" value="${state.settings.music}" data-setting="music"></label>
<label class="lima-setting"><span>Effects</span><input type="range" min="0" max="1" step="0.05" value="${state.settings.effects}" data-setting="effects"></label>
<label class="lima-setting"><span>Narration</span><input type="range" min="0" max="1" step="0.05" value="${state.settings.voice}" data-setting="voice"></label>
<label class="lima-check"><input type="checkbox" data-setting="narrationEnabled" ${state.settings.narrationEnabled ? "checked" : ""}> Spoken narration</label>
<label class="lima-check"><input type="checkbox" data-setting="subtitles" ${state.settings.subtitles ? "checked" : ""}> Cinematic subtitles</label>
<label class="lima-check"><input type="checkbox" data-setting="reducedMotion" ${state.settings.reducedMotion ? "checked" : ""}> Reduced motion</label>
<label class="lima-check"><input type="checkbox" data-setting="screenShake" ${state.settings.screenShake ? "checked" : ""}> Screen shake</label>
<label class="lima-check"><input type="checkbox" data-setting="highContrast" ${state.settings.highContrast ? "checked" : ""}> High-contrast interface</label>
<label class="lima-setting"><span>Text speed</span><select data-setting="textSpeed">${["slow", "normal", "fast", "instant"].map((value) => `<option ${state.settings.textSpeed === value ? "selected" : ""}>${value}</option>`).join("")}</select></label>
`);
body.querySelectorAll("[data-setting]").forEach((control) => control.addEventListener("change", () => {
const value = control.type === "checkbox" ? control.checked : control.type === "range" ? Number(control.value) : control.value;
actions.onSetting(control.dataset.setting, value);
}));
} else if (kind === "pause") {
show("pause", "Paused", `
<p>${escapeHtml(Systems.currentObjective(state))}</p>
<div class="lima-menu-stack">
<button type="button" data-panel="inventory">Inventory</button>
<button type="button" data-panel="quests">Quest Log</button>
<button type="button" data-panel="settings">Settings & accessibility</button>
<button type="button" data-lima-replay-intro-panel>Replay introduction</button>
<button type="button" data-lima-fullscreen-panel>Toggle fullscreen</button>
<button type="button" data-lima-reset-request>Reset save</button>
<a href="/">Exit to Website</a>
</div>
`);
body.querySelectorAll("[data-panel]").forEach((button) => button.addEventListener("click", () => openPanel(button.dataset.panel, actions.getState())));
body.querySelector("[data-lima-replay-intro-panel]").addEventListener("click", actions.onReplayIntro);
body.querySelector("[data-lima-fullscreen-panel]").addEventListener("click", actions.onFullscreen);
body.querySelector("[data-lima-reset-request]").addEventListener("click", confirmReset);
}
}
function confirmReset() {
show("confirm", "Delete this adventure?", `
<p>This permanently removes the Princess Lima save on this device.</p>
<div class="lima-confirm"><button type="button" class="lima-danger" data-confirm-reset>Delete save</button><button type="button" data-cancel-reset>Keep progress</button></div>
`, false);
body.querySelector("[data-confirm-reset]").addEventListener("click", actions.onReset, { once: true });
body.querySelector("[data-cancel-reset]").addEventListener("click", () => openPanel("pause", actions.getState()), { once: true });
}
function gameOver(state) {
show("defeat", "The road is not finished", `
<p>You awaken at the latest safe checkpoint with your quests and important items intact.</p>
<button type="button" class="lima-primary" data-respawn>Return to checkpoint</button>
`, false);
body.querySelector("[data-respawn]").addEventListener("click", actions.onRespawn, { once: true });
}
function ending(state) {
show("ending", "Princess Lima Rescued", `
<p>At dawn, the roads reopen. The villages ring their bells, the forest paths quiet, and the mountain fires become beacons instead of warnings.</p>
<p><strong>${escapeHtml(state.player.name)}</strong> is offered a place at the royal table—and chooses first to walk the repaired road home with Lima.</p>
<p class="lima-ending-note">The kingdom remains explorable from your final save.</p>
<button type="button" class="lima-primary" data-ending-continue>Continue exploring</button>
<a class="lima-button-link" href="/">Exit to Website</a>
`, false);
body.querySelector("[data-ending-continue]").addEventListener("click", close, { once: true });
}
closeButton.addEventListener("click", close);
overlay.addEventListener("click", (event) => { if (event.target === overlay && !dialogue) close(); });
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && !overlay.hidden && !dialogue) {
event.preventDefault();
close();
}
if ((event.key === "Enter" || event.key === " ") && dialogue && !overlay.hidden) {
event.preventDefault();
advanceDialogue();
}
if (!overlay.hidden && !dialogue && ["ArrowDown", "ArrowUp"].includes(event.key)) {
const controls = Array.from(panel.querySelectorAll("button:not([hidden]), a[href], input, select"))
.filter((control) => !control.disabled && control.offsetParent !== null);
if (!controls.length) return;
event.preventDefault();
const current = controls.indexOf(document.activeElement);
const change = event.key === "ArrowDown" ? 1 : -1;
controls[(current + change + controls.length) % controls.length].focus();
}
});
return Object.freeze({ show, close, openDialogue, openPanel, gameOver, ending, confirmReset });
}
function escapeHtml(value) {
return String(value).replace(/[&<>"']/g, (character) => ({
"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;"
}[character]));
}
root.PrincessLimaUI = Object.freeze({ create });
}(typeof globalThis !== "undefined" ? globalThis : this));