This commit is contained in:
Freestyle-Play
2026-08-25 18:22:36 +03:00
commit 60991875c0
7 changed files with 5165 additions and 0 deletions

119
README.md Normal file
View File

@@ -0,0 +1,119 @@
# Дело № 260826. Исчезновение подарка — v2
Интерактивный веб-квест ко дню рождения. Чистые HTML/CSS/JS, без сборки и
серверной части.
Это переработанная версия. Папка `../quest/` (v1) оставлена нетронутой
для сравнения.
## Быстрый старт
1. Откройте `data.js` и заполните блок `CONFIG` вверху файла:
```js
const CONFIG = {
name: "Алиса", // имя именинницы
caseNumber: "260826", // номер дела = дата в формате ДДММГГ
caseDateWords: "двадцать шестое августа две тысячи двадцать шестого года",
pvzAddress: "Москва, Кочновский пр-д, 4 к. 1. ПВЗ Озон",
pickupCode: "АРБУЗ", // кодовое слово для запроса штрихкода
pvzPageUrl: "", // необязательно: ссылка для QR
songSrc: "assets/song.mp3", // необязательно: аудио песни
};
```
**Важно:** `caseNumber` и `caseDateWords` должны совпадать — номер дела
выводится игроком из даты, записанной прописью. Если меняете дату,
поменяйте оба поля.
2. (Необязательно) Положите mp3 в `assets/song.mp3`. Без файла плеер сам
скроется, текст песни останется — загадка решается по тексту.
3. Откройте `index.html` или запустите статический сервер:
```bash
python -m http.server 8792
```
## Что изменилось по сравнению с v1
Главная претензия к v1: загадки выглядели как загадки, но не требовали
думать — ответ почти всегда лежал на том же экране.
| Этап | Было (v1) | Стало (v2) |
|---|---|---|
| Номер дела | Просто перепечатать число из уведомления | Вывести из даты прописью по правилу ДДММГГ |
| Место происшествия | Пиксель-хант: клик по еле заметному овалу | Сверить инвентарные бирки на столе со списком в описи |
| Допрос | Свидетель дословно повторял текст улики | Построить таймлайн по журналу турникета и найти физически невозможное показание |
| Аудио | Ответ «сюжет» был напечатан в припеве дважды | Слова `ПАУЗА` в песне нет: акростих надо собрать, а припев подсовывает ложное «ожидание» |
| Ложный след | Тот же ответ, что и в филологии — повтор | Два независимых признака подделки: противоречие журналу и падеж; искать в сплошном тексте |
| Финал | Журнал улик прямо показывал П-О-Д-А-Р-О-К | Заполнить форму ордера из трёх граф, каждая — из своей главы |
Ключевое изменение: **журнал дела из спойлера стал рабочим блокнотом**.
Он копит установленные факты (`09:08`, `ДС-04`, `ПАУЗА`), которые
понадобятся в финальной форме.
## Ответы (шпаргалка)
| Глава | Ответ |
|---|---|
| Допуск | `260826` (из даты прописью) |
| Опись | коробка с биркой `ДС-04` (единственный предмет вне описи) |
| Показания | Клавдия Ивановна Шлюз (вышла 09:02, вернулась 09:12, а «была на посту в 09:07») |
| Экспертиза | Документ № 3 («согласно приказа» вместо «приказу») |
| Запись | `ПАУЗА` (первые буквы пяти строк бриджа) |
| Ложный след | `в 09:05` (Каверза вошёл только в 09:15) и `согласно приказа` |
| Ордер | `09:08` · `ДС-04` · `ПАУЗА` |
Ввод терпим к формату: время принимает `9:08`, `09.08`, `0908`; бирка —
`дс 04`, `ДС-04`; регистр везде не важен.
## Финальный экран и выдача подарка
Штрихкод намеренно **не показывается в квесте**. Последний шаг —
написать организатору:
1. Именинница отправляет в личные сообщения кодовое слово `АРБУЗ`.
2. В ответ вы присылаете штрихкод заказа.
3. Она показывает штрихкод на ПВЗ по адресу с экрана.
Это встроено в сюжет как последняя бюрократическая процедура: «Бюро не
хранит штрихкоды в открытом виде — они выдаются ответственному лицу под
расписку». Инструкция выведена нумерованным списком под ордером.
QR-код кодирует адрес, кодовое слово и номер дела короткой строкой,
чтобы код оставался разреженным и уверенно ловился камерой. Если указан
`pvzPageUrl`, QR ведёт на эту страницу вместо текста.
Адрес и кодовое слово продублированы обычным текстом, плюс кнопка
копирования — с запасным путём на случай, когда Clipboard API
недоступен (например, при открытии страницы файлом).
## Структура
```
index.html — точка входа
styles.css — оформление
data.js — CONFIG, FACTS и весь текст (правьте здесь)
app.js — логика: состояния экранов, журнал, подсказки, звук
vendor/qrcode.js — MIT-библиотека генерации QR (kazuhikoarase/qrcode-generator)
assets/ — сюда song.mp3 (опционально)
```
## Механики помощи
- Кнопка **«Журнал»** — все установленные факты, нужны в финале.
- Кнопка **«Подсказка»** — до трёх уровней на каждую загадку, от намёка
до прямого ответа.
- Подсказки открываются и сами: после 3, 5 и 8 неверных попыток.
- После 8 ошибок появляется ссылка «Следствие настаивает на правильном
ответе», которая подсвечивает или подставляет ответ. Застрять нельзя.
- Прогресс сохраняется автоматически; «Начать сначала» — внизу экрана,
требует двойного нажатия.
## Проверено
Полное прохождение в Chrome: все главы, неверные ответы, ловушки,
подсказки, журнал, финал с QR. Консоль чистая. Мобильная вёрстка
проверена на ширине 361px — переполнения нет, стол со сцены получает
горизонтальную прокрутку, чтобы бирки оставались читаемыми.

1080
app.js Normal file

File diff suppressed because it is too large Load Diff

546
data.js Normal file
View File

