Files
org_web/assets/scripts/pages/princess-lima-intro.js
gitea-actions 15b626ee0d
All checks were successful
Build Org Website / build (push) Successful in 38s
Rebuild Princess Lima maps around the new visual atlas
2026-07-30 12:48:17 +01:00

155 lines
5.6 KiB
JavaScript
Executable File

(function (root) {
"use strict";
const BEATS = Object.freeze([
Object.freeze({
panel: 0, duration: 15000, voice: "intro-narration-1",
subtitle: "Before shadow crossed the northern road, Princess Lima walked among her people—listening before she ruled, and helping before she asked."
}),
Object.freeze({
panel: 1, duration: 14000, voice: "intro-narration-2", effect: "door",
subtitle: "Then Lord Malrec descended from the Fortress of Shadows. His riders carried fear through the valleys, searching for the royal oath."
}),
Object.freeze({
panel: 2, duration: 15000, voice: "intro-narration-3", effect: "attack",
subtitle: "Lima stood between the riders and the village. Malrec could not bend her will, so he bound her in shadow and carried her beyond the mountains."
}),
Object.freeze({
panel: 3, duration: 15000, voice: "intro-narration-4", effect: "quest",
subtitle: "At dawn, a lone traveller reached the broken village. The road was dangerous, but every rescued life would become another light leading to Lima."
}),
Object.freeze({
panel: 3, duration: 8000, voice: null, effect: "victory",
subtitle: "RESCUE PRINCESS LIMA — Chapter One: The Broken Village"
})
]);
function create(container, actions) {
const element = container.querySelector("[data-lima-intro]");
const picture = element.querySelector("[data-lima-intro-picture]");
const subtitle = element.querySelector("[data-lima-intro-subtitle]");
const progress = element.querySelector("[data-lima-intro-progress]");
const pauseButton = element.querySelector("[data-lima-intro-pause]");
const muteButton = element.querySelector("[data-lima-intro-mute]");
let beatIndex = 0;
let elapsed = 0;
let startedAt = 0;
let timer = 0;
let paused = false;
let replay = false;
let escapeStarted = 0;
let running = false;
function start(options) {
stopTimer();
beatIndex = 0;
elapsed = 0;
paused = false;
replay = Boolean(options && options.replay);
running = true;
element.hidden = false;
container.classList.add("is-intro-running");
actions.onLock(true);
actions.onAudioUnlock();
renderBeat();
element.focus();
}
function renderBeat() {
const beat = BEATS[beatIndex];
picture.dataset.panel = String(beat.panel);
subtitle.textContent = beat.subtitle;
subtitle.hidden = !actions.subtitlesEnabled();
progress.style.width = `${beatIndex / BEATS.length * 100}%`;
progress.parentElement.setAttribute("aria-valuenow", String(beatIndex + 1));
if (beat.voice && actions.narrationEnabled()) actions.playVoice(beat.voice);
if (beat.effect) actions.playEffect(beat.effect, 0.55);
startedAt = Date.now();
timer = window.setTimeout(next, beat.duration);
pauseButton.textContent = "Pause";
pauseButton.setAttribute("aria-pressed", "false");
}
function next() {
stopTimer();
actions.stopVoice();
beatIndex += 1;
elapsed = 0;
if (beatIndex >= BEATS.length) return finish(false);
renderBeat();
}
function togglePause() {
if (!running) return;
paused = !paused;
if (paused) {
elapsed += Date.now() - startedAt;
stopTimer();
actions.stopVoice();
} else {
const beat = BEATS[beatIndex];
startedAt = Date.now();
timer = window.setTimeout(next, Math.max(500, beat.duration - elapsed));
if (beat.voice && actions.narrationEnabled()) actions.playVoice(beat.voice);
}
pauseButton.textContent = paused ? "Resume" : "Pause";
pauseButton.setAttribute("aria-pressed", String(paused));
element.classList.toggle("is-paused", paused);
}
function finish(skipped) {
if (!running) return;
running = false;
stopTimer();
actions.stopVoice();
progress.style.width = "100%";
element.hidden = true;
element.classList.remove("is-paused");
container.classList.remove("is-intro-running");
actions.onComplete({ replay, skipped });
}
function stopTimer() {
if (timer) window.clearTimeout(timer);
timer = 0;
}
function updateMute() {
const enabled = actions.soundEnabled();
muteButton.textContent = enabled ? "Mute" : "Enable sound";
muteButton.setAttribute("aria-pressed", String(!enabled));
}
pauseButton.addEventListener("click", togglePause);
muteButton.addEventListener("click", () => {
actions.toggleSound();
updateMute();
});
element.querySelector("[data-lima-intro-skip]").addEventListener("click", () => finish(true));
document.addEventListener("keydown", (event) => {
if (!running) return;
if (event.key === "Escape" && !escapeStarted) {
event.preventDefault();
escapeStarted = Date.now();
} else if (event.key.toLowerCase() === "p") {
event.preventDefault();
togglePause();
}
});
document.addEventListener("keyup", (event) => {
if (!running || event.key !== "Escape") return;
event.preventDefault();
if (escapeStarted && Date.now() - escapeStarted >= 900) finish(true);
else actions.status("Hold Escape for one second to skip the introduction.");
escapeStarted = 0;
});
return Object.freeze({ start, finish, togglePause, isRunning: () => running, updateMute, BEATS });
}
const api = Object.freeze({ create, BEATS });
if (typeof module === "object" && module.exports) module.exports = api;
root.PrincessLimaIntro = api;
}(typeof globalThis !== "undefined" ? globalThis : this));