/* ============================================================
ДЕЛО № 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)}
Журнал${state.journal.length}
Подсказка
${state.muted ? "🔇" : "🔊"}
`;
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 = `
${esc(e.speedButton)}
${esc(e.confusedButton)}
${esc(e.complaintButton)}
${esc(e.resetButton)} `;
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 = `
${esc(title)} ×
${bodyHtml}
`;
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("")}
${esc(c.button)}
`);
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)}
Аудиозапись технически недоступна. Но текст перед вами — это тоже документ.
${esc(c.button)}
`);
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 = `
${esc(f.label)}
${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) => `${esc(s)} `).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();
})();