@@ -0,0 +1,546 @@
/* ============================================================
ДЕЛО № 260826. ИСЧЕЗНОВЕНИЕ ПОДАРКА — версия 2
Конфигурация и содержимое.
Правьте блок CONFIG — остальное трогать не обязательно.
============================================================ */
const CONFIG = {
name: "Алиса",
caseNumber: "260826",
caseDateWords: "двадцать шестое августа две тысячи двадцать шестого года",
pvzAddress: "Москва, Кочновский пр-д, 4 к. 1. ПВЗ Озон",
/* Штрихкод не показывается в квесте: именинница присылает это кодовое
слово в личные сообщения, а штрихкод отправляется в ответ. */
pickupCode: "АРБУЗ",
pvzPageUrl: "",
songSrc: "assets/song.mp3",
};
/* Три факта, которые игрок добывает по ходу дела и вписывает
в финальную форму ордера. Меняются только вместе с загадками. */
const FACTS = {
exitTime: "09:08", // глава 2: журнал турникета
foreignTag: "ДС-04", // глава 1: бирка сторонней службы
codeWord: "ПАУЗА", // глава 4: акростих бриджа
};
const COMMON = {
hintIntro: [
"Следствие разрешает воспользоваться подсказкой. Это не считается поражением. Хотя статистика говорит об обратном.",
"Подсказка выдана. Бюро делает вид, что это было предусмотрено.",
"Следствие идёт навстречу. Один раз. И ещё один. И, кажется, ещё.",
],
wrongGeneric: [
"Ответ не подтверждён. Следователь сделал вид, что так и задумано.",
"Нет. Но уверенность впечатляет.",
"Такая версия рассматривалась ещё в 1974 году. Не спрашивайте почему.",
"Система вежливо не согласна. И просит передать это деликатно.",
"Ошибка. Один из документов тихо извинился.",
"Не подтверждается. Но в отчёте можно написать «смелая гипотеза».",
],
};
const CONTENT = {
/* ---------- ПРОЛОГ ---------- */
intro: {
tag: "УВЕДОМЛЕНИЕ БКТО",
title: "Объект задержан",
body: [
"В отношении объекта, предназначавшегося вам в качестве подарка, открыто дело.",
"Статус объекта: ВРЕМЕННО ОГРАНИЧЕН.",
"Прямое раскрытие сюрприза запрещено регламентом. Всё, что вам могут дать, — документы.",
],
button: "Принять дело к производству",
footnote: "Отказ не предусмотрен. День рождения уже в процессе.",
},
loadingScreen: {
system: "ГИС «ПАПКА»",
subtitle: "Подарочный Архив · Праздничный Контроль · Финальная Торжественность · Абсурд",
lines: [
"Проверка праздничного статуса…",
"Сверка папок…",
"Папки найдены. Одна моргнула.",
"Загрузка материалов дела…",
],
button: "Войти в систему",
},
/* ---------- ГЛАВА 0. НОМЕР ДЕЛА ---------- */
caseNumber: {
chapter: "Допуск",
title: "ПОДТВЕРЖДЕНИЕ ДОСТУПА",
body: [
"Доступ к материалам открыт только получателю объекта.",
"Номер дела не сообщается напрямую: он формируется автоматически.",
],
ruleTitle: "ВЫПИСКА ИЗ РЕГЛАМЕНТА, П. 4.2",
ruleText: "Номер дела образуется из даты регистрации в формате ДДММГГ, без разделителей.",
registryTitle: "КАРТОЧКА РЕГИСТРАЦИИ",
registryRows: [
["Заявитель", "под грифом ДРУЖБА"],
["Дата регистрации", CONFIG.caseDateWords],
["Предмет", "объект торжественного назначения, 1 шт."],
],
placeholder: "Номер дела",
button: "Открыть дело",
wrong: [
"Номер дела не совпадает. Бюрократия тихо плачет в папку.",
"Дело с таким номером существует, но оно про кота. Не ваше.",
"Не сходится. Регламент напоминает: сначала день, потом месяц, потом год. Все по две цифры.",
],
hints: [
"Дата регистрации записана прописью. Переведите её в цифры.",
"Формат ДДММГГ: день, месяц и год — каждый ровно двумя цифрами, подряд, без точек.",
"Двадцать шестое августа две тысячи двадцать шестого — это " + CONFIG.caseNumber + ".",
],
},
caseOpened: {
title: "ДЕЛО ОТКРЫТО",
body: [
"Вам присвоен статус: временный следователь по собственному делу.",
"",
"Права: ошибаться, сомневаться, запрашивать подсказки, быть именинницей.",
"Ограничения: нельзя пропустить день рождения.",
"",
"Бюро обращает внимание: всё, что вы установите, попадёт в журнал дела. В финале журнал понадобится целиком.",
],
button: "Осмотреть место происшествия",
},
/* ---------- ГЛАВА 1. ОПИСЬ ---------- */
scene: {
chapter: "Глава 1",
title: "МЕСТО ПРОИСШЕСТВИЯ",
body: [
"Снимок рабочего стола, сделанный после исчезновения объекта.",
"Каждый предмет Бюро снабжает инвентарной биркой. Все предметы отдела имеют префикс БКТО.",
"",
"Задача: найти на столе предмет, которого нет в описи.",
],
protocolTitle: "ОПИСЬ ИМУЩЕСТВА ОТДЕЛА",
protocolNote: "Проверено вручную. Примерно.",
inventory: [
["БКТО-11", "лампа настольная, зелёная"],
["БКТО-12", "кружка с надписью «НЕ ТРОГАТЬ»"],
["БКТО-13", "календарь настенный"],
["БКТО-14", "папка дела"],
["БКТО-15", "карандаш"],
["БКТО-16", "кактус (эмоционально устойчив)"],
["БКТО-17", "лист с надписью «ПЕЧАТЬ НЕ ТРОГАТЬ»"],
],
correctId: "box",
successTitle: "Предмет вне описи обнаружен",
successBody: "Картонная коробка с биркой " + FACTS.foreignTag + ". Префикс не принадлежит Бюро: это служба доставки «Доставка с чувством». Коробка приехала снаружи — и, судя по всему, уехала обратно.",
reactions: {
lamp: "Бирка БКТО-11. Лампа в описи. Лампа отрицает участие, но свет у неё подозрительно уверенный.",
mug: "Бирка БКТО-12. Кружка в описи. Уликой не является, но морально поддерживает следствие.",
calendar: "Бирка БКТО-13. Календарь в описи. Число 26 обведено дважды — это личное дело календаря.",
folder: "Бирка БКТО-14. Папка уже в деле. Не заставляйте её волноваться повторно.",
pencil: "Бирка БКТО-15. Карандаш в описи. Готов сотрудничать, но ему нечего сказать.",
cactus: "Бирка БКТО-16. Кактус в описи и от показаний отказывается, ссылаясь на колючий характер.",
paper: "Бирка БКТО-17. Лист в описи. Надпись «ПЕЧАТЬ НЕ ТРОГАТЬ» соблюдена. Формально.",
},
hints: [
"Сверяйте бирки на предметах со списком в описи. Один предмет в списке отсутствует.",
"Бюро использует только префикс БКТО. На одной бирке префикс другой.",
"Картонная коробка справа внизу помечена «" + FACTS.foreignTag + "» — это чужая служба. Нажмите на неё.",
],
evidence: {
name: "КОРОБКА",
fact: FACTS.foreignTag,
factLabel: "Инвентарный признак сторонней службы",
desc: "Картонная коробка службы «Доставка с чувством». В опись Бюро не входит.",
},
button: "Приобщить к делу",
},
/* ---------- ГЛАВА 2. ЖУРНАЛ И ПОКАЗАНИЯ ---------- */
witnesses: {
chapter: "Глава 2",
title: "СВЕРКА ПОКАЗАНИЙ",
body: [
"Коробку кто-то вынес. Бюро подняло журнал турникета и опросило четверых.",
"",
"Задача: найти свидетеля, чьи показания физически невозможны — то есть противоречат журналу проходов.",
],
logTitle: "ЖУРНАЛ ТУРНИКЕТА · 26 АВГУСТА",
logRows: [
["08:41", "Пылинкин А.", "вход"],
["09:02", "Шлюз К. И.", "выход"],
["09:06", "Быстрый Т.", "вход"],
["09:08", "Быстрый Т.", "выход"],
["09:12", "Шлюз К. И.", "вход"],
["09:15", "Каверза Л. П.", "вход"],
],
cards: [
{
id: "klavdia",
name: "Клавдия Ивановна Шлюз",
role: "вахтёрша",
text: "«В 09:07 я была на посту. Мимо меня никто не проходил — ни туда, ни обратно. У меня глаз профессиональный».",
},
{
id: "pylinkin",
name: "Архивариус Пылинкин",
role: "младший хранитель бумажных сущностей",
text: "«Около девяти мимо прошёл человек в шарфе. Папка при этом моргнула. Я доложил. Папке стало легче».",
},
{
id: "timur",
name: "Тимур Быстрый",
role: "курьер службы «Доставка с чувством»",
text: "«Пришёл, забрал коробку, ушёл. Две минуты, не больше. Коробка была лёгкая. Подозрительно лёгкая».",
},
{
id: "kaverza",
name: "Л. П. Каверза",
role: "филолог-консультант",
text: "«Когда я вошёл, на столе уже ничего не было. И сразу уточню: не “украдено”, а “изъято”. Это разные вещи».",
},
],
correct: "klavdia",
successTitle: "Противоречие установлено",
successBody: "По журналу Шлюз К. И. вышла в 09:02 и вернулась в 09:12. В 09:07 её не было в здании — значит, видеть коридор она не могла. Свидетель ошибается, не становясь преступником.",
wrongReactions: {
pylinkin: "Пылинкин вошёл в 08:41 и не выходил. Около девяти он действительно был внутри — журналу это не противоречит. Странность показаний ещё не делает их невозможными.",
timur: "Быстрый вошёл в 09:06 и вышел в 09:08. Ровно две минуты, как он и говорит. Журнал подтверждает его слово в точности.",
kaverza: "Каверза вошёл в 09:15 — уже после того, как коробку вынесли. Он и правда мог застать пустой стол. Противоречия нет.",
},
hints: [
"Возьмите время из каждого показания и проверьте по журналу, был ли этот человек в здании в тот момент.",
"Один свидетель называет время, когда он, судя по журналу, находился снаружи.",
"Шлюз вышла в 09:02 и вернулась в 09:12 — а утверждает, что была на посту в 09:07.",
],
evidence: {
name: "ЖУРНАЛ",
fact: FACTS.exitTime,
factLabel: "Время выхода объекта из здания",
desc: "Коробку вынес курьер Быстрый Т. Турникет зафиксировал выход в " + FACTS.exitTime + ".",
},
button: "Приобщить к делу",
},
/* ---------- ГЛАВА 3. ФИЛОЛОГИЯ ---------- */
philology: {
chapter: "Глава 3",
title: "ФИЛОЛОГИЧЕСКАЯ ЭКСПЕРТИЗА",
body: [
"Отдел нарративной безопасности сообщает: среди трёх документов дела один изготовлен не Бюро.",
"",
"Подделка может иметь подпись и даже печать. Но она не выдерживает проверки языком.",
],
memoTitle: "ПАМЯТКА КОНСУЛЬТАНТА Л. П. КАВЕРЗЫ",
memoText: "«Предлог “согласно” управляет дательным падежом: согласно чему? — согласно приказу, протоколу, распоряжению. Форма “согласно приказа” — родительный падеж — в служебном документе невозможна. Запятая — это дверь; падеж — это замок».",
docs: [
{
id: 1,
head: "Распоряжение № 4/тор",
text: "«Согласно приказу № " + CONFIG.caseNumber + ", объект подлежит проверке на торжественную пригодность».",
sign: "К. К. Пропавший-Без-Вести",
},
{
id: 2,
head: "Служебная записка № 7",
text: "«В соответствии с протоколом № 7 свидетель обязан сохранять загадочность и не разглашать сюрприз в обеденный перерыв».",
sign: "З. М. Штемпель",
},
{
id: 3,
head: "Постановление б/н",
text: "«Согласно приказа о похищении, подарок считается украденным, а филолог — подозрительным».",
sign: "подпись неразборчива",
},
],
correct: 3,
successTitle: "Подделка обнаружена",
successBody: "«Согласно приказа» — родительный падеж. Настоящее Бюро написало бы «согласно приказу». Отдел грамматики аплодирует, но по регламенту делает это беззвучно.",
wrongReactions: {
1: "Документ № 1 оскорблён. Он всю жизнь использовал дательный падеж и не намерен это обсуждать: «согласно приказу» — безупречно.",
2: "Документ № 2 загадочен, но законен. «В соответствии с протоколом» — корректная конструкция. Свидетель сохраняет загадочность; дело пока нет.",
},
hints: [
"Прочтите памятку Каверзы и проверьте по ней каждый документ.",
"Ищите документ, где после «согласно» стоит не дательный падеж.",
"Документ № 3: «согласно приказа» вместо «согласно приказу». Это подделка.",
],
evidence: {
name: "ЭКСПЕРТИЗА",
fact: "согласно приказу",
factLabel: "Признак подлинности документа",
desc: "Настоящие документы Бюро используют дательный падеж. «Согласно приказа» — маркер подделки.",
},
button: "Приобщить к делу",
},
/* ---------- ГЛАВА 4. АУДИО ---------- */
audio: {
chapter: "Глава 4",
title: "АУДИОЭКСПЕРТИЗА",
body: [
"Из архива поступила служебная песня. Бюро напоминает: музыка считается документом, если в ней есть хотя бы одна печать.",
"",
"В песне спрятано служебное слово. В самом тексте оно не звучит ни разу — иначе какая же это тайна.",
"",
"Припев намеренно подсовывает похожие слова. Не доверяйте припеву.",
],
songTitle: "«Временное ограничение»",
songAuthor: "ВИА «Канцелярия» при участии Отдела музыкальной отчётности",
bridgeLabel: "служебная вставка",
lyrics: [
{ type: "verse", label: "Куплет 1", lines: [
"В архиве пахнет пылью и чаем,",
"Объект на месте. Объект молчит.",
"Мы ничего не подозреваем —",
"Мы просто ждём, пока форма звучит.",
]},
{ type: "chorus", label: "Припев", lines: [
"Не украден, не забыт,",
"Он в законном состоянье.",
"Не потерян, не сокрыт —",
"Просто длится ожиданье.",
]},
{ type: "verse", label: "Куплет 2", lines: [
"Свидетель смотрит в журнал слишком долго,",
"Курьер уверяет, что вышел пустым.",
"Филолог поправит падеж — и только",
"Тогда документ перестанет быть злым.",
]},
{ type: "chorus", label: "Припев", lines: [
"Не украден, не забыт,",
"Он в законном состоянье.",
"Не потерян, не сокрыт —",
"Просто длится ожиданье.",
]},
{ type: "bridge", label: "Бридж", lines: [
"Пусть протокол никуда не спешит,",
"Архив тишину до конца сторожит.",
"Условие радости: дать ей отстояться,",
"Задержка объекта — не повод бояться,",
"А выдача будет. По форме. Не сразу.",
]},
{ type: "chorus", label: "Финальный припев", lines: [
"Не украден, не забыт,",
"Он почти уже открыт.",
"Не потерян, не сокрыт —",
"Форма выдачи звучит.",
]},
],
placeholder: "Служебное слово",
button: "Подтвердить",
correct: FACTS.codeWord,
successTitle: "Служебное слово принято",
successBody: "Первые буквы бриджа складываются в «" + FACTS.codeWord + "». Именно этим словом Бюро обозначает состояние объекта: не украден, не потерян — поставлен на паузу до появления сюжета.",
wrongHints: {
ozhidanie: "Слово из припева. Припев вас именно на это и ловил. Служебное слово в тексте не звучит — его нужно собрать.",
syuzhet: "Красивая версия, но этого слова в бридже не собрать. Смотрите на первые буквы пяти строк служебной вставки.",
},
wrong: [
"Слово не принято. Музыкальная комиссия сделала вид, что это была репетиция.",
"Неверно. Хор Бюро выражает сочувствие в допустимом диапазоне громкости.",
"Не совпадает. Возможно, вы слушали слишком доверчиво.",
],
hints: [
"Служебная вставка — это бридж, пять строк, выделенных отдельно. Смотрите на начала строк.",
"Возьмите по одной первой букве каждой из пяти строк бриджа и прочтите сверху вниз.",
"П, А, У, З, А — получается «" + FACTS.codeWord + "».",
],
evidence: {
name: "ЗАПИСЬ",
fact: FACTS.codeWord,
factLabel: "Служебное кодовое слово",
desc: "Акростих бриджа. Обозначает состояние объекта: задержан до появления сюжета.",
},
button: "Приобщить к делу",
},
/* ---------- ГЛАВА 5. ЛОЖНЫЙ СЛЕД ---------- */
falseTrail: {
chapter: "Глава 5",
title: "СРОЧНОЕ ОБНОВЛЕНИЕ",
alertBody: [
"В дело поступил рапорт о похищении объекта.",
"Обвиняется филолог-консультант Л. П. Каверза.",
],
reportTitle: "РАПОРТ О ПОХИЩЕНИИ",
intro: "Рапорт выглядит официально: есть номер дела, время, подпись и печать. Бюро предлагает проверить его до того, как кого-нибудь задержат.",
/* Текст рапорта разбит на фрагменты. is: тип реакции.
tell: true — настоящий признак подделки (нужно найти оба). */
reportParts: [
{ t: "Дело № " + CONFIG.caseNumber + ". ", id: "num" },
{ t: "Установлено, что филолог-консультант Л. П. Каверза изъял объект ", id: "plain" },
{ t: "в 09:05", id: "time", tell: true },
{ t: " и вынес его через турникет ", id: "plain2" },
{ t: "согласно приказа", id: "case", tell: true },
{ t: " № " + CONFIG.caseNumber + ". Мотив: день рождения казался недостаточно сюжетным. ", id: "motive" },
{ t: "Подпись: Л. П. Каверза.", id: "sign" },
{ t: " Печать: имеется. Уверенность: повышенная.", id: "stamp" },
],
task: "Найдите в тексте признаки подделки. Их два. Нажимайте прямо на подозрительные места.",
counter: "Признаков найдено",
tellReactions: {
time: {
title: "Признак 1: невозможное время",
text: "По журналу турникета Каверза вошёл в здание в 09:15. В 09:05 его здесь не было — изъять объект он не мог физически.",
},
case: {
title: "Признак 2: чужая грамматика",
text: "«Согласно приказа» — родительный падеж. Настоящее Бюро пишет «согласно приказу». Тот же почерк, что и у поддельного постановления.",
},
},
plainReactions: {
num: "Номер дела указан верно. Подделка всегда начинается с правдоподобного.",
plain: "Формулировка как формулировка. Обвинение — ещё не доказательство.",
plain2: "Турникет действительно существует. Это, пожалуй, единственное, что здесь бесспорно.",
motive: "Мотив звучит обидно правдоподобно. Но мотив не является уликой.",
sign: "Подпись есть. Каверза утверждает, что подписывается всегда со сноской. Сноски нет — но это спорный аргумент.",
stamp: "«Уверенность: повышенная» — не реквизит документа. Смешно, но подделку этим не доказать.",
},
successTitle: "Рапорт признан подделкой",
successBody: "Оба признака подтверждены: время противоречит журналу, падеж противоречит регламенту. Филолог освобождён от подозрений с формулировкой «слишком грамотен для этого преступления».",
kaverzaQuote: "«Я подозревал, что меня подставят. Но не ожидал, что сделают это с такой слабой грамматикой. Это обиднее подозрений».",
hints: [
"Один признак — фактический: сверьте время в рапорте с журналом турникета. Второй — языковой: вспомните памятку Каверзы.",
"Каверза вошёл в 09:15, а рапорт называет другое время. И где-то в тексте снова хромает падеж.",
"Нажмите на «в 09:05» и на «согласно приказа».",
],
evidence: {
name: "ОПРОВЕРЖЕНИЕ",
fact: "версия о краже несостоятельна",
factLabel: "Итог проверки",
desc: "Рапорт изготовлен Отделом срочных ложных версий. Кражи не было.",
},
button: "Приобщить к делу",
},
/* ---------- ГЛАВА 6. ОРДЕР ---------- */
order: {
chapter: "Глава 6",
title: "ФОРМА ОРДЕРА НА ВЫДАЧУ",
body: [
"Кражи не было. Объект не похищен — он задержан.",
"",
"Чтобы снять ограничение, заполните ордер. Все три графы уже установлены следствием: загляните в журнал дела, если что-то забылось.",
],
fields: [
{
id: "exitTime",
label: "Графа 1. Время выхода объекта из здания",
placeholder: "чч:мм",
hintSource: "глава 2, журнал турникета",
wrong: "Время не соответствует журналу турникета. Проверьте, когда именно объект покинул здание.",
},
{
id: "foreignTag",
label: "Графа 2. Инвентарный признак сторонней службы",
placeholder: "XX-00",
hintSource: "глава 1, опись имущества",
wrong: "Такой бирки в деле нет. Речь о предмете, которого не было в описи Бюро.",
},
{
id: "codeWord",
label: "Графа 3. Служебное кодовое слово",
placeholder: "слово",
hintSource: "глава 4, аудиоэкспертиза",
wrong: "Слово не совпадает со служебным. Оно было собрано из первых букв бриджа.",
},
],
button: "Подать ордер",
partial: "Форма принята частично. Бюро не любит частично.",
hints: [
"Все три значения уже есть в журнале дела — откройте его кнопкой «Журнал» наверху.",
"Графа 1 — время из журнала турникета. Графа 2 — бирка коробки. Графа 3 — акростих бриджа.",
FACTS.exitTime + " · " + FACTS.foreignTag + " · " + FACTS.codeWord,
],
},
/* ---------- ФИНАЛ ---------- */
twist: {
lines: [
"Преступления не было.",
"Дело № " + CONFIG.caseNumber + " переквалифицировано.",
"Объект не похищали. Объект был поставлен на паузу — до появления сюжета.",
"Сюжет создан. Вами. Только что.",
],
stamp: "СЮЖЕТ ПРИЛОЖЕН",
button: "Закрыть дело",
},
congrats: {
title: "Дело № " + CONFIG.caseNumber + " закрыто",
body: [
"Преступления не было.",
"",
"Были бумаги, штампы, журнал турникета, один подделанный рапорт и один филолог, который переживал за падежи больше, чем за собственную репутацию.",
"",
"А ещё был день рождения, который заслуживал не просто подарок, а историю.",
"",
CONFIG.name + ", ты прошла это дело с внимательностью, иронией и терпением, которым позавидовала бы любая бюрократическая система.",
"",
"С днём рождения.",
"",
"Пусть запятые стоят там, где им удобно.",
"Пусть правила нарушаются с умом.",
"Пусть подарки ждут тебя не в камерах хранения, а в руках.",
"",
"И если жизнь когда-нибудь снова покажется слишком серьёзной — вспомни: где-то в этом деле тебя официально признали человеком, способным расследовать собственную радость.",
],
button: "Получить ордер на выдачу",
},
orderScreen: {
title: "ОРДЕР НА ВЫДАЧУ ОБЪЕКТА № " + CONFIG.caseNumber,
rows: [
["Объект", "Подарок, 1 шт."],
["Статус", "готов к выдаче"],
["Основание", "сюжет приложен"],
],
qrNote: "Наведите камеру на контрольную печать.",
/* Штрихкод намеренно не выдаётся системой: последний шаг —
написать организатору. */
claimTitle: "ПОЛУЧЕНИЕ ШТРИХКОДА",
claimLead: "Бюро не хранит штрихкоды в открытом виде: они выдаются ответственному лицу под расписку.",
claimSteps: [
"Отправьте мне в личные сообщения кодовое слово: " + CONFIG.pickupCode + ".",
"В ответ придёт штрихкод заказа.",
"Покажите штрихкод на пункте выдачи по адресу выше.",
],
claimNote: "Да, это ещё одна бюрократическая процедура. Последняя. Обещаю.",
joke: "Если сотрудник ПВЗ спросит, зачем вам подарок, отвечайте: «По делу № " + CONFIG.caseNumber + "». Это не поможет. Но звучит весомо.",
reserveNote: "Если что-то не открывается — это не подарок, это техническая подделка. Напишите мне.",
copyButton: "Скопировать адрес и кодовое слово",
},
easter: {
speedButton: "Ускорить следствие",
speedFirst: "Кнопка работает только в следующем квесте.",
speedSecond: "Следствие ускорено эмоционально. Фактических изменений нет.",
complaintButton: "Пожаловаться на Бюро",
complaintResult: "Ваша жалоба принята. Ей присвоен номер № 0. Номер № 0 благодарит вас за внимание.",
confusedButton: "Я ничего не понимаю",
confusedFirst: "Это нормальная реакция. Следствие признаёт её официальной.",
confusedSecond: "Уровень понимания не изменился. Уровень атмосферы вырос.",
resetButton: "Начать сначала",
resetArm: "Нажмите ещё раз, чтобы начать дело заново. Прогресс будет стёрт.",
footer: "если вы читаете это, вы внимательны. подарок всё ещё реален.",
},
};
/* Порядок глав для индикатора прогресса */
const CHAPTERS = [
{ stage: "caseNumber", label: "Допуск" },
{ stage: "scene", label: "Опись" },
{ stage: "witnesses", label: "Показания" },
{ stage: "philology", label: "Экспертиза" },
{ stage: "audio", label: "Запись" },
{ stage: "falseTrail", label: "Ложный след" },
{ stage: "orderForm", label: "Ордер" },
];

