/* ============================================================ ДЕЛО № 260826 — движок v2 Состояния экранов, журнал фактов, подсказки, звук. ============================================================ */ (function () { "use strict"; const STORAGE_KEY = "papka_v2_state"; const MUTE_KEY = "papka_v2_muted"; /* Экраны без верхней панели и дока */ const BARE = new Set(["intro", "loading", "twist", "congrats", "orderScreen"]); /* Экраны-загадки: на них доступна подсказка */ const PUZZLES = { caseNumber: CONTENT.caseNumber, scene: CONTENT.scene, witnesses: CONTENT.witnesses, philology: CONTENT.philology, audio: CONTENT.audio, falseTrail: CONTENT.falseTrail, orderForm: CONTENT.order, }; const NEXT = { caseNumber: "caseOpened", caseOpened: "scene", scene: "witnesses", witnesses: "philology", philology: "audio", audio: "falseTrail", falseTrail: "orderForm", orderForm: "twist", twist: "congrats", congrats: "orderScreen", }; /* ---------------- состояние ---------------- */ const defaultState = () => ({ stage: "intro", solved: {}, // stageId -> true journal: [], // накопленные факты mistakes: {}, hintRequested: {}, tellsFound: [], // найденные признаки подделки в рапорте orderFilled: {}, // подтверждённые графы ордера easter: {}, muted: localStorage.getItem(MUTE_KEY) === "1", }); let state = load(); function load() { try { const raw = localStorage.getItem(STORAGE_KEY); return raw ? Object.assign(defaultState(), JSON.parse(raw)) : defaultState(); } catch (e) { return defaultState(); } } function save() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) {} } function resetAll() { localStorage.removeItem(STORAGE_KEY); state = defaultState(); render(); } /* ---------------- звук ---------------- */ const Sound = (function () { let ctx = null; function ensure() { if (!ctx) { const AC = window.AudioContext || window.webkitAudioContext; if (AC) ctx = new AC(); } if (ctx && ctx.state === "suspended") ctx.resume(); return ctx; } function tone(freq, dur, type, peak, delay) { if (state.muted) return; const c = ensure(); if (!c) return; const t0 = c.currentTime + (delay || 0); const osc = c.createOscillator(); const g = c.createGain(); osc.type = type || "sine"; osc.frequency.setValueAtTime(freq, t0); g.gain.setValueAtTime(0.0001, t0); g.gain.exponentialRampToValueAtTime(peak || 0.07, t0 + 0.015); g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur); osc.connect(g); g.connect(c.destination); osc.start(t0); osc.stop(t0 + dur + 0.02); } return { click() { tone(620, 0.045, "square", 0.025); }, success() { tone(523, 0.09, "sine", 0.06); tone(784, 0.14, "sine", 0.05, 0.09); }, error() { tone(150, 0.15, "sawtooth", 0.04); }, stamp() { tone(90, 0.09, "square", 0.085); tone(68, 0.13, "square", 0.06, 0.03); }, warm() { tone(392, 0.3, "sine", 0.045); tone(523, 0.35, "sine", 0.035, 0.12); tone(659, 0.5, "sine", 0.035, 0.24); }, }; })(); /* ---------------- утилиты ---------------- */ const digits = (s) => ((s || "").match(/\d+/g) || []).join(""); const word = (s) => (s || "").trim().toLowerCase().replace(/ё/g, "е"); const bare = (s) => (s || "").toUpperCase().replace(/[\s\-_.·№]/g, "").replace(/Ё/g, "Е"); /* Время приводим к чч:мм, чтобы «9:08», «09.08» и «0908» считались тем же ответом, что и «09:08». */ function normTime(s) { const m = (s || "").trim().match(/^(\d{1,2})\s*[:.\-\s]?\s*(\d{2})$/); if (!m) return null; const h = +m[1], min = +m[2]; if (h > 23 || min > 59) return null; return String(h).padStart(2, "0") + ":" + m[2]; } /* Сравнение ответа для конкретной графы ордера. */ function matchesFact(fieldId, value) { if (fieldId === "exitTime") { const t = normTime(value); return t !== null && t === FACTS.exitTime; } return bare(value) === bare(FACTS[fieldId]); } function mistakesFor(id) { return state.mistakes[id] || 0; } function addMistake(id) { state.mistakes[id] = mistakesFor(id) + 1; save(); } function autoHint(id) { const m = mistakesFor(id); return m >= 8 ? 3 : m >= 5 ? 2 : m >= 3 ? 1 : 0; } function hintLevel(id) { return Math.max(autoHint(id), state.hintRequested[id] || 0); } function bumpHint(id, max) { const cur = state.hintRequested[id] || 0; if (cur < max) state.hintRequested[id] = cur + 1; save(); } function canReveal(id) { return mistakesFor(id) >= 8; } function pick(arr, i) { return arr[i % arr.length]; } function addFact(ev) { if (state.journal.some((j) => j.name === ev.name)) return; state.journal.push({ name: ev.name, fact: ev.fact, factLabel: ev.factLabel, desc: ev.desc }); save(); } function markSolved(id) { state.solved[id] = true; save(); } function go(stage) { state.stage = stage; save(); render(); window.scrollTo({ top: 0, behavior: "smooth" }); } function esc(s) { const d = document.createElement("div"); d.textContent = s == null ? "" : String(s); return d.innerHTML; } function paras(lines) { return (Array.isArray(lines) ? lines : [lines]) .map((l) => (l === "" ? '
' : `

${esc(l)}

`)) .join(""); } /* Фидбек переживает перерисовку: render() пересобирает DOM целиком, поэтому текст ошибки кладём сюда и забираем на следующем рендере. */ let pending = {}; /* То же для граф ордера: у каждой свой текст ошибки и своё введённое значение, которые обязаны пережить перерисовку. */ let pendingOrder = {}; function setFeedback(id, text, cls, value) { pending[id] = { text, cls, value }; } function takeFeedback(fbEl, id, inputEl) { const f = pending[id]; if (!f) return; delete pending[id]; fbEl.innerHTML = `${esc(f.text)}`; fbEl.className = "feedback " + f.cls; if (inputEl && f.value != null) { inputEl.value = f.value; inputEl.focus(); try { inputEl.setSelectionRange(f.value.length, f.value.length); } catch (e) {} } } /* ---------------- каркас ---------------- */ const app = document.getElementById("app"); function render() { app.innerHTML = ""; document.body.className = ["twist", "congrats", "orderScreen"].includes(state.stage) ? "warm" : ""; const wrap = document.createElement("div"); wrap.className = "shell"; if (!BARE.has(state.stage)) { wrap.appendChild(topbar()); wrap.appendChild(progressRail()); } const main = document.createElement("main"); main.className = "stage"; main.appendChild(stageView(state.stage)); wrap.appendChild(main); if (!BARE.has(state.stage)) { wrap.appendChild(dock()); wrap.appendChild(footer()); } app.appendChild(wrap); } function topbar() { const el = document.createElement("header"); el.className = "topbar"; const hintable = !!PUZZLES[state.stage] && !state.solved[state.stage]; /* Номер дела — ответ первой загадки, до её решения его не показываем. */ const caseLabel = state.solved.caseNumber ? "дело № " + CONFIG.caseNumber : "дело № ——————"; el.innerHTML = `
${esc(caseLabel)}
`; el.querySelector('[data-a="journal"]').onclick = openJournal; el.querySelector('[data-a="hint"]').onclick = () => openHint(state.stage); el.querySelector('[data-a="sound"]').onclick = () => { state.muted = !state.muted; localStorage.setItem(MUTE_KEY, state.muted ? "1" : "0"); render(); }; return el; } function progressRail() { const el = document.createElement("div"); el.className = "rail"; const curIdx = CHAPTERS.findIndex((c) => c.stage === state.stage); el.innerHTML = CHAPTERS.map((c, i) => { const done = state.solved[c.stage]; const cur = i === curIdx; return `
${done ? "✓" : i + 1} ${esc(c.label)}
`; }).join(""); return el; } function dock() { const e = CONTENT.easter; const el = document.createElement("div"); el.className = "dock"; el.innerHTML = ` `; el.querySelector('[data-a="speed"]').onclick = () => { const n = (state.easter.speed = (state.easter.speed || 0) + 1); Sound.click(); save(); toast(n === 1 ? e.speedFirst : e.speedSecond); }; el.querySelector('[data-a="confused"]').onclick = () => { const n = (state.easter.confused = (state.easter.confused || 0) + 1); Sound.click(); save(); toast(n === 1 ? e.confusedFirst : e.confusedSecond); }; el.querySelector('[data-a="complaint"]').onclick = () => { Sound.click(); toast(e.complaintResult); }; el.querySelector('[data-a="reset"]').onclick = () => { const now = Date.now(); if (resetArmed && now - resetArmed < 4000) { Sound.stamp(); resetArmed = 0; resetAll(); } else { resetArmed = now; Sound.click(); toast(e.resetArm); } }; return el; } let resetArmed = 0; function footer() { const f = document.createElement("div"); f.className = "footer"; f.textContent = CONTENT.easter.footer; return f; } /* Clipboard API недоступен при открытии страницы файлом (file://) и в ряде мобильных браузеров, а writeText умеет отклоняться. Поэтому запасной путь через execCommand и честное сообщение, если не вышло. */ function copyText(text) { const fallback = () => { try { const ta = document.createElement("textarea"); ta.value = text; ta.setAttribute("readonly", ""); ta.style.cssText = "position:fixed;top:0;left:0;opacity:0"; document.body.appendChild(ta); ta.select(); const ok = document.execCommand("copy"); document.body.removeChild(ta); toast(ok ? "Скопировано." : "Скопировать не удалось — выделите текст выше вручную."); } catch (e) { toast("Скопировать не удалось — выделите текст выше вручную."); } }; if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(text).then(() => toast("Скопировано."), fallback); } else { fallback(); } } let toastT = null; function toast(text) { let t = document.getElementById("toast"); if (!t) { t = document.createElement("div"); t.id = "toast"; document.body.appendChild(t); } t.textContent = text; t.classList.add("on"); clearTimeout(toastT); toastT = setTimeout(() => t.classList.remove("on"), 3800); } /* ---------------- панели ---------------- */ function closePanels() { document.querySelectorAll(".backdrop").forEach((n) => n.remove()); } function panel(title, bodyHtml, cls) { closePanels(); const b = document.createElement("div"); b.className = "backdrop"; b.innerHTML = ` `; b.onclick = (e) => { if (e.target === b) closePanels(); }; b.querySelector('[data-a="x"]').onclick = closePanels; document.body.appendChild(b); return b; } function openJournal() { const body = state.journal.length === 0 ? '

Журнал пуст. Следствие в самом начале.

' : `

Установленные факты. В финале понадобятся все.

` + state.journal.map((j) => `
${esc(j.name)}
${esc(j.factLabel)}${esc(j.fact)}
${esc(j.desc)}
`).join(""); panel("Журнал дела", body); } function openHint(stageId) { const cfg = PUZZLES[stageId]; if (!cfg || !cfg.hints) return; bumpHint(stageId, cfg.hints.length); const lvl = hintLevel(stageId); const body = `

${esc(pick(COMMON.hintIntro, lvl))}

${cfg.hints.slice(0, lvl).map((h, i) => `

${i + 1}. ${esc(h)}

`).join("")} ${lvl === 0 ? '

Подсказка ещё не раскрыта. Нажмите ещё раз.

' : ""}`; panel("Подсказка", body, "panel-hint"); } /* ---------------- строительные блоки ---------------- */ function sheet(html, cls) { const d = document.createElement("section"); d.className = "sheet" + (cls ? " " + cls : ""); d.innerHTML = html; return d; } function btn(label, cls) { const b = document.createElement("button"); b.className = "btn " + (cls || "primary"); b.textContent = label; return b; } function row(nodes) { const r = document.createElement("div"); r.className = "row"; nodes.forEach((n) => r.appendChild(n)); return r; } function head(chapter, title) { return `
${esc(chapter)}

${esc(title)}

`; } /* Блок успеха с карточкой добытого факта */ function successBlock(title, text, ev) { return `
✓${esc(title)}

${esc(text)}

${ev ? `
${esc(ev.name)}
${esc(ev.factLabel)}${esc(ev.fact)}
` : ""}
`; } function revealLink(stageId, onClick) { if (!canReveal(stageId)) return null; const b = document.createElement("button"); b.className = "linkbtn"; b.textContent = "Следствие настаивает на правильном ответе"; b.onclick = onClick; return b; } /* ============================================================ ЭКРАНЫ ============================================================ */ function stageView(s) { switch (s) { case "intro": return vIntro(); case "loading": return vLoading(); case "caseNumber": return vCaseNumber(); case "caseOpened": return vCaseOpened(); case "scene": return vScene(); case "witnesses": return vWitnesses(); case "philology": return vPhilology(); case "audio": return vAudio(); case "falseTrail": return vFalseTrail(); case "orderForm": return vOrderForm(); case "twist": return vTwist(); case "congrats": return vCongrats(); case "orderScreen": return vOrderScreen(); default: return sheet("

Раздел ушёл в отпуск по сюжетным обстоятельствам.

"); } } /* ---- интро ---- */ function vIntro() { const c = CONTENT.intro; const el = document.createElement("div"); el.className = "center-wrap"; el.innerHTML = `
${esc(c.tag)}

${esc(c.title)}

${paras(c.body)}

${esc(c.footnote)}

`; const b = btn(c.button); b.onclick = () => { Sound.click(); go("loading"); }; el.querySelector(".intro").insertBefore(row([b]), el.querySelector(".note")); return el; } /* ---- загрузка ---- */ function vLoading() { const c = CONTENT.loadingScreen; const el = document.createElement("div"); el.className = "boot"; el.innerHTML = `

${esc(c.system)}

${esc(c.subtitle)}

`; const box = el.querySelector(".boot-lines"); c.lines.forEach((l, i) => { const p = document.createElement("p"); p.textContent = l; p.style.animationDelay = i * 0.5 + "s"; box.appendChild(p); }); setTimeout(() => { const b = btn(c.button); b.onclick = () => { Sound.click(); go("caseNumber"); }; const r = row([b]); r.classList.add("center"); el.appendChild(r); }, c.lines.length * 500 + 400); return el; } /* ---- глава 0: номер дела ---- */ function vCaseNumber() { const c = CONTENT.caseNumber; const id = "caseNumber"; const el = sheet(` ${head(c.chapter, c.title)} ${paras(c.body)}
${esc(c.ruleTitle)}

${esc(c.ruleText)}

${esc(c.registryTitle)}
${c.registryRows.map(([k, v]) => `
${esc(k)}${esc(v)}
`).join("")}
`); const input = el.querySelector("#in"); const fb = el.querySelector("#fb"); takeFeedback(fb, id, input); const submit = () => { const raw = input.value; if (digits(raw) === CONFIG.caseNumber) { Sound.success(); markSolved(id); go("caseOpened"); } else { Sound.error(); addMistake(id); setFeedback(id, pick(c.wrong, mistakesFor(id) - 1), "bad shake", raw); render(); } }; el.querySelector("#go").onclick = submit; input.onkeydown = (e) => { if (e.key === "Enter") submit(); }; const rv = revealLink(id, () => { input.value = CONFIG.caseNumber; input.focus(); }); if (rv) el.appendChild(rv); return el; } /* ---- дело открыто ---- */ function vCaseOpened() { const c = CONTENT.caseOpened; const el = sheet(`${head("Допуск получен", c.title)}${paras(c.body)}`); const b = btn(c.button); b.onclick = () => { Sound.click(); go("scene"); }; el.appendChild(row([b])); return el; } /* ---- глава 1: опись ---- */ function vScene() { const c = CONTENT.scene; const id = "scene"; const done = !!state.solved[id]; const el = sheet(` ${head(c.chapter, c.title)} ${paras(c.body)}
${esc(c.protocolTitle)}
${c.inventory.map(([n, d]) => `
${esc(n)}${esc(d)}
`).join("")}
${esc(c.protocolNote)}
${desk()}
`, "wide"); const fb = el.querySelector("#fb"); const after = el.querySelector("#after"); if (done) { after.innerHTML = successBlock(c.successTitle, c.successBody, c.evidence); const b = btn(c.button); b.onclick = () => { Sound.click(); go("witnesses"); }; after.appendChild(row([b])); el.querySelector('[data-id="box"]').classList.add("found"); } else { takeFeedback(fb, id); el.querySelectorAll(".hit").forEach((n) => { n.onclick = () => { const hid = n.getAttribute("data-id"); if (hid === c.correctId) { Sound.success(); addFact(c.evidence); markSolved(id); render(); } else { Sound.error(); addMistake(id); setFeedback(id, c.reactions[hid] || "Предмет осмотрен. Предмет молчит.", "bad"); render(); } }; }); const rv = revealLink(id, () => el.querySelector('[data-id="box"]').classList.add("pulse")); if (rv) el.appendChild(rv); } return el; } /* Стол с предметами и читаемыми бирками */ function desk() { const item = (id, x, y, w, h, tag, inner) => ` ${inner} ${tag} `; return ` ${Array.from({ length: 14 }, (_, i) => ``).join("")} ${item("folder", 26, 26, 128, 82, "БКТО-14", ` ДЕЛО № ${esc(CONFIG.caseNumber)}`)} ${item("mug", 196, 40, 54, 54, "БКТО-12", ` НЕ ТРОГАТЬ`)} ${item("calendar", 292, 30, 84, 68, "БКТО-13", ` 26 `)} ${item("lamp", 424, 22, 76, 96, "БКТО-11", ` `)} ${item("pencil", 34, 190, 116, 12, "БКТО-15", ` `)} ${item("paper", 176, 168, 170, 92, "БКТО-17", ` ПЕЧАТЬ НЕ ТРОГАТЬ (печать отсутствует)`)} ${item("cactus", 424, 176, 54, 84, "БКТО-16", ` `)} ${item("box", 246, 306, 104, 66, "ДС-04", ` хрупкое. сюжет внутри`)} `; } /* ---- глава 2: журнал и показания ---- */ function vWitnesses() { const c = CONTENT.witnesses; const id = "witnesses"; const done = !!state.solved[id]; const el = sheet(` ${head(c.chapter, c.title)} ${paras(c.body)}
${esc(c.logTitle)}
${c.logRows.map(([t, n, d]) => `
${esc(t)} ${esc(n)} ${esc(d)}
`).join("")}
`); const cards = el.querySelector("#cards"); c.cards.forEach((w) => { const b = document.createElement("button"); b.className = "pcard"; b.disabled = done; if (w.id === c.correct) b.dataset.correct = "1"; if (done && w.id === c.correct) b.classList.add("found"); b.innerHTML = `
${esc(w.name)}${esc(w.role)}
${esc(w.text)}
`; b.onclick = () => { if (w.id === c.correct) { Sound.success(); addFact(c.evidence); markSolved(id); render(); } else { Sound.error(); addMistake(id); setFeedback(id, c.wrongReactions[w.id], "bad"); render(); } }; cards.appendChild(b); }); const after = el.querySelector("#after"); if (done) { after.innerHTML = successBlock(c.successTitle, c.successBody, c.evidence); const b = btn(c.button); b.onclick = () => { Sound.click(); go("philology"); }; after.appendChild(row([b])); } else { takeFeedback(el.querySelector("#fb"), id); const rv = revealLink(id, () => el.querySelector('[data-correct="1"]').classList.add("pulse")); if (rv) el.appendChild(rv); } return el; } /* ---- глава 3: филология ---- */ function vPhilology() { const c = CONTENT.philology; const id = "philology"; const done = !!state.solved[id]; const el = sheet(` ${head(c.chapter, c.title)} ${paras(c.body)}
${esc(c.memoTitle)}

${esc(c.memoText)}

`); const docs = el.querySelector("#docs"); c.docs.forEach((d) => { const b = document.createElement("button"); b.className = "doc"; b.disabled = done; if (d.id === c.correct) b.dataset.correct = "1"; if (done && d.id === c.correct) b.classList.add("found"); b.innerHTML = `
Документ № ${d.id}${esc(d.head)}
${esc(d.text)}
${esc(d.sign)}
`; b.onclick = () => { if (d.id === c.correct) { Sound.success(); addFact(c.evidence); markSolved(id); render(); } else { Sound.error(); addMistake(id); setFeedback(id, c.wrongReactions[d.id], "bad"); render(); } }; docs.appendChild(b); }); const after = el.querySelector("#after"); if (done) { after.innerHTML = successBlock(c.successTitle, c.successBody, c.evidence); const b = btn(c.button); b.onclick = () => { Sound.click(); go("audio"); }; after.appendChild(row([b])); } else { takeFeedback(el.querySelector("#fb"), id); const rv = revealLink(id, () => el.querySelector('[data-correct="1"]').classList.add("pulse")); if (rv) el.appendChild(rv); } return el; } /* ---- глава 4: аудио ---- */ function vAudio() { const c = CONTENT.audio; const id = "audio"; const done = !!state.solved[id]; const el = sheet(` ${head(c.chapter, c.title)} ${paras(c.body)}
${esc(c.songTitle)}
${esc(c.songAuthor)}
`); const ly = el.querySelector("#lyrics"); c.lyrics.forEach((bl) => { const d = document.createElement("div"); d.className = "lblock " + bl.type; const isBridge = bl.type === "bridge"; /* Первые буквы бриджа намеренно НЕ подсвечиваем: найти акростих — и есть загадка. Блок выделен визуально («куда смотреть»), а «как читать» объясняют подсказки. */ d.innerHTML = `
${esc(bl.label)}${isBridge ? ` ${esc(c.bridgeLabel)}` : ""}
` + bl.lines.map((l) => `

${esc(l)}

`).join(""); ly.appendChild(d); }); const player = el.querySelector("#player"); player.addEventListener("error", () => { player.hidden = true; el.querySelector("#sfb").hidden = false; }); const after = el.querySelector("#after"); if (done) { after.innerHTML = successBlock(c.successTitle, c.successBody, c.evidence); const b = btn(c.button); b.onclick = () => { Sound.click(); go("falseTrail"); }; after.appendChild(row([b])); } else { const input = el.querySelector("#in"); const fb = el.querySelector("#fb"); takeFeedback(fb, id, input); const submit = () => { const v = word(input.value); if (v === word(c.correct)) { Sound.success(); addFact(c.evidence); markSolved(id); render(); return; } Sound.error(); addMistake(id); let msg; if (v === "ожидание" || v === "ожиданье") msg = c.wrongHints.ozhidanie; else if (v === "сюжет") msg = c.wrongHints.syuzhet; else msg = pick(c.wrong, mistakesFor(id) - 1); setFeedback(id, msg, "bad shake", input.value); render(); }; el.querySelector("#go").onclick = submit; input.onkeydown = (e) => { if (e.key === "Enter") submit(); }; const rv = revealLink(id, () => { input.value = c.correct; input.focus(); }); if (rv) el.appendChild(rv); } return el; } /* ---- глава 5: ложный след ---- */ function vFalseTrail() { const c = CONTENT.falseTrail; const id = "falseTrail"; const done = !!state.solved[id]; const total = c.reportParts.filter((p) => p.tell).length; const el = sheet(` ${head(c.chapter, c.title)}
${paras(c.alertBody)}

${esc(c.intro)}

${esc(c.reportTitle)}

печать имеется
${done ? "" : `

${esc(c.task)}

${esc(c.counter)}: ${state.tellsFound.length} / ${total}
`}
`); const rb = el.querySelector("#rbody"); c.reportParts.forEach((p) => { if (p.tell) { const sp = document.createElement("button"); sp.className = "tell" + (state.tellsFound.includes(p.id) ? " got" : ""); sp.textContent = p.t; sp.disabled = done || state.tellsFound.includes(p.id); sp.onclick = () => { if (state.tellsFound.includes(p.id)) return; state.tellsFound.push(p.id); save(); const r = c.tellReactions[p.id]; Sound.success(); if (state.tellsFound.length >= total) { addFact(c.evidence); markSolved(id); } setFeedback(id, r.title + " — " + r.text, "good"); render(); }; rb.appendChild(sp); } else { const sp = document.createElement("span"); sp.className = "plain"; sp.textContent = p.t; if (!done) { sp.onclick = () => { Sound.error(); addMistake(id); setFeedback(id, c.plainReactions[p.id] || "Ничего примечательного.", "bad"); render(); }; } rb.appendChild(sp); } }); const after = el.querySelector("#after"); if (done) { after.innerHTML = successBlock(c.successTitle, c.successBody, c.evidence) + `

${esc(c.kaverzaQuote)}

`; const b = btn(c.button); b.onclick = () => { Sound.click(); go("orderForm"); }; after.appendChild(row([b])); } else { takeFeedback(el.querySelector("#fb"), id); const rv = revealLink(id, () => el.querySelectorAll(".tell").forEach((t) => t.classList.add("pulse"))); if (rv) el.appendChild(rv); } return el; } /* ---- глава 6: форма ордера ---- */ function vOrderForm() { const c = CONTENT.order; const id = "orderForm"; const el = sheet(` ${head(c.chapter, c.title)} ${paras(c.body)}
`); const form = el.querySelector("#form"); c.fields.forEach((f) => { const ok = !!state.orderFilled[f.id]; const prev = pendingOrder[f.id] || {}; const shown = ok ? FACTS[f.id] : (prev.value || ""); const g = document.createElement("div"); g.className = "fgroup" + (ok ? " ok" : ""); g.innerHTML = `
${ok ? "✓" : ""}
источник: ${esc(f.hintSource)}
${ok ? "" : esc(prev.error || "")}
`; form.appendChild(g); }); pendingOrder = {}; const submitAll = () => { let anyWrong = false; const carry = {}; c.fields.forEach((f) => { if (state.orderFilled[f.id]) return; const inp = el.querySelector("#f_" + f.id); const v = inp.value.trim(); if (v === "") return; if (matchesFact(f.id, v)) { state.orderFilled[f.id] = true; save(); } else { anyWrong = true; carry[f.id] = { value: inp.value, error: f.wrong }; } }); const filled = c.fields.every((f) => state.orderFilled[f.id]); if (filled) { Sound.stamp(); markSolved(id); go("twist"); return; } pendingOrder = carry; if (anyWrong) { Sound.error(); addMistake(id); } else { Sound.click(); } render(); if (!anyWrong) toast(c.partial); }; const b = btn(c.button); b.onclick = submitAll; el.appendChild(row([b])); el.querySelectorAll(".fgroup input").forEach((i) => { i.onkeydown = (e) => { if (e.key === "Enter") submitAll(); }; }); const rv = revealLink(id, () => { c.fields.forEach((f) => { const inp = el.querySelector("#f_" + f.id); if (inp && !inp.disabled) inp.value = FACTS[f.id]; }); }); if (rv) el.appendChild(rv); return el; } /* ---- твист ---- */ function vTwist() { const c = CONTENT.twist; const el = document.createElement("div"); el.className = "twist"; const box = document.createElement("div"); box.className = "twist-lines"; el.appendChild(box); c.lines.forEach((l, i) => { const p = document.createElement("p"); p.textContent = l; p.style.animationDelay = i * 1.05 + "s"; box.appendChild(p); }); const t = c.lines.length * 1050; setTimeout(() => Sound.stamp(), t + 200); setTimeout(() => { const st = document.createElement("div"); st.className = "bigstamp"; st.textContent = c.stamp; el.appendChild(st); const b = btn(c.button); b.onclick = () => { Sound.warm(); go("congrats"); }; const r = row([b]); r.classList.add("center"); el.appendChild(r); }, t + 450); return el; } /* ---- поздравление ---- */ function vCongrats() { const c = CONTENT.congrats; const el = document.createElement("div"); el.className = "center-wrap"; el.innerHTML = `

${esc(c.title)}

${paras(c.body)}
`; const b = btn(c.button); b.onclick = () => { Sound.click(); go("orderScreen"); }; const r = row([b]); r.classList.add("center"); el.querySelector(".sheet").appendChild(r); return el; } /* ---- ордер и QR ---- */ function vOrderScreen() { const c = CONTENT.orderScreen; const el = document.createElement("div"); el.className = "center-wrap"; el.innerHTML = `

${esc(c.title)}

${c.rows.map(([k, v]) => `
${esc(k)}${esc(v)}
`).join("")}

${esc(c.qrNote)}

Адрес выдачи${esc(CONFIG.pvzAddress)}
Кодовое слово${esc(CONFIG.pickupCode)}
Дело№ ${esc(CONFIG.caseNumber)}
${esc(c.claimTitle)}

${esc(c.claimLead)}

    ${c.claimSteps.map((s) => `
  1. ${esc(s)}
  2. `).join("")}

${esc(c.claimNote)}

${esc(c.joke)}

${esc(c.reserveNote)}

`; try { /* Держим текст QR коротким: чем он длиннее, тем плотнее код и тем хуже он ловится камерой. Подробная инструкция — на экране рядом. */ const text = CONFIG.pvzPageUrl || `${CONFIG.pvzAddress}. Кодовое слово: ${CONFIG.pickupCode}. Дело №${CONFIG.caseNumber}.`; const q = qrcode(0, "M"); q.addData(text); q.make(); el.querySelector("#qr").innerHTML = q.createSvgTag(6, 8); } catch (e) { el.querySelector("#qr").innerHTML = '

QR не сформирован. Используйте данные ниже.

'; } const cp = btn(c.copyButton, "ghost"); cp.onclick = () => { const t = `Адрес выдачи: ${CONFIG.pvzAddress}\n` + `Кодовое слово: ${CONFIG.pickupCode} (напишите его мне в ЛС — пришлю штрихкод)\n` + `Дело № ${CONFIG.caseNumber}`; copyText(t); }; const r = row([cp]); r.classList.add("center"); el.querySelector(".order-final").appendChild(r); return el; } render(); })();