(function (root) { "use strict"; const Data = root.PrincessLimaV2Data; const State = root.PrincessLimaV2State; const Systems = root.PrincessLimaV2Systems; function create(container, actions) { const overlay = container.querySelector("[data-lima-overlay]"); const panel = container.querySelector("[data-lima-panel]"); const heading = container.querySelector("[data-lima-panel-title]"); const body = container.querySelector("[data-lima-panel-body]"); const closeButton = container.querySelector("[data-lima-panel-close]"); const live = container.querySelector("[data-lima-screenreader]"); let previousFocus = null; let dialogue = null; let typingTimer = null; let displayed = ""; let fullText = ""; function portraitPosition(speaker, expression) { const index = Data.PORTRAITS[speaker] && Data.PORTRAITS[speaker][expression] !== undefined ? Data.PORTRAITS[speaker][expression] : 14; return `${(index % 4) * 33.333}% ${Math.floor(index / 4) * 33.333}%`; } function open(kind, title, html, closable = true) { previousFocus = document.activeElement; overlay.hidden = false; panel.dataset.kind = kind; heading.textContent = title; body.innerHTML = html; closeButton.hidden = !closable; requestAnimationFrame(() => (body.querySelector("button, input, [tabindex]") || closeButton).focus()); } function close() { if (dialogue) return advance(); overlay.hidden = true; panel.dataset.kind = ""; body.innerHTML = ""; actions.lock(false, "menu"); if (previousFocus && previousFocus.focus) previousFocus.focus(); else actions.focusGame(); } function stopTyping() { if (typingTimer) clearInterval(typingTimer); typingTimer = null; } function typeLine(target, text) { stopTyping(); fullText = text; displayed = ""; const settings = actions.getState().settings; const speed = { slow: 42, normal: 27, fast: 12, instant: 0 }[settings.textSpeed]; if (!speed || settings.reducedMotion) { target.textContent = text; displayed = text; return; } let index = 0; typingTimer = setInterval(() => { index += 1; displayed = text.slice(0, index); target.textContent = displayed; if (index >= text.length) stopTyping(); }, speed); } function startDialogue(id, options = {}) { const definition = Data.DIALOGUES[id]; if (!definition) return; actions.lock(true, "dialogue"); actions.beginDialogue(id, options.target); dialogue = { id, node: definition.start, history: [], done: options.done || null, essential: definition.essential }; renderDialogue(); } function renderDialogue() { const definition = Data.DIALOGUES[dialogue.id]; const node = Systems.dialogueNode(dialogue.id, dialogue.node, actions.getState()); if (!node) return finishDialogue(); const speaker = Data.ACTORS[node.speaker] ? Data.ACTORS[node.speaker].name : node.speaker; dialogue.history.push({ speaker, text: node.text }); live.textContent = `${speaker}: ${node.text}`; open("dialogue", speaker, `
${escapeHtml(speaker)} ${escapeHtml(node.expression || "")}
${escapeHtml(entry.text)}
A locally hosted storybook RPG built for this site.
All game data, maps, images, code, and audio are served from this website.
`); } else if (kind === "quests") { const quests = Object.entries(Data.QUESTS).filter(([id]) => state.quests[id].status !== "locked").map(([id, quest]) => { const progress = state.quests[id]; return `${escapeHtml(Systems.currentObjective(state))}
${escapeHtml(Data.MAP_NAMES[state.region])}
`); body.querySelector("[data-close-panel]").addEventListener("click", close); body.querySelectorAll("[data-open-panel]").forEach((button) => button.addEventListener("click", () => openPanel(button.dataset.openPanel))); body.querySelector("[data-replay-intro]").addEventListener("click", actions.replayIntro); } } function gameOver() { actions.lock(true, "defeat"); open("gameover", "The Road Grows Quiet", `You wake at the last safe fire. Your story progress and important items remain.
`, false); body.querySelector("[data-respawn]").addEventListener("click", () => { overlay.hidden = true; actions.lock(false, "defeat"); actions.respawn(); }); } function ending() { actions.lock(true, "ending"); const allies = ["promised_village", "defences_disabled", "prisoners_freed"].filter((flag) => actions.getState().flags[flag]).length; open("ending", "Dawn Over Lima", `At dawn the roads reopen. Village bells answer the resistance fires, and the fortress windows shine with ordinary sunlight.
${allies >= 2 ? "The people you helped arrive together; no one returns home alone." : "The kingdom begins the slower work of finding one another again."}
Princess Lima is free.
`, false); body.querySelector("[data-ending-menu]").addEventListener("click", actions.returnToMenu); } closeButton.addEventListener("click", close); overlay.addEventListener("click", (event) => { if (event.target === overlay && !dialogue) close(); }); document.addEventListener("keydown", (event) => { if (dialogue && (event.key === "Enter" || event.key === " ")) { event.preventDefault(); advance(); } if (!dialogue && !overlay.hidden && event.key === "Escape") { event.preventDefault(); close(); } }); return Object.freeze({ openPanel, startDialogue, gameOver, ending, close, finishDialogue }); } function escapeHtml(value) { return String(value || "").replace(/[&<>"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[character])); } root.PrincessLimaV2UI = Object.freeze({ create }); }(typeof globalThis !== "undefined" ? globalThis : this));