20
index.html Normal file
View File

@@ -0,0 +1,20 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ГИС «ПАПКА» · Исчезновение подарка</title>
<meta name="description" content="Интерактивный квест-расследование ко дню рождения" />
<meta name="color-scheme" content="dark" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=PT+Serif:ital,wght@0,400;0,700;1,400&family=JetBrains+Mono:wght@400;600;700;800&family=Inter:wght@400;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div id="app"></div>
<script src="vendor/qrcode.js"></script>
<script src="data.js"></script>
<script src="app.js"></script>
</body>
</html>

767
styles.css Normal file
View File

@@ -0,0 +1,767 @@
/* ============================================================
ДЕЛО № 260826 — визуальный слой v2
Бюрократический нуар: тёмный терминал + бумажные документы.
============================================================ */
:root {
--bg: #0d1117;
--bg-2: #131a22;
--line: #232c37;
--line-2: #2e3946;
--paper: #efe4cd;
--paper-2: #f6eeda;
--paper-3: #e5d8bd;
--ink: #23201a;
--ink-2: #5c5344;
--ink-3: #8a7f6a;
--red: #b03a3a;
--red-soft: #d8a3a3;
--amber: #d7a44a;
--green: #4a7d55;
--dim: #7d8896;
--mono: "JetBrains Mono", "IBM Plex Mono", ui-monospace, "Courier New", monospace;
--serif: "PT Serif", "Source Serif 4", Georgia, serif;
--sans: "Inter", "Manrope", "Segoe UI", system-ui, sans-serif;
--r: 4px;
--shadow: 0 18px 50px rgba(0,0,0,.45);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background: var(--bg);
color: #ccd4dd;
font-family: var(--sans);
font-size: 15px;
line-height: 1.6;
min-height: 100vh;
background-image:
radial-gradient(1200px 600px at 20% -5%, rgba(215,164,74,.055), transparent 60%),
radial-gradient(900px 500px at 85% 105%, rgba(176,58,58,.05), transparent 60%);
transition: background-color .9s ease;
}
body.warm {
background-color: #241b13;
background-image:
radial-gradient(1000px 520px at 50% -10%, rgba(215,164,74,.16), transparent 62%),
radial-gradient(760px 460px at 15% 110%, rgba(215,164,74,.09), transparent 60%);
}
.shell { min-height: 100vh; display: flex; flex-direction: column; }
/* ---------------- верхняя панель ---------------- */
.topbar {
position: sticky; top: 0; z-index: 20;
display: flex; justify-content: space-between; align-items: center;
gap: 12px; flex-wrap: wrap;
padding: 11px 20px;
background: rgba(13,17,23,.92);
backdrop-filter: blur(8px);
border-bottom: 1px solid var(--line);
font-family: var(--mono); font-size: 12.5px;
}
.tb-left { display: flex; align-items: baseline; gap: 14px; min-width: 0; }
.tb-logo { color: var(--amber); letter-spacing: .06em; white-space: nowrap; }
.tb-case { color: var(--dim); white-space: nowrap; }
.tb-right { display: flex; gap: 7px; }
.chip {
display: inline-flex; align-items: center; gap: 6px;
background: #19212b; color: #c4ced9;
border: 1px solid var(--line-2); border-radius: var(--r);
padding: 6px 11px;
font: inherit; cursor: pointer;
transition: border-color .15s, color .15s, transform .08s;
}
.chip:hover:not(:disabled) { border-color: var(--amber); color: #e6ecf2; }
.chip:active:not(:disabled) { transform: translateY(1px); }
.chip:disabled { opacity: .32; cursor: default; }
.chip-n {
background: var(--amber); color: #1a1409;
border-radius: 20px; padding: 0 6px;
font-size: 11px; font-weight: 700; line-height: 17px;
}
.chip-i { padding: 6px 9px; }
/* ---------------- прогресс ---------------- */
.rail {
display: flex; gap: 2px; overflow-x: auto;
padding: 10px 20px 0;
scrollbar-width: none;
}
.rail::-webkit-scrollbar { display: none; }
.rail-step {
display: flex; align-items: center; gap: 7px;
padding: 5px 13px 5px 7px;
border-bottom: 2px solid var(--line);
color: #55606d;
font-family: var(--mono); font-size: 11px;
white-space: nowrap; flex: none;
transition: color .2s, border-color .2s;
}
.rail-dot {
width: 18px; height: 18px; border-radius: 50%;
display: grid; place-items: center;
background: #19212b; border: 1px solid var(--line-2);
font-size: 10px; line-height: 1;
}
.rail-step.done { color: #7f8b98; border-bottom-color: var(--green); }
.rail-step.done .rail-dot { background: var(--green); border-color: var(--green); color: #eaf3ec; }
.rail-step.cur { color: var(--amber); border-bottom-color: var(--amber); }
.rail-step.cur .rail-dot { border-color: var(--amber); color: var(--amber); }
/* ---------------- лист документа ---------------- */
.stage {
flex: 1;
display: flex; justify-content: center; align-items: flex-start;
padding: 26px 16px 40px;
animation: rise .38s cubic-bezier(.2,.7,.3,1);
}
@keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.center-wrap { width: 100%; display: flex; justify-content: center; }
.sheet {
position: relative;
width: 100%; max-width: 680px;
background: var(--paper);
background-image:
repeating-linear-gradient(0deg, rgba(0,0,0,.012) 0 1px, transparent 1px 3px),
radial-gradient(600px 400px at 15% 0%, rgba(255,255,255,.5), transparent 65%);
color: var(--ink);
font-family: var(--serif);
padding: 30px 34px 32px;
border-radius: var(--r);
box-shadow: var(--shadow);
}
.sheet::before {
content: ""; position: absolute; inset: 0;
border-radius: var(--r);
box-shadow: inset 0 0 0 1px rgba(120,100,60,.22);
pointer-events: none;
}
.sheet p { margin: 0 0 11px; }
.vspace { height: 9px; }
.sheet-head { margin-bottom: 18px; }
.chapter {
font-family: var(--mono); font-size: 11px;
letter-spacing: .14em; text-transform: uppercase;
color: var(--ink-3);
}
.chapter.red { color: var(--red); }
.sheet h1, .sheet h2 {
font-family: var(--sans); color: #1b1812;
letter-spacing: -.01em; margin: 4px 0 0;
}
.sheet h1 { font-size: 25px; line-height: 1.25; }
.sheet h2 { font-size: 19px; }
.intro h1 { margin-bottom: 14px; }
.note { font-family: var(--mono); font-size: 11.5px; color: var(--ink-3); margin-top: 14px; }
.dim { color: var(--ink-3); }
.quote {
font-style: italic; color: var(--ink-2);
border-left: 2px solid var(--amber);
padding-left: 13px; margin: 15px 0 0;
}
.center-t { text-align: center; }
/* ---------------- кнопки ---------------- */
.row { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 20px; }
.row.center { justify-content: center; }
.btn {
font-family: var(--sans); font-weight: 600; font-size: 14px;
padding: 11px 21px; border: 0; border-radius: var(--r);
cursor: pointer; transition: filter .15s, transform .07s, box-shadow .15s;
}
.btn:active { transform: translateY(1px); }
.btn.primary {
background: var(--red); color: #fbf1e6;
box-shadow: 0 2px 0 rgba(0,0,0,.18);
}
.btn.primary:hover { filter: brightness(1.09); }
.btn.ghost {
background: transparent; color: var(--ink);
box-shadow: inset 0 0 0 1px rgba(120,100,60,.5);
}
.btn.ghost:hover { box-shadow: inset 0 0 0 1px var(--ink-2); }
.linkbtn {
display: block; margin-top: 14px; padding: 0;
background: none; border: 0;
color: var(--ink-3); font-family: var(--mono); font-size: 11.5px;
text-decoration: underline; cursor: pointer;
}
.linkbtn:hover { color: var(--red); }
/* ---------------- поля ввода ---------------- */
.inrow { display: flex; gap: 9px; flex-wrap: wrap; margin: 18px 0 4px; }
.inrow input {
flex: 1 1 200px; min-width: 0;
padding: 11px 13px;
border: 1px solid rgba(120,100,60,.45); border-radius: var(--r);
background: var(--paper-2); color: var(--ink);
font-family: var(--mono); font-size: 14px;
}
.inrow input:focus { outline: 2px solid var(--amber); outline-offset: -1px; }
.inrow input:disabled { opacity: .65; }
/* ---------------- обратная связь ---------------- */
.feedback {
display: flex; gap: 8px; align-items: flex-start;
font-family: var(--mono); font-size: 12.5px; line-height: 1.5;
min-height: 18px; margin-top: 8px;
}
.feedback:empty { margin: 0; }
.fb-mark { flex: none; width: 3px; align-self: stretch; border-radius: 2px; }
.feedback.bad { color: var(--red); }
.feedback.bad .fb-mark { background: var(--red); }
.feedback.good { color: var(--green); }
.feedback.good .fb-mark { background: var(--green); }
.shake { animation: shake .32s; }
@keyframes shake {
0%,100% { transform: translateX(0); }
25% { transform: translateX(-4px); }
75% { transform: translateX(4px); }
}
/* ---------------- служебные вставки ---------------- */
.excerpt, .memo {
background: var(--paper-3);
border-left: 3px solid var(--amber);
padding: 13px 16px; margin: 16px 0;
border-radius: 0 var(--r) var(--r) 0;
}
.excerpt-h, .memo-h {
font-family: var(--mono); font-size: 10.5px;
letter-spacing: .12em; text-transform: uppercase;
color: var(--ink-3); margin-bottom: 6px;
}
.excerpt p, .memo p { margin: 0; font-size: 14px; }
.memo p { font-style: italic; }
.record {
background: var(--paper-2);
border: 1px solid rgba(120,100,60,.3);
border-radius: var(--r);
padding: 13px 16px; margin: 16px 0;
}
.record-h {
font-family: var(--mono); font-size: 10.5px;
letter-spacing: .12em; text-transform: uppercase;
color: var(--ink-3); margin-bottom: 9px;
padding-bottom: 7px; border-bottom: 1px solid rgba(120,100,60,.22);
}
.rec-row, .inv-row {
display: flex; justify-content: space-between; gap: 14px;
padding: 5px 0; font-size: 14px;
border-bottom: 1px dotted rgba(120,100,60,.24);
}
.rec-row:last-child, .inv-row:last-child { border-bottom: 0; }
.rec-row span { color: var(--ink-2); }
.rec-row b { text-align: right; font-family: var(--mono); font-size: 13px; }
.inv-row { font-family: var(--mono); font-size: 12.5px; justify-content: flex-start; }
.inv-n { color: var(--red); flex: none; width: 82px; }
.record-note {
font-family: var(--mono); font-size: 11px;
color: var(--ink-3); font-style: italic; margin-top: 8px;
}
/* журнал турникета */
.log-grid { display: flex; flex-direction: column; }
.log-row {
display: grid; grid-template-columns: 62px 1fr 74px;
gap: 10px; align-items: center;
padding: 6px 0; font-family: var(--mono); font-size: 13px;
border-bottom: 1px dotted rgba(120,100,60,.24);
}
.log-row:last-child { border-bottom: 0; }
.log-t { color: var(--ink); font-weight: 700; }
.log-n { color: var(--ink-2); }
.log-d {
justify-self: end; font-size: 11px;
padding: 2px 8px; border-radius: 20px; letter-spacing: .04em;
}
.log-d.in { background: rgba(74,125,85,.16); color: var(--green); }
.log-d.out { background: rgba(176,58,58,.13); color: var(--red); }
/* ---------------- две колонки (опись + стол) ---------------- */
/* Глава с описью шире прочих: бирки на столе должны читаться без усилий. */
.sheet.wide { max-width: 940px; }
.two-col {
display: grid; grid-template-columns: 264px minmax(0, 1fr);
gap: 18px; align-items: start; margin: 16px 0;
}
.two-col .record { margin: 0; }
.scene-box {
border: 1px solid rgba(120,100,60,.35);
border-radius: var(--r);
overflow: hidden;
overflow-x: auto;
background: #43331f;
-webkit-overflow-scrolling: touch;
}
.desk { width: 100%; display: block; }
.desk .hit { cursor: pointer; }
.desk .hit > *:not(.tag) { transition: filter .15s; }
.desk .hit:hover > *:not(.tag) { filter: brightness(1.14); }
.desk .tag rect { fill: #f2e7cf; stroke: #8a7550; stroke-width: .8; }
.desk .tag text {
fill: #3a3125; font-family: var(--mono); font-size: 9.5px; font-weight: 700;
}
.desk .hit:hover .tag rect { fill: #fff8e6; stroke: var(--amber); }
.desk .lbl { fill: #3a3125; font-family: var(--mono); font-size: 12px; }
.desk .lbl-s { fill: #3a3125; font-family: var(--mono); font-size: 10.5px; }
.desk .lbl-xs { fill: #6b5c44; font-family: var(--mono); font-size: 8.5px; }
.desk .num { fill: #23201a; font-family: var(--sans); font-size: 27px; font-weight: 700; }
.desk .found .tag rect { fill: var(--amber); stroke: #8a6a20; }
.pulse { animation: pulse 1.05s ease-in-out infinite; }
@keyframes pulse {
0%,100% { filter: none; }
50% { filter: drop-shadow(0 0 9px var(--amber)) brightness(1.15); }
}
/* ---------------- карточки-варианты ---------------- */
.cards, .docs { display: grid; gap: 10px; margin: 16px 0; }
.pcard, .doc {
display: block; width: 100%; text-align: left;
background: var(--paper-2);
border: 1px solid rgba(120,100,60,.32);
border-left: 3px solid rgba(120,100,60,.32);
border-radius: var(--r);
padding: 13px 15px;
font-family: var(--serif); font-size: 14px; color: var(--ink);
cursor: pointer;
transition: border-color .16s, background .16s, transform .1s, box-shadow .16s;
}
.pcard:hover:not(:disabled), .doc:hover:not(:disabled) {
border-left-color: var(--red);
background: #f8f1de;
transform: translateX(2px);
box-shadow: 0 3px 12px rgba(0,0,0,.07);
}
.pcard:disabled, .doc:disabled { cursor: default; opacity: .62; }
.pcard.found, .doc.found {
opacity: 1;
border-left-color: var(--green);
background: #f1f5ea;
}
.pcard-top { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; margin-bottom: 6px; }
.pcard-name { font-family: var(--sans); font-weight: 700; font-size: 14.5px; }
.pcard-role { font-family: var(--mono); font-size: 10.5px; color: var(--ink-3); }
.pcard-text { color: var(--ink-2); }
.doc-h { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 7px; }
.doc-n { font-family: var(--mono); font-size: 10.5px; color: var(--red); letter-spacing: .08em; }
.doc-title { font-family: var(--sans); font-weight: 600; font-size: 13.5px; }
.doc-t { margin-bottom: 8px; }
.doc-sign {
font-family: var(--mono); font-size: 10.5px; color: var(--ink-3);
text-align: right; font-style: italic;
}
/* ---------------- успех ---------------- */
.success {
background: linear-gradient(180deg, rgba(74,125,85,.11), rgba(74,125,85,.05));
border: 1px solid rgba(74,125,85,.34);
border-radius: var(--r);
padding: 15px 17px; margin-top: 18px;
animation: rise .35s;
}
.success-head {
display: flex; align-items: center; gap: 9px;
font-family: var(--sans); font-weight: 700; font-size: 15px;
color: #2f5b39; margin-bottom: 7px;
}
.tick {
flex: none; width: 20px; height: 20px; border-radius: 50%;
background: var(--green); color: #fff;
display: grid; place-items: center; font-size: 12px;
}
.success p { margin: 0; color: var(--ink-2); font-size: 14px; }
/* ---------------- карточка факта ---------------- */
.jcard {
background: var(--paper-2);
border: 1px dashed rgba(120,100,60,.45);
border-radius: var(--r);
padding: 11px 14px; margin-bottom: 10px;
}
.jcard.inline { margin: 13px 0 0; background: rgba(255,255,255,.5); }
.jcard-name {
font-family: var(--sans); font-weight: 700; font-size: 13px;
letter-spacing: .06em; color: var(--red); margin-bottom: 6px;
}
.jcard-fact {
display: flex; justify-content: space-between; align-items: baseline;
gap: 12px; flex-wrap: wrap;
padding: 7px 10px; border-radius: 3px;
background: rgba(215,164,74,.16);
}
.jcard-label { font-family: var(--sans); font-size: 11.5px; color: var(--ink-2); }
.jcard-fact b { font-family: var(--mono); font-size: 15px; color: var(--ink); letter-spacing: .04em; }
.jcard-desc { font-size: 12.5px; color: var(--ink-3); margin-top: 7px; }
/* ---------------- песня ---------------- */
.song {
background: var(--paper-2);
border: 1px solid rgba(120,100,60,.3);
border-radius: var(--r);
padding: 16px 18px; margin: 16px 0;
}
.song-t { font-family: var(--sans); font-weight: 700; font-size: 16px; }
.song-a { font-family: var(--mono); font-size: 10.5px; color: var(--ink-3); margin-top: 2px; }
.song audio { width: 100%; margin: 12px 0 6px; }
.song-fb { font-family: var(--mono); font-size: 11.5px; color: var(--ink-3); }
.lyrics { display: grid; gap: 14px; margin-top: 12px; }
.lblock p { margin: 1px 0; font-size: 13.5px; color: var(--ink-2); }
.lblock-h {
font-family: var(--mono); font-size: 10px;
letter-spacing: .12em; text-transform: uppercase;
color: var(--ink-3); margin-bottom: 4px;
}
.badge {
background: var(--red); color: #fdf3e8;
border-radius: 20px; padding: 1px 8px;
font-size: 9px; letter-spacing: .06em; margin-left: 4px;
}
.lblock.chorus p { font-style: italic; color: var(--ink-3); }
.lblock.bridge {
background: rgba(215,164,74,.13);
border-left: 3px solid var(--amber);
padding: 10px 13px; margin-left: -4px;
border-radius: 0 var(--r) var(--r) 0;
}
.lblock.bridge p { color: var(--ink); font-weight: 600; }
.acro {
color: var(--red); font-weight: 800;
font-size: 1.14em;
border-bottom: 2px solid rgba(176,58,58,.32);
}
/* ---------------- ложный след ---------------- */
.alert {
background: linear-gradient(180deg, rgba(176,58,58,.12), rgba(176,58,58,.05));
border: 1px solid rgba(176,58,58,.42);
border-radius: var(--r);
padding: 13px 16px; margin-bottom: 15px;
}
.alert p { margin: 0 0 4px; font-size: 14px; }
.alert p:last-child { margin: 0; }
.report {
position: relative;
background: var(--paper-2);
border: 1px solid rgba(120,100,60,.34);
border-radius: var(--r);
padding: 17px 19px 22px; margin: 15px 0;
}
.report-h {
font-family: var(--sans); font-weight: 700; font-size: 15px;
letter-spacing: .04em; margin-bottom: 10px;
padding-bottom: 8px; border-bottom: 1px solid rgba(120,100,60,.22);
}
.report-body { font-size: 14.5px; line-height: 1.85; margin: 0; }
.report-seal {
position: absolute; right: 16px; bottom: 10px;
font-family: var(--mono); font-size: 9.5px;
color: var(--red-soft); border: 1px solid var(--red-soft);
border-radius: 3px; padding: 2px 7px;
transform: rotate(-6deg); opacity: .8;
}
/* Искомые фрагменты НЕ отличаются от обычного текста: найти их —
и есть загадка. Кликается весь рапорт, «подозрительность» игрок
определяет сам, сверяя с журналом и памяткой. */
.plain { cursor: pointer; border-radius: 2px; transition: background .14s; }
.plain:hover { background: rgba(120,100,60,.11); }
.tell {
font: inherit; color: inherit;
background: none; border: 0; padding: 0;
border-radius: 2px;
cursor: pointer; transition: background .14s, color .14s;
}
.tell:hover:not(:disabled) { background: rgba(120,100,60,.11); }
.tell.got {
background: rgba(74,125,85,.2); color: #2f5b39;
box-shadow: inset 0 -1.5px 0 var(--green);
font-weight: 700; padding: 1px 3px;
cursor: default;
}
.task {
display: flex; justify-content: space-between; align-items: center;
gap: 14px; flex-wrap: wrap;
background: var(--paper-3); border-radius: var(--r);
padding: 11px 15px; margin-top: 4px;
}
.task p { margin: 0; font-size: 13.5px; }
.counter { font-family: var(--mono); font-size: 12.5px; color: var(--ink-2); white-space: nowrap; }
.counter b { color: var(--red); font-size: 15px; }
/* ---------------- форма ордера ---------------- */
.form { display: grid; gap: 17px; margin: 20px 0 4px; }
.fgroup label {
display: block;
font-family: var(--sans); font-weight: 600; font-size: 13.5px;
color: var(--ink); margin-bottom: 6px;
}
.fline { display: flex; align-items: center; gap: 9px; }
.fline input {
flex: 1; min-width: 0;
padding: 10px 13px;
border: 1px solid rgba(120,100,60,.45); border-radius: var(--r);
background: var(--paper-2); color: var(--ink);
font-family: var(--mono); font-size: 14px;
}
.fline input:focus { outline: 2px solid var(--amber); outline-offset: -1px; }
.fmark {
flex: none; width: 22px; text-align: center;
color: var(--green); font-size: 17px; font-weight: 700;
}
.fsrc {
font-family: var(--mono); font-size: 10.5px;
color: var(--ink-3); margin-top: 5px;
}
.ferr {
font-family: var(--mono); font-size: 11.5px;
color: var(--red); margin-top: 5px;
}
.ferr:empty { margin: 0; }
.fgroup.ok label { color: var(--green); }
.fgroup.ok .fline input {
background: rgba(74,125,85,.09);
border-color: rgba(74,125,85,.5);
}
/* ---------------- финал ---------------- */
.twist {
width: 100%; max-width: 620px; margin: 70px auto 0; text-align: center;
}
.twist-lines { min-height: 150px; }
.twist-lines p {
font-family: var(--mono); font-size: 16.5px; line-height: 1.6;
color: #e8ded0; margin: 12px 0;
opacity: 0; animation: fade .85s forwards;
}
@keyframes fade { to { opacity: 1; } }
.bigstamp {
display: inline-block; margin-top: 26px;
border: 3px solid var(--red); color: var(--red);
font-family: var(--mono); font-weight: 700; font-size: 17px;
letter-spacing: .1em; padding: 11px 22px;
transform: rotate(-4deg);
animation: slam .38s cubic-bezier(.2,1.4,.4,1);
}
@keyframes slam {
from { transform: rotate(-4deg) scale(2.1); opacity: 0; }
to { transform: rotate(-4deg) scale(1); opacity: 1; }
}
.warm-sheet {
background: #fbf4e6;
background-image: radial-gradient(700px 400px at 20% 0%, rgba(255,255,255,.7), transparent 60%);
max-width: 620px;
}
.warm-sheet h1 { text-align: center; font-size: 22px; margin-bottom: 18px; }
.warm-sheet p { line-height: 1.72; }
.order-final .record { background: rgba(255,255,255,.55); }
.qr {
width: 232px; margin: 18px auto;
background: #fff; padding: 8px; border-radius: var(--r);
box-shadow: 0 3px 14px rgba(0,0,0,.12);
}
.qr svg { width: 100%; height: auto; display: block; }
.pickup .rec-row b { font-size: 14px; }
/* Блок «как получить штрихкод» — последний шаг вне системы */
.claim {
background: rgba(215,164,74,.16);
border: 1px solid rgba(215,164,74,.5);
border-radius: var(--r);
padding: 15px 18px; margin: 16px 0 4px;
text-align: left;
}
.claim-h {
font-family: var(--mono); font-size: 10.5px;
letter-spacing: .12em; text-transform: uppercase;
color: #8a6a20; margin-bottom: 7px;
}
.claim-lead { font-size: 13.5px; color: var(--ink-2); margin: 0 0 10px; }
.claim-steps {
margin: 0; padding-left: 20px;
display: grid; gap: 6px;
}
.claim-steps li { font-size: 14.5px; line-height: 1.5; }
.claim-steps li::marker { color: var(--red); font-weight: 700; }
.claim-note {
font-family: var(--mono); font-size: 11.5px;
color: var(--ink-3); font-style: italic;
margin: 14px 0 0;
padding-top: 11px;
border-top: 1px dotted rgba(120,100,60,.35);
}
/* ---------------- док и футер ---------------- */
.dock {
display: flex; gap: 7px; justify-content: center; flex-wrap: wrap;
padding: 6px 16px 0; opacity: .42; transition: opacity .2s;
}
.dock:hover { opacity: 1; }
.dk {
background: none; border: 1px solid #232c37; color: #5b6673;
border-radius: var(--r); padding: 5px 10px;
font-family: var(--mono); font-size: 10.5px; cursor: pointer;
transition: border-color .15s, color .15s;
}
.dk:hover { border-color: var(--amber); color: var(--amber); }
.footer {
text-align: center; padding: 14px 0 20px;
font-family: var(--mono); font-size: 10px; color: #222a34;
}
/* ---------------- панели ---------------- */
.backdrop {
position: fixed; inset: 0; z-index: 60;
background: rgba(4,7,10,.6);
display: flex; justify-content: flex-end;
animation: fadein .18s;
}
@keyframes fadein { from { opacity: 0; } to { opacity: 1; } }
.panel {
width: min(400px, 93vw); height: 100vh;
background: var(--bg-2); color: #ccd4dd;
overflow-y: auto;
box-shadow: -14px 0 44px rgba(0,0,0,.5);
animation: slidein .24s cubic-bezier(.2,.7,.3,1);
}
@keyframes slidein { from { transform: translateX(24px); opacity: .4; } to { transform: none; opacity: 1; } }
.panel-head {
position: sticky; top: 0; z-index: 2;
display: flex; justify-content: space-between; align-items: center;
padding: 15px 18px;
background: var(--bg-2); border-bottom: 1px solid var(--line);
}
.panel-head h3 {
margin: 0; font-family: var(--mono); font-size: 13px;
color: var(--amber); letter-spacing: .06em;
}
.panel-x {
background: none; border: 0; color: var(--dim);
font-size: 21px; line-height: 1; cursor: pointer; padding: 0 4px;
}
.panel-x:hover { color: #e6ecf2; }
.panel-body { padding: 16px 18px 28px; }
.panel-lead { font-size: 12.5px; margin-bottom: 14px; }
.panel-body .dim { color: var(--dim); }
.panel-body .jcard {
background: #19212b; border-color: var(--line-2);
}
.panel-body .jcard-name { color: var(--amber); }
.panel-body .jcard-fact { background: rgba(215,164,74,.1); }
.panel-body .jcard-label { color: var(--dim); }
.panel-body .jcard-fact b { color: #f0e3c8; }
.panel-body .jcard-desc { color: var(--dim); }
.hint {
font-family: var(--mono); font-size: 12.5px; line-height: 1.55;
color: #d3dbe3; margin: 10px 0;
padding-left: 11px; border-left: 2px solid var(--amber);
}
.hint b { color: var(--amber); }
/* ---------------- загрузка ---------------- */
.boot {
width: 100%; text-align: center; padding-top: 54px;
display: flex; flex-direction: column; align-items: center;
}
.boot-title {
font-family: var(--mono); color: var(--amber);
letter-spacing: .1em; font-size: 21px; margin: 0;
}
.boot-sub {
color: var(--dim); font-size: 12.5px; max-width: 440px;
margin: 8px auto 30px; font-family: var(--mono);
}
.boot-lines p {
font-family: var(--mono); font-size: 13.5px; color: #b9c3ce;
margin: 7px 0; opacity: 0; animation: fade .5s forwards;
}
/* ---------------- тост ---------------- */
#toast {
position: fixed; left: 50%; bottom: 26px; z-index: 90;
transform: translateX(-50%) translateY(10px);
background: #19212b; color: #ecd9b4;
border: 1px solid var(--amber); border-radius: var(--r);
padding: 10px 18px; max-width: 90vw;
font-family: var(--mono); font-size: 12px;
opacity: 0; pointer-events: none;
transition: opacity .24s, transform .24s;
}
#toast.on { opacity: 1; transform: translateX(-50%) translateY(0); }
/* ---------------- адаптив ---------------- */
@media (max-width: 720px) {
.two-col { grid-template-columns: 1fr; }
/* На узком экране стол не ужимаем: иначе инвентарные бирки —
а на них держится вся загадка — становятся нечитаемыми.
Вместо этого даём сцене горизонтальную прокрутку. */
.desk { min-width: 500px; }
.scene-box { box-shadow: inset -14px 0 12px -12px rgba(0,0,0,.5); }
}
@media (max-width: 520px) {
body { font-size: 14.5px; }
.sheet { padding: 22px 18px 24px; }
.sheet h1 { font-size: 21px; }
.sheet h2 { font-size: 17px; }
.topbar { padding: 10px 13px; }
.rail { padding: 9px 13px 0; }
.stage { padding: 18px 10px 32px; }
.task { flex-direction: column; align-items: flex-start; }
.twist { margin-top: 44px; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

2323
vendor/qrcode.js vendored Normal file

File diff suppressed because it is too large Load Diff

310
ПРОХОЖДЕНИЕ.md Normal file
View File

@@ -0,0 +1,310 @@
# Дело № 260826 — полное прохождение
> **Внимание: сплошные спойлеры.** Этот файл — для организатора квеста,
> а не для именинницы. Здесь все ответы и вся логика загадок.
> Не отправляйте его вместе со ссылкой на квест.
Версия: `quest-v2`. Ориентировочное время прохождения — 30–45 минут.
---
## Быстрая таблица ответов
| № | Глава | Что сделать | Ответ |
|---|---|---|---|
| 1 | Допуск | Ввести номер дела | `260826` |
| 2 | Опись | Кликнуть предмет вне описи | коробка с биркой `ДС-04` |
| 3 | Показания | Выбрать невозможное показание | Клавдия Ивановна Шлюз |
| 4 | Экспертиза | Найти подделку | Документ № 3 |
| 5 | Запись | Ввести служебное слово | `ПАУЗА` |
| 6 | Ложный след | Найти 2 признака подделки | `в 09:05` и `согласно приказа` |
| 7 | Ордер | Заполнить три графы | `09:08` · `ДС-04` · `ПАУЗА` |
---
## Подробное прохождение
### Экран 0. Уведомление
Заголовок «Объект задержан». Ничего решать не нужно.
**Действие:** кнопка «Принять дело к производству» → экран загрузки →
кнопка «Войти в систему».
---
### Глава 1 (шаг «Допуск»). Номер дела
**Что на экране:** выписка из регламента и карточка регистрации.
- Регламент, п. 4.2: «Номер дела образуется из даты регистрации в
формате ДДММГГ, без разделителей».
- Карточка регистрации, поле «Дата регистрации»: *двадцать шестое
августа две тысячи двадцать шестого года*.
**Логика:** дату прописью перевести в цифры и склеить по формату:
26 (день) + 08 (месяц) + 26 (год) → `260826`.
**Ответ:** `260826`
**Что принимается:** любые нецифровые символы игнорируются, так что
`26-08-26` и `26 08 26` тоже пройдут.
**Примечание:** до решения этой загадки номер дела нигде не показывается —
он скрыт и в шапке («дело № ——————»), и в заголовке вкладки.
**Получаем:** статус следователя. Дальше — кнопка «Осмотреть место
происшествия».
---
### Глава 2 (шаг «Опись»). Место происшествия
**Что на экране:** слева опись имущества отдела, справа фото стола.
Каждый предмет на столе снабжён инвентарной биркой.
Опись Бюро:
| Бирка | Предмет |
|---|---|
| БКТО-11 | лампа настольная, зелёная |
| БКТО-12 | кружка с надписью «НЕ ТРОГАТЬ» |
| БКТО-13 | календарь настенный |
| БКТО-14 | папка дела |
| БКТО-15 | карандаш |
| БКТО-16 | кактус (эмоционально устойчив) |
| БКТО-17 | лист с надписью «ПЕЧАТЬ НЕ ТРОГАТЬ» |
**Логика:** на столе восемь предметов с бирками. Семь совпадают с
описью. У восьмого — картонной коробки внизу — бирка `ДС-04`: другой
префикс. Все предметы отдела имеют префикс БКТО, значит коробка
принадлежит сторонней организации (служба доставки «Доставка с
чувством») и в опись не входит.
**Ответ:** клик по картонной коробке (бирка `ДС-04`).
**На телефоне:** сцена прокручивается по горизонтали — коробка находится
в нижней части стола, ближе к правому краю.
**Клики по остальным предметам** дают шутливые реакции и всегда называют
бирку — это подсказывает саму механику сверки.
**В журнал записывается:** `ДС-04` — инвентарный признак сторонней
службы. **Понадобится в графе 2 финального ордера.**
---
### Глава 3 (шаг «Показания»). Сверка показаний
**Что на экране:** журнал турникета и четыре показания.
Журнал турникета за 26 августа:
| Время | Кто | Направление |
|---|---|---|
| 08:41 | Пылинкин А. | вход |
| 09:02 | Шлюз К. И. | **выход** |
| 09:06 | Быстрый Т. | вход |
| 09:08 | Быстрый Т. | **выход** |
| 09:12 | Шлюз К. И. | вход |
| 09:15 | Каверза Л. П. | вход |
**Логика — проверяем каждое показание по журналу:**
- **Клавдия Шлюз:** «В 09:07 я была на посту». Но по журналу она вышла
в 09:02 и вернулась только в 09:12. В 09:07 её не было в здании →
**показание физически невозможно**. ✅ правильный ответ
- **Пылинкин:** «Около девяти мимо прошёл человек в шарфе». Вошёл в
08:41 и не выходил — внутри был, противоречия нет.
- **Тимур Быстрый:** «Пришёл, забрал коробку, ушёл. Две минуты».
09:06 → 09:08 = ровно две минуты. Журнал подтверждает слово в слово.
- **Каверза:** «Когда я вошёл, на столе уже ничего не было». Вошёл в
09:15, то есть после выноса коробки в 09:08. Мог застать пустой стол.
**Ответ:** Клавдия Ивановна Шлюз.
**В журнал записывается:** `09:08` — время выхода объекта из здания
(коробку вынес курьер). **Понадобится в графе 1 финального ордера.**
---
### Глава 4 (шаг «Экспертиза»). Филологическая экспертиза
**Что на экране:** памятка консультанта Каверзы и три документа.
Памятка: «Предлог *согласно* управляет дательным падежом: согласно
чему? — согласно приказу, протоколу, распоряжению. Форма *согласно
приказа* — родительный падеж — в служебном документе невозможна».
Документы:
1. «Согласно **приказу** № 260826…» — дательный падеж, верно.
2. «В соответствии **с протоколом** № 7…» — корректная конструкция.
3. «Согласно **приказа** о похищении…» — родительный падеж. ❌
**Ответ:** Документ № 3.
**Почему это двойной намёк:** документ не только содержит ошибку, но и
слишком удобно обвиняет филолога — то есть подброшен.
**В журнал записывается:** «согласно приказу» — признак подлинности
документа. Пригодится в главе 6 (не в форме, а как знание).
---
### Глава 5 (шаг «Запись»). Аудиоэкспертиза
**Что на экране:** плеер (если mp3 не добавлен — прячется, остаётся
текст) и текст песни ВИА «Канцелярия» — «Временное ограничение».
**Логика:** нужно служебное слово, которого **в тексте песни нет ни
разу**. Блок «Бридж» помечен badge «служебная вставка» — смотреть надо
туда. Берём первые буквы пяти его строк:
```
Пусть протокол никуда не спешит, → П
Архив тишину до конца сторожит. → А
Условие радости: дать ей отстояться, → У
Задержка объекта — не повод бояться, → З
А выдача будет. По форме. Не сразу. → А
```
**Ответ:** `ПАУЗА`
**Ловушка:** припев дважды повторяет «Просто длится **ожиданье**» —
похожее по смыслу слово, которое напрашивается. На ответ «ожидание»
предусмотрена отдельная реакция: «Слово из припева. Припев вас именно на
это и ловил». На ответ «сюжет» (правильный ответ из v1) — тоже своя
реакция.
**В журнал записывается:** `ПАУЗА` — служебное кодовое слово.
**Понадобится в графе 3 финального ордера.**
---
### Глава 6 (шаг «Ложный след»). Рапорт о похищении
**Что на экране:** срочное уведомление и рапорт, обвиняющий Каверзу.
Кликается **каждый** фрагмент текста, визуально они не различаются.
Текст рапорта: «Дело № 260826. Установлено, что филолог-консультант
Л. П. Каверза изъял объект **в 09:05** и вынес его через турникет
**согласно приказа** № 260826. Мотив: день рождения казался недостаточно
сюжетным. Подпись: Л. П. Каверза. Печать: имеется. Уверенность:
повышенная».
**Нужно найти два признака подделки:**
1. **`в 09:05`** — по журналу турникета (глава 3) Каверза вошёл в
здание только в 09:15. В 09:05 его здесь не было, изъять объект он
не мог физически.
2. **`согласно приказа`** — родительный падеж вместо дательного.
Тот же почерк, что и у поддельного постановления из главы 4.
Счётчик показывает «Признаков найдено: N / 2». Нужны оба.
**Клики по остальным фрагментам** дают юмористические ответы и
объясняют, почему это не улика (мотив, подпись, печать, номер дела).
**В журнал записывается:** версия о краже несостоятельна.
---
### Глава 7 (шаг «Ордер»). Форма ордера на выдачу
Финал. Три графы, каждая — из своей главы. Под каждой подписан источник.
| Графа | Значение | Откуда |
|---|---|---|
| 1. Время выхода объекта из здания | `09:08` | глава 3, журнал турникета |
| 2. Инвентарный признак сторонней службы | `ДС-04` | глава 2, опись |
| 3. Служебное кодовое слово | `ПАУЗА` | глава 5, аудиоэкспертиза |
Все три значения лежат в **журнале дела** (кнопка «Журнал» в шапке) —
если игрок что-то не записал, они всегда под рукой.
**Терпимость к формату ввода:**
- время: `09:08`, `9:08`, `09.08`, `0908`, `9 08` — всё принимается;
невалидное (`25:00`, `09:60`) отклоняется;
- бирка: `ДС-04`, `дс 04`, `дс04` — регистр, пробелы и дефисы не важны;
- слово: регистр не важен, `ё` приравнивается к `е`.
Графы проверяются по отдельности: верная сразу становится зелёной с
галочкой и блокируется, у неверной появляется своя подсказка. Введённый
текст не теряется.
**После заполнения всех трёх:** твист → поздравление → ордер с QR-кодом.
---
## Финал
1. **Твист.** Построчно проявляется: преступления не было, объект был
поставлен на паузу до появления сюжета, сюжет создан игроком.
Печать «СЮЖЕТ ПРИЛОЖЕН».
2. **Поздравление.** Тёплый текст с именем из `CONFIG.name`.
3. **Ордер.** QR-код, адрес выдачи, кодовое слово и номер дела, а также
блок «Получение штрихкода».
**Как именинница получает подарок (последний шаг вне квеста):**
1. Пишет вам в личные сообщения кодовое слово **АРБУЗ**.
2. Вы присылаете в ответ штрихкод заказа.
3. Она показывает штрихкод на ПВЗ Озон по адресу
**Москва, Кочновский пр-д, 4 к. 1**.
Сам штрихкод в квесте нигде не хранится и не показывается — это
сознательное решение, обыгранное в сюжете: «Бюро не хранит штрихкоды в
открытом виде, они выдаются ответственному лицу под расписку».
**Что нужно от вас:** дождаться сообщения с кодовым словом и отправить
штрихкод. Держите его под рукой в день рождения.
QR ведёт на `CONFIG.pvzPageUrl`, если он указан; иначе кодирует строку с
адресом, кодовым словом и номером дела.
---
## Если игрок застрял
Квест невозможно заблокировать — механики помощи работают на каждой
загадке:
- **Кнопка «Подсказка»** в шапке — три уровня, от намёка до прямого
ответа. Каждое нажатие открывает следующий.
- **Автоподсказки** — открываются сами после 3, 5 и 8 неверных попыток.
- **«Следствие настаивает на правильном ответе»** — появляется после
8 ошибок. Подсвечивает нужный элемент или подставляет ответ в поле.
- **Кнопка «Журнал»** — все добытые факты в одном месте.
---
## Пасхалки (на прохождение не влияют)
Внизу каждого экрана, приглушённой строкой:
- **«Ускорить следствие»** — «Кнопка работает только в следующем
квесте», при повторном нажатии — «Следствие ускорено эмоционально».
- **«Я ничего не понимаю»** — «Это нормальная реакция. Следствие
признаёт её официальной».
- **«Пожаловаться на Бюро»** — жалобе присваивается номер № 0.
- **«Начать сначала»** — сброс прогресса, требует двойного нажатия.
- В самом низу мелким шрифтом: «если вы читаете это, вы внимательны.
подарок всё ещё реален».
Прогресс сохраняется в браузере автоматически: если закрыть вкладку,
квест продолжится с того же места.
---
## Что нужно подготовить организатору
1. В `data.js` → `CONFIG`: имя именинницы; адрес и кодовое слово уже
проставлены (ПВЗ Озон на Кочновском, слово `АРБУЗ`).
2. Проверить, что `caseNumber` и `caseDateWords` согласованы между собой
(номер выводится игроком из даты прописью).
3. Опционально: `assets/song.mp3` и `pvzPageUrl`.
4. Выложить папку на любой статический хостинг и отправить ссылку.
5. **Держать штрихкод заказа наготове** — его нужно будет отправить в
ответ на кодовое слово `АРБУЗ`.