// comic.js — centered comic-panel overlay for crisis beats. // // Shows a wordless FLUX illustration with a UI-rendered caption above it // (FLUX renders garbled text, so all words live in the crisp caption, not the // picture). When there is no image (FLUX unavailable / failed / timed out / not // yet ready), it does NOTHING and calls onDone() straight away — the fallback // is simply no overlay, and the normal dialogue opens as usual. import { G } from "./state.js"; import { sfx } from "./audio.js"; let dismiss = null; // active dismiss handler while a comic is showing function escapeHtml(s) { return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); } export function isOpen() { const el = G.els.comic; return !!(el && !el.classList.contains("hidden")); } // imageB64: a base64 PNG string, or null/"" → skip the overlay entirely. // caption: optional text drawn above the picture. export function showComic(imageB64, caption, onDone) { if (!imageB64) { onDone && onDone(); return; } const el = G.els.comic; const cap = caption && caption.trim() ? `
${escapeHtml(caption.trim())}
` : ""; el.innerHTML = `
${cap}
CLICK or SPACE ▶
`; el.classList.remove("hidden"); requestAnimationFrame(() => el.classList.add("comic-in")); sfx.comic(); let done = false; const timer = setTimeout(() => dismiss && dismiss(), 7000); // auto-advance dismiss = () => { if (done) return; done = true; clearTimeout(timer); el.classList.remove("comic-in"); el.classList.add("hidden"); el.innerHTML = ""; el.removeEventListener("click", dismiss); dismiss = null; onDone && onDone(); }; el.addEventListener("click", dismiss); } // called by the SPACE/interact handler in main.js while a comic is showing export function dismissComic() { if (dismiss) { dismiss(); return true; } return false; }