1081 lines
41 KiB
JavaScript
1081 lines
41 KiB
JavaScript
/* ============================================================
|
||
ДЕЛО № 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 === "" ? '<div class="vspace"></div>' : `<p>${esc(l)}</p>`))
|
||
.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 = `<span class="fb-mark"></span><span>${esc(f.text)}</span>`;
|
||
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 = `
|
||
<div class="tb-left">
|
||
<span class="tb-logo">ГИС «ПАПКА»</span>
|
||
<span class="tb-case">${esc(caseLabel)}</span>
|
||
</div>
|
||
<div class="tb-right">
|
||
<button class="chip" data-a="journal">Журнал<span class="chip-n">${state.journal.length}</span></button>
|
||
<button class="chip" data-a="hint"${hintable ? "" : " disabled"}>Подсказка</button>
|
||
<button class="chip chip-i" data-a="sound" title="звук">${state.muted ? "🔇" : "🔊"}</button>
|
||
</div>`;
|
||
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 `<div class="rail-step${done ? " done" : ""}${cur ? " cur" : ""}">
|
||
<span class="rail-dot">${done ? "✓" : i + 1}</span>
|
||
<span class="rail-label">${esc(c.label)}</span>
|
||
</div>`;
|
||
}).join("");
|
||
return el;
|
||
}
|
||
|
||
function dock() {
|
||
const e = CONTENT.easter;
|
||
const el = document.createElement("div");
|
||
el.className = "dock";
|
||
el.innerHTML = `
|
||
<button class="dk" data-a="speed">${esc(e.speedButton)}</button>
|
||
<button class="dk" data-a="confused">${esc(e.confusedButton)}</button>
|
||
<button class="dk" data-a="complaint">${esc(e.complaintButton)}</button>
|
||
<button class="dk" data-a="reset">${esc(e.resetButton)}</button>`;
|
||
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 = `
|
||
<aside class="panel ${cls || ""}">
|
||
<div class="panel-head"><h3>${esc(title)}</h3><button class="panel-x" data-a="x">×</button></div>
|
||
<div class="panel-body">${bodyHtml}</div>
|
||
</aside>`;
|
||
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
|
||
? '<p class="dim">Журнал пуст. Следствие в самом начале.</p>'
|
||
: `<p class="dim panel-lead">Установленные факты. В финале понадобятся все.</p>` +
|
||
state.journal.map((j) => `
|
||
<div class="jcard">
|
||
<div class="jcard-name">${esc(j.name)}</div>
|
||
<div class="jcard-fact"><span class="jcard-label">${esc(j.factLabel)}</span><b>${esc(j.fact)}</b></div>
|
||
<div class="jcard-desc">${esc(j.desc)}</div>
|
||
</div>`).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 = `
|
||
<p class="panel-lead dim">${esc(pick(COMMON.hintIntro, lvl))}</p>
|
||
${cfg.hints.slice(0, lvl).map((h, i) => `<p class="hint"><b>${i + 1}.</b> ${esc(h)}</p>`).join("")}
|
||
${lvl === 0 ? '<p class="dim">Подсказка ещё не раскрыта. Нажмите ещё раз.</p>' : ""}`;
|
||
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 `<div class="sheet-head">
|
||
<div class="chapter">${esc(chapter)}</div>
|
||
<h2>${esc(title)}</h2>
|
||
</div>`;
|
||
}
|
||
/* Блок успеха с карточкой добытого факта */
|
||
function successBlock(title, text, ev) {
|
||
return `<div class="success">
|
||
<div class="success-head"><span class="tick">✓</span>${esc(title)}</div>
|
||
<p>${esc(text)}</p>
|
||
${ev ? `<div class="jcard inline">
|
||
<div class="jcard-name">${esc(ev.name)}</div>
|
||
<div class="jcard-fact"><span class="jcard-label">${esc(ev.factLabel)}</span><b>${esc(ev.fact)}</b></div>
|
||
</div>` : ""}
|
||
</div>`;
|
||
}
|
||
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("<p>Раздел ушёл в отпуск по сюжетным обстоятельствам.</p>");
|
||
}
|
||
}
|
||
|
||
/* ---- интро ---- */
|
||
function vIntro() {
|
||
const c = CONTENT.intro;
|
||
const el = document.createElement("div");
|
||
el.className = "center-wrap";
|
||
el.innerHTML = `
|
||
<div class="sheet intro">
|
||
<div class="chapter red">${esc(c.tag)}</div>
|
||
<h1>${esc(c.title)}</h1>
|
||
${paras(c.body)}
|
||
<p class="note">${esc(c.footnote)}</p>
|
||
</div>`;
|
||
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 = `
|
||
<h2 class="boot-title">${esc(c.system)}</h2>
|
||
<p class="boot-sub">${esc(c.subtitle)}</p>
|
||
<div class="boot-lines"></div>`;
|
||
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)}
|
||
<div class="excerpt">
|
||
<div class="excerpt-h">${esc(c.ruleTitle)}</div>
|
||
<p>${esc(c.ruleText)}</p>
|
||
</div>
|
||
<div class="record">
|
||
<div class="record-h">${esc(c.registryTitle)}</div>
|
||
${c.registryRows.map(([k, v]) => `<div class="rec-row"><span>${esc(k)}</span><b>${esc(v)}</b></div>`).join("")}
|
||
</div>
|
||
<div class="inrow">
|
||
<input id="in" type="text" placeholder="${esc(c.placeholder)}" autocomplete="off" inputmode="numeric" />
|
||
<button class="btn primary" id="go">${esc(c.button)}</button>
|
||
</div>
|
||
<div class="feedback" id="fb"></div>`);
|
||
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)}
|
||
<div class="two-col">
|
||
<div class="record">
|
||
<div class="record-h">${esc(c.protocolTitle)}</div>
|
||
${c.inventory.map(([n, d]) => `<div class="inv-row"><span class="inv-n">${esc(n)}</span><span>${esc(d)}</span></div>`).join("")}
|
||
<div class="record-note">${esc(c.protocolNote)}</div>
|
||
</div>
|
||
<div class="scene-box">${desk()}</div>
|
||
</div>
|
||
<div class="feedback" id="fb"></div>
|
||
<div id="after"></div>`, "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) => `
|
||
<g class="hit" data-id="${id}" transform="translate(${x},${y})">
|
||
${inner}
|
||
<g class="tag" transform="translate(0,${h + 6})">
|
||
<rect x="0" y="0" width="${Math.max(46, tag.length * 7.2)}" height="15" rx="2"/>
|
||
<text x="4" y="11">${tag}</text>
|
||
</g>
|
||
</g>`;
|
||
|
||
return `
|
||
<svg viewBox="0 0 560 420" class="desk" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Стол с предметами">
|
||
<defs>
|
||
<linearGradient id="wood" x1="0" y1="0" x2="0" y2="1">
|
||
<stop offset="0" stop-color="#5b4632"/><stop offset="1" stop-color="#43331f"/>
|
||
</linearGradient>
|
||
</defs>
|
||
<rect width="560" height="420" fill="url(#wood)"/>
|
||
<g opacity=".08" stroke="#000">
|
||
${Array.from({ length: 14 }, (_, i) => `<path d="M0 ${i * 30} H560" stroke-width="1"/>`).join("")}
|
||
</g>
|
||
|
||
${item("folder", 26, 26, 128, 82, "БКТО-14",
|
||
`<rect width="128" height="82" rx="3" fill="#cbb389" stroke="#8b7550"/>
|
||
<rect x="0" y="0" width="128" height="16" rx="3" fill="#b89f74"/>
|
||
<text x="9" y="42" class="lbl">ДЕЛО №</text>
|
||
<text x="9" y="60" class="lbl">${esc(CONFIG.caseNumber)}</text>`)}
|
||
|
||
${item("mug", 196, 40, 54, 54, "БКТО-12",
|
||
`<rect width="54" height="54" rx="8" fill="#e9dcc2" stroke="#b4a17c"/>
|
||
<path d="M54 16 q14 11 0 24" fill="none" stroke="#b4a17c" stroke-width="4"/>
|
||
<text x="6" y="31" class="lbl-s">НЕ</text>
|
||
<text x="6" y="44" class="lbl-s">ТРОГАТЬ</text>`)}
|
||
|
||
${item("calendar", 292, 30, 84, 68, "БКТО-13",
|
||
`<rect width="84" height="68" rx="3" fill="#eee3cb" stroke="#b4a17c"/>
|
||
<rect width="84" height="15" rx="3" fill="#b33a3a"/>
|
||
<text x="26" y="52" class="num">26</text>
|
||
<circle cx="41" cy="45" r="18" fill="none" stroke="#b33a3a" stroke-width="2.5"/>`)}
|
||
|
||
${item("lamp", 424, 22, 76, 96, "БКТО-11",
|
||
`<ellipse cx="38" cy="26" rx="38" ry="24" fill="#3f7a52"/>
|
||
<ellipse cx="38" cy="26" rx="26" ry="15" fill="#d9a441" opacity=".85"/>
|
||
<rect x="33" y="46" width="10" height="42" fill="#7d8892"/>
|
||
<rect x="18" y="88" width="40" height="8" rx="3" fill="#7d8892"/>`)}
|
||
|
||
${item("pencil", 34, 190, 116, 12, "БКТО-15",
|
||
`<rect y="2" width="100" height="9" rx="3" fill="#d9a441"/>
|
||
<path d="M100 2 l16 5.5 l-16 5.5z" fill="#e9dcc2"/>`)}
|
||
|
||
${item("paper", 176, 168, 170, 92, "БКТО-17",
|
||
`<rect width="170" height="92" fill="#f4ecd8" stroke="#b33a3a" stroke-width="1.5"/>
|
||
<text x="12" y="34" class="lbl-s">ПЕЧАТЬ</text>
|
||
<text x="12" y="52" class="lbl-s">НЕ ТРОГАТЬ</text>
|
||
<text x="12" y="76" class="lbl-xs">(печать отсутствует)</text>`)}
|
||
|
||
${item("cactus", 424, 176, 54, 84, "БКТО-16",
|
||
`<rect x="12" y="62" width="30" height="22" rx="3" fill="#a6714a"/>
|
||
<path d="M27 62 V16 q0-10 5-10 t5 10 v10" fill="none" stroke="#5c8a5c" stroke-width="9" stroke-linecap="round"/>
|
||
<path d="M27 44 q-14 0 -14 -14" fill="none" stroke="#5c8a5c" stroke-width="8" stroke-linecap="round"/>`)}
|
||
|
||
${item("box", 246, 306, 104, 66, "ДС-04",
|
||
`<rect width="104" height="66" rx="2" fill="#c19a6b" stroke="#8a6a44"/>
|
||
<path d="M0 22 H104" stroke="#8a6a44" stroke-width="1.5"/>
|
||
<rect x="40" y="0" width="24" height="66" fill="#b08a5e" opacity=".7"/>
|
||
<text x="8" y="46" class="lbl-xs">хрупкое. сюжет внутри</text>`)}
|
||
</svg>`;
|
||
}
|
||
|
||
/* ---- глава 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)}
|
||
<div class="record log">
|
||
<div class="record-h">${esc(c.logTitle)}</div>
|
||
<div class="log-grid">
|
||
${c.logRows.map(([t, n, d]) => `
|
||
<div class="log-row">
|
||
<span class="log-t">${esc(t)}</span>
|
||
<span class="log-n">${esc(n)}</span>
|
||
<span class="log-d ${d === "выход" ? "out" : "in"}">${esc(d)}</span>
|
||
</div>`).join("")}
|
||
</div>
|
||
</div>
|
||
<div class="cards" id="cards"></div>
|
||
<div class="feedback" id="fb"></div>
|
||
<div id="after"></div>`);
|
||
|
||
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 = `
|
||
<div class="pcard-top"><span class="pcard-name">${esc(w.name)}</span><span class="pcard-role">${esc(w.role)}</span></div>
|
||
<div class="pcard-text">${esc(w.text)}</div>`;
|
||
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)}
|
||
<div class="memo">
|
||
<div class="memo-h">${esc(c.memoTitle)}</div>
|
||
<p>${esc(c.memoText)}</p>
|
||
</div>
|
||
<div class="docs" id="docs"></div>
|
||
<div class="feedback" id="fb"></div>
|
||
<div id="after"></div>`);
|
||
|
||
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 = `
|
||
<div class="doc-h"><span class="doc-n">Документ № ${d.id}</span><span class="doc-title">${esc(d.head)}</span></div>
|
||
<div class="doc-t">${esc(d.text)}</div>
|
||
<div class="doc-sign">${esc(d.sign)}</div>`;
|
||
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)}
|
||
<div class="song">
|
||
<div class="song-h">
|
||
<div><div class="song-t">${esc(c.songTitle)}</div><div class="song-a">${esc(c.songAuthor)}</div></div>
|
||
</div>
|
||
<audio id="player" controls preload="none" src="${esc(CONFIG.songSrc)}"></audio>
|
||
<p class="song-fb" id="sfb" hidden>Аудиозапись технически недоступна. Но текст перед вами — это тоже документ.</p>
|
||
<div class="lyrics" id="lyrics"></div>
|
||
</div>
|
||
<div class="inrow">
|
||
<input id="in" type="text" placeholder="${esc(c.placeholder)}" autocomplete="off"${done ? " disabled" : ""} />
|
||
<button class="btn primary" id="go"${done ? " disabled" : ""}>${esc(c.button)}</button>
|
||
</div>
|
||
<div class="feedback" id="fb"></div>
|
||
<div id="after"></div>`);
|
||
|
||
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 = `<div class="lblock-h">${esc(bl.label)}${isBridge ? ` <span class="badge">${esc(c.bridgeLabel)}</span>` : ""}</div>` +
|
||
bl.lines.map((l) => `<p>${esc(l)}</p>`).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)}
|
||
<div class="alert">${paras(c.alertBody)}</div>
|
||
<p>${esc(c.intro)}</p>
|
||
<div class="report">
|
||
<div class="report-h">${esc(c.reportTitle)}</div>
|
||
<p class="report-body" id="rbody"></p>
|
||
<div class="report-seal">печать имеется</div>
|
||
</div>
|
||
${done ? "" : `<div class="task"><p>${esc(c.task)}</p>
|
||
<div class="counter">${esc(c.counter)}: <b id="cnt">${state.tellsFound.length}</b> / ${total}</div></div>`}
|
||
<div class="feedback" id="fb"></div>
|
||
<div id="after"></div>`);
|
||
|
||
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) +
|
||
`<p class="quote">${esc(c.kaverzaQuote)}</p>`;
|
||
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)}
|
||
<div class="form" id="form"></div>
|
||
<div class="feedback" id="fb"></div>`);
|
||
|
||
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 = `
|
||
<label for="f_${f.id}">${esc(f.label)}</label>
|
||
<div class="fline">
|
||
<input id="f_${f.id}" type="text" placeholder="${esc(f.placeholder)}" autocomplete="off"
|
||
value="${esc(shown)}"${ok ? " disabled" : ""} />
|
||
<span class="fmark">${ok ? "✓" : ""}</span>
|
||
</div>
|
||
<div class="fsrc">источник: ${esc(f.hintSource)}</div>
|
||
<div class="ferr" id="e_${f.id}">${ok ? "" : esc(prev.error || "")}</div>`;
|
||
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 = `<div class="sheet warm-sheet"><h1>${esc(c.title)}</h1>${paras(c.body)}</div>`;
|
||
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 = `
|
||
<div class="sheet warm-sheet order-final">
|
||
<h1>${esc(c.title)}</h1>
|
||
<div class="record">
|
||
${c.rows.map(([k, v]) => `<div class="rec-row"><span>${esc(k)}</span><b>${esc(v)}</b></div>`).join("")}
|
||
</div>
|
||
<p class="center-t">${esc(c.qrNote)}</p>
|
||
<div class="qr" id="qr"></div>
|
||
<div class="record pickup">
|
||
<div class="rec-row"><span>Адрес выдачи</span><b>${esc(CONFIG.pvzAddress)}</b></div>
|
||
<div class="rec-row"><span>Кодовое слово</span><b>${esc(CONFIG.pickupCode)}</b></div>
|
||
<div class="rec-row"><span>Дело</span><b>№ ${esc(CONFIG.caseNumber)}</b></div>
|
||
</div>
|
||
<div class="claim">
|
||
<div class="claim-h">${esc(c.claimTitle)}</div>
|
||
<p class="claim-lead">${esc(c.claimLead)}</p>
|
||
<ol class="claim-steps">
|
||
${c.claimSteps.map((s) => `<li>${esc(s)}</li>`).join("")}
|
||
</ol>
|
||
<p class="claim-note">${esc(c.claimNote)}</p>
|
||
</div>
|
||
<p class="quote">${esc(c.joke)}</p>
|
||
<p class="note">${esc(c.reserveNote)}</p>
|
||
</div>`;
|
||
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 = '<p class="dim">QR не сформирован. Используйте данные ниже.</p>';
|
||
}
|
||
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();
|
||
})();
|