(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 || "")}

${!dialogue.essential && actions.getState().dialogueHistory.includes(dialogue.id) ? '' : ""}
`, false); const line = body.querySelector("[data-dialogue-line]"); typeLine(line, node.text); const choices = body.querySelector("[data-dialogue-choices]"); if (node.choices) { body.querySelector("[data-dialogue-advance]").hidden = true; choices.innerHTML = node.choices.map((choice, index) => ``).join(""); choices.querySelectorAll("[data-choice]").forEach((button) => button.addEventListener("click", () => choose(Number(button.dataset.choice)))); } body.querySelector("[data-dialogue-advance]").addEventListener("click", advance); body.querySelector("[data-dialogue-history]").addEventListener("click", showHistory); const skip = body.querySelector("[data-dialogue-skip]"); if (skip) skip.addEventListener("click", finishDialogue); actions.frameDialogue(node); } function advance() { if (!dialogue) return; if (displayed !== fullText) { stopTyping(); const line = body.querySelector("[data-dialogue-line]"); if (line) line.textContent = fullText; displayed = fullText; return; } const node = Systems.dialogueNode(dialogue.id, dialogue.node, actions.getState()); if (!node || node.choices) return; actions.applyEffects(node.effects); if (node.next) { dialogue.node = node.next; renderDialogue(); } else finishDialogue(); } function choose(index) { const result = Systems.dialogueChoice(dialogue.id, dialogue.node, index, actions.getState()); if (!result) return; actions.setState(result.state); dialogue.node = result.next; renderDialogue(); } function finishDialogue() { stopTyping(); const done = dialogue && dialogue.done; if (dialogue) actions.recordDialogue(dialogue.id); dialogue = null; overlay.hidden = true; body.innerHTML = ""; live.textContent = ""; actions.endDialogue(); actions.lock(false, "dialogue"); if (done) done(); actions.focusGame(); } function showHistory() { const history = dialogue.history.map((entry) => `
  • ${escapeHtml(entry.speaker)}

    ${escapeHtml(entry.text)}

  • `).join(""); const dialog = document.createElement("dialog"); dialog.className = "lima-history"; dialog.innerHTML = `

    Conversation history

      ${history}
    `; container.appendChild(dialog); dialog.querySelector("button").addEventListener("click", () => { dialog.close(); dialog.remove(); }); dialog.showModal(); } function openPanel(kind) { const state = actions.getState(); actions.lock(true, "menu"); if (kind === "credits") { open("credits", "Credits", `

    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 `
  • ${quest.main ? "Main · " : "Optional · "}${escapeHtml(quest.title)}${progress.status === "complete" ? "Complete" : `${progress.count}/${quest.target}`}${escapeHtml(Data.MAP_NAMES[quest.region])}
  • `; }).join(""); open("quests", "Quest Chronicle", `

    ${escapeHtml(Systems.currentObjective(state))}

    `); } else if (kind === "inventory") { const items = state.inventory.map((entry) => `
  • ${escapeHtml(Data.ITEMS[entry.id].name)}×${entry.quantity}
  • `).join(""); open("inventory", "Traveller's Satchel", ``); body.querySelector("[data-use-tonic]").addEventListener("click", () => { actions.useTonic(); openPanel("inventory"); }); } else if (kind === "settings") { const checked = (key) => state.settings[key] ? "checked" : ""; open("settings", "Accessibility & Effects", `
    `); body.querySelector('[data-setting="effectsQuality"]').value = state.settings.effectsQuality; body.querySelector('[data-setting="textSpeed"]').value = state.settings.textSpeed; body.querySelectorAll("[data-setting]").forEach((control) => control.addEventListener("change", () => actions.setting(control.dataset.setting, control.type === "checkbox" ? control.checked : control.value))); } else { open("pause", "Roadside Menu", `

    ${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));