Skip to content

Repository files navigation

Gothic Lockpick Solver

Браузерный решатель и визуализатор головоломки с дисковыми замками — механика взлома из Gothic и подобных RPG.

Запуск: открыть index.html в браузере. Зависимостей нет, сборки нет.

demo.mp4

Скриншоты: Настройки · Решение · Поиск

Мобильная версия


Что это такое

Головоломка — N плашек на линейной шкале позиций. Каждая плашка может двигаться влево или вправо. Плашки связаны зависимостями: движение одной тянет связанные плашки в том же или обратном направлении. Зависимости не транзитивны — если A→B и B→C, то движение A затрагивает только B. C реагирует лишь когда B двигают напрямую. Цель — привести все плашки в центральную позицию одновременно.

Приложение:

  • визуализирует конфигурацию в 3D-изометрической сцене
  • решает головоломку через BFS (поиск в ширину) с гарантией минимального количества ходов — а среди всех минимальных решений выбирает то, где меньше всего переключений между плашками (короче список шагов)
  • пошагово воспроизводит решение
  • генерирует случайные конфигурации заданной сложности

Использование

Язык интерфейса

В правом верхнем углу — три кнопки-флага: 🇷🇺 Русский / 🇺🇦 Українська / 🇬🇧 English. Выбор сохраняется в localStorage.

Вкладка настроек

Элемент Действие
Элементы −/+ Количество плашек (2–8)
Позиции −/+ Нечётное количество позиций (3, 5, 7, …)
🟢 🟡 🔴 Случайная конфигурация: лёгкая / средняя / сложная
📤 Копировать конфигурацию в буфер (JSON)
📥 Импортировать конфигурацию из буфера
Ctrl+C Копировать текущую конфигурацию
Ctrl+V Вставить конфигурацию из буфера
🔍 / Ctrl+K (на Mac — ⌘K) Открыть поиск по базе реальных замков из игры
РЕШЕНИЕ Запустить BFS и перейти к решению

Барабан позиций (колонка + / цифра / − на каждую плашку, над матрицей) и клик по дырке в 3D-виде:

  • выставляют позицию напрямую, без проверки зависимостей — инструмент конфигурации: любое положение задаётся независимо от других плашек
  • цифру можно ввести с клавиатуры; + / −, стрелки ↑ / ↓ и колесо мыши над цифрой меняют позицию на ±1
  • вставка нескольких цифр заменяет весь набор, причём база определяется по граничным цифрам: 0 бывает только при отсчёте с нуля (0–6), 7 — только при отсчёте с единицы (1–7); если цифры лишь 1–6, вставка читается как 1-based (барабан задаёт то, что показывает). Одиночная цифра — всегда 1-based
  • в 3D-виде при наведении на дырку — подсветка; клик (или тап) выставляет позицию той плашки, по дырке которой кликнули
  • все правки позиции проходят через единый обработчик (posSetPlateValue), поэтому 3D, кэш решения и подсказки обновляются согласованно

Матрица зависимостей:

  • ЛКМ — переключить связь: нет → прямо → обратно
  • ПКМ — переключить в обратную сторону
  • Диагональ (самозависимость) заблокирована и обозначена полосками
  • Когда ячейкам становится тесно для слова, вместо него показывается иконка направления (стрелки в одну сторону = «прямо», врозь = «обратно»); цвет наследует цвет ячейки. Порог — по ширине самой матрицы (@container, 670px), а не по ширине экрана: ширина ячейки равна матрица / (N+1), поэтому восемь плашек переходят на иконки и на десктопе

Управление плашками клавиатурой:

  • W / S или ↑ / ↓ — выбор активной плашки
  • A / D или ← / → — сдвинуть активную плашку на ±1 (одну плашку, без зависимостей — как барабан; зависимости применяются только на этапе решения)

Вкладка решения

Решение отображается строкой нотации сверху и списком шагов-карточек. Каждый шаг — карточка: элемент, цветной значок направления (зелёный = вправо, красный = влево) с кратностью ×N, и код в нотации справа. Текущий шаг подсвечен; активная плашка одновременно подсвечивается в 3D-сцене.

Кнопка Действие
‹ Шаг Шаг назад по решению
▶ Авто Автовоспроизведение (500 мс/шаг)
Шаг › Шаг вперёд по решению
Вернуться Вернуться в настройки

Нажать на шаг в списке — перейти к нему напрямую.

Если все плашки уже стоят в центре, решать нечего: вместо пустого списка выводится пояснение «замок уже решён, возможно вы забыли выставить начальные позиции», а кнопки шагов недоступны.

Режим исследования

Нажатие A / D или ← / → в режиме решения автоматически входит в режим исследования:

  • Авто-воспроизведение останавливается.
  • Плашки можно двигать свободно, независимо от BFS-решения.
  • Последовательные ходы одной плашки в одном направлении схлопываются в одну запись (1A3 вместо 1A 1A 1A); обратные ходы схлопываются в обратную сторону.
  • Если ход заблокирован, мешающая плашка подсвечивается красным и дёргается на 250 мс. На мобильных устройствах (Android) блок отзывается коротким вибро, а поворот диска / ручной шаг решения — лёгким тиком (тихий no-op на iOS/десктопе, без запроса разрешений).
  • В списке шагов появляется разделитель ↩ вернуться к шагу N — клик по нему или по любому пройденному BFS-шагу возвращает к решению.

Ссылка на замок

Конфигурация живёт в адресной строке — ?lock=<dotted>, при необходимости плюс &solve. Достаточно скопировать URL, чтобы поделиться замком.

  • Любая правка конфигурации сразу обновляет ссылку (replaceState — история не засоряется).
  • Переход по ссылке применяет замок молча, без диалога подтверждения; с &solve решение запускается сразу.
  • Переход между настройками и решением — pushState, поэтому браузерные «назад/вперёд» переключают вкладку.
  • Ходы в режиме исследования и позиция воспроизведения в ссылку не попадают: делится всегда исходная конфигурация, решение пересчитывается на той стороне.
  • Если ?lock есть, но не разбирается, появляется уведомление о повреждённой ссылке (отсутствие ?lock — норма и молчит).

Поиск по базе замков

🔍 (или Ctrl+K) открывает поиск по базе реальных сундуков/дверей из Gothic 1 Remake. Кнопка недоступна (с подсказкой почему), если не загрузились база или библиотека поиска.

Одно поле, режим определяется автоматически:

  • Текст — нечёткий поиск (опечатки не страшны) по названию и тегам сразу на всех 4 языках базы (ru/en/de/uk), независимо от текущего языка интерфейса
  • Позиции — точный поиск по расположению дисков. Поддерживается:
    • через запятую: 4,0,6,3,2,1
    • компактно, по одной цифре на позицию: 406326
    • и так и так — учитывается сдвиг на +1 (если считать позиции с 1, а не с 0)

↑/↓ — выбор результата без потери фокуса в поле, Enter или клик — применить найденную конфигурацию (переход на вкладку настроек).

Подсказки-совпадения

По мере ввода начальных позиций между барабаном и матрицей появляется ряд карточек реальных замков из базы, чьи диски совпадают с текущими по префиксу слева. Это способ найти замок по видимым дискам и получить его скрытые правила зависимостей.

  • Совпадение считается детерминированно (позиции пользователя 1-based → 0-based базы, без «пробуем оба варианта», как в текстовом поиске).
  • Балл совпадения: score = prefix · (0.7 + 0.3 · count), где prefix = L / N (L — длина совпавшего префикса, N — число плашек), count = 1 / (1 + |дисков − N|). Умножение делает префикс обязательным: замок без позиционного совпадения (prefix = 0) не всплывёт.
  • В список попадают записи со score > 0.25 (по сути L ≥ 2), максимум 3, отсортированные по баллу.
  • Зависимости из матрицы тоже влияют — но только если что-то введено (пустая матрица ничего не меняет). Оцениваются лишь введённые связи: совпадение направления даёт бонус (+0.5), отсутствие связи у замка — мягкий штраф (−0.2), противоположное направление — жёсткий (−1.5, одно противоречие обнуляет замок, вытянуть могут только сильно перевешивающие совпадения). Позиции остаются обязательными.
  • Яркость карточки растёт с баллом; полное совпадение (score ≈ 1) заметно ярче частичных. Совпавшие диски в превью подсвечены зелёным.
  • Третья строка карточки — цепочка связей замка в gothic-формате (A:B-,C+;D:E-, без позиций). Токены зелёные, если совпадают с введёнными в матрицу, красные — если противоречат.
  • Карточка кликабельна целиком — применяет весь конфиг замка (позиции + правила). При наведении полное имя показывается в title (в UI оно может быть обрезано).
  • Раскладка: имя + теги слева, превью дисков справа. Количество карточек зависит от ширины панели (не вьюпорта, через CSS container queries): широкая — 3, уже — 2, узкая — 1.
  • Если база не загрузилась (оффлайн/сбой) — ряд просто не показывается, без ошибок.

Нотация ходов

<id плашки><направление><кол-во шагов>
  • A = влево, D = вправо
  • Количество шагов опускается если равно 1

Примеры: 1A, 2D3, 7A2

Последовательные ходы одной плашки в одном направлении сжимаются: 1A 1A 1A1A3.


Форматы конфигурации

Импорт распознаёт четыре формата автоматически (вставка / Ctrl+V → диалог подтверждения; каждый кодек — самодостаточный юнит в namespace Codecs, блок codecs). Разбор gothic-правил щадящ к пробелам и регистру, но строг к содержимому: связь без указания направления (+/-) — это битые данные, и такой конфиг не импортируется. Экспорт/копирование выдают gothic или JSON (JSON — по Shift).

Конфиг никогда не перетирается молча:

  • Ctrl+V открывает диалог подтверждения с превью замка (позиции + матрица связей) и, если такой замок есть в базе, карточкой найденного. Текст, не похожий на конфиг, просто игнорируется — про «неверный формат» больше не сообщается на каждую случайную вставку.
  • Вставка в барабан — это про цифры. Если в буфере полный конфиг, он применяется целиком только когда матрица пуста (ни одной связи); иначе берутся только цифры, а о том, что конфиг не применён, сообщает уведомление.
  • Позиции вне диапазона не «подгоняются» по одной: набор либо сдвигается целиком, либо отвергается — иначе получился бы конфиг, которого пользователь не вводил.

JSON

[
  {
    "id": 1,
    "positions": 7,
    "currentPos": 3,
    "deps": [
      { "targetId": 2, "direction": "same", "steps": 1 }
    ]
  },
  {
    "id": 2,
    "positions": 7,
    "currentPos": 5,
    "deps": []
  }
]

Ограничения:

  • positions — нечётное, минимум 3, одинаковое для всех плашек
  • currentPos — от 1 до positions; 1 = крайняя левая дырка в плашке (плашка при этом стоит крайне вправо), N = крайняя правая дырка (плашка стоит крайне влево)
  • id — строго последовательность 1..N
  • deps.direction"same" или "opposite"
  • deps.steps — целое положительное число
  • Самозависимости (targetId === id) не допускаются

gothic — позиции + связи

Компактная строка вида 040615 A:B-,C+;D:E-: цифры позиций (0-based) и связи в любом порядке. Требует и позиции, и связи. + = same, = opposite; буквы A..H → плашки 1..8.

«gothic» — принятое в проекте условное название этой нотации, не формат самой игры и не её экспорт; из Gothic 1 Remake пришли только данные базы замков.

dotted — N.позиции.пары

Например 3.531.saaoaa: N плашек, по цифре позиции на плашку (0-based), затем по символу на упорядоченную пару (from-мажорно, self пропущен) — s = same, o = opposite, a = нет связи.

byte-array (unlockmyloot v2)

Компактный base64url-код (как в ?lock= на unlockmyloot) — их ссылки импортируются напрямую. Каноничный (фиксированные длины, нулевые pad-биты).


Сложность генерации

Кнопка Минимум плашек Длина решения
🟢 Лёгкая 2 7–13 ходов
🟡 Средняя 5 14–20 ходов
🔴 Сложная 7 ≥ 21 хода

Длина считается по сжатой нотации (1A3 = 1 ход).


Архитектура

Один файлindex.html, без зависимостей и шага сборки.

Воркеры

BFS и генерация выполняются в Web Workers через Blob URL — UI не блокируется.

Solve-воркер (один) — запускается при нажатии РЕШЕНИЕ, возвращает минимальный путь с групповой оптимизацией (см. ниже).

Пул random-воркеров (до 8, по числу CPU) — параллельно перебирают случайные конфигурации. Первый, нашедший подходящую, останавливает остальных через worker.terminate().

Прогресс вычисления отображается в оверлее:

Состояний проверено: 5 222 тыс.     ← для BFS
Попытки: 47 / 400 · 5 222 тыс.      ← для рандомизации
├─ #1    5 попыток ·   412 тыс.
├─ #2    8 попыток ·   630 тыс.
...
└─ #8    5 попыток ·   395 тыс.

Решатель (bfsSolveGrouped)

Единственный солвер в проекте — им пользуются и кнопка РЕШЕНИЕ, и рандомизатор. Кратчайших по нажатиям решений обычно много, и наивный BFS вернул бы первое попавшееся, которое «зигзагом» скачет между плашками. Поэтому алгоритм трёхфазный, точный:

  1. BFS от старта → distS до обнаружения цели на глубине L (минимум нажатий — гарантия BFS)
  2. Обратный BFS от цели по «коридору» кратчайших путей (distS + distG == L; ходы обратимы, граф неориентированный)
  3. Точный DP по коридору с узлом (состояние, последняя плашка) — минимум переключений. Направление в узле не нужно: на кратчайшем пути плашка не может пойти дважды подряд в разные стороны (это возврат в предыдущее состояние)

Итог: минимальное число нажатий и минимально возможный список шагов (напр. 41 нажатие: 30 групп у наивного BFS → 11 у нашего). Для патологически широких коридоров (>200 тыс. состояний) — жадный fallback «предпочитай ту же плашку», по-прежнему минимальный по нажатиям.

Прогресс стримится через onProgress каждые 2000 открытых состояний; максимальное пространство — positions^N (7 позиций × 8 плашек ≈ 5.7 млн). Сложность рандома фильтруется по длине группового решения — то есть ровно по тому списку шагов, который увидит пользователь.

Кеш решения

Решение, найденное при генерации (cachedSolution), сохраняется и переиспользуется при нажатии РЕШЕНИЕ без изменения конфигурации. Любое изменение (ход клавишей, правка зависимости, смена количества плашек/позиций) инвалидирует кеш. BFS-решение тоже кешируется при первом вычислении.

Структура стилей

CSS разбит на блоки <style> с семантическими id — так же, как скрипты:

style-base        reset, body, .stage, .hidden
style-layout      панели, стадии, обёртка 3D
style-preview     read-only виз: мини-матрица связей, карточка превью импорта
style-controls    заголовки, кнопки, инпуты, пилюля поиска
style-config      барабан позиций + матрица зависимостей
style-notation    диалог справки по нотации
style-search      диалог поиска, карточки результатов, подсказки-совпадения
style-solve       список решения, карточки шагов, разделитель explore, «уже решён»
style-overlay     вспышка блокировки, оверлей расчёта, диалог импорта, тосты, легенда
style-tokens      палитра (:root custom properties)
style-scene       3D-сцена: плашки, грани, дырки, пины, переключатель языка
style-responsive  медиа-запросы под вьюпорт

Несколько блоков применяются в порядке документа — ровно как один склеенный лист. Поэтому перенос правила между блоками меняет каскад: новые правила добавляются в блок своей зоны, а перемещение между блоками считается изменением поведения, а не рефакторингом. !important в проекте нет — так и держим.

Структура скриптов

Блоки <script> имеют семантические id — вытащить нужный: grep 'id="<имя>"' index.html.

solver-src    Ядро решателя: center(), computeMove(), нотация,
              bfsSolveGrouped(), compressPath(). Исполняется страницей
              И инъектится в Blob воркера — один исходник, два потребителя
state         Константы, state, makePlate(), TRANSLATIONS, t(), setLanguage()
ui-utils      showToast(), copyToClipboard() — общие UI-утилиты
game-logic    applyMove(), getBlockingPlateId(), cycleActivePlate(), flashBlockedPlate()
render-scene   buildScene(), updateScene(), posToOffsetX() (3D-сцена)
render-preview posStripHTML(), depMatrixHTML(+depCellColor/Icon), depDirIcon — read-only виз
codecs        Codecs = {json,gothic,dotted,bytearray}; глобали-фасад:
              validatePlates(), PARSERS, parseConfig(), looksLikeImportConfig()
url           Шаримый ?lock=<dotted>[&solve]: urlQueryFor/urlReadConfig (чистые),
              urlReplace/urlPush, urlApplyOnLoad, urlReconcile (popstate)
config        Барабан позиций (posSetPlateValue — единая точка смены позиции:
              ввод/±/стрелки/колесо/клик по дырке), матрица (renderMatrix,
              depCellHTML), импорт/экспорт, рандомизация
solve-ui      Список шагов-карточек, навигация, авто-воспроизведение,
              addExploreMove(), returnToSolution()
keyboard      Обработчик клавиатуры (config + solve + explore)
init          init()
worker-src    Точка входа воркера: только onmessage (solve + рандомизатор);
  (text/x-worker)  логика — из solver-src
worker-host   createWorker(), пул, прогресс, отмена
db-search     loadChestDb(), Fuse.js поиск, поиск по позициям, рендер карточек;
              computeChestHints() + renderChestHints() — живые подсказки

База данных замков

chests.json — то, что грузит в браузере поиск (fetch('chests.json')). Собирается из трёх слоёв:

chests.ini                     сырец: community-набранная база (опечатки,
   │                           разнобой языков; НЕ лежит в репозитории)
   ▼ parse + normalize
tools/db-decisions.json        слой решений («git rerere»): каждое ручное
   │                           решение записано один раз и переигрывается
   ▼ apply                     при каждой пересборке
tools/gaps/ → translations     переводы Google-Translate-раундтрипом
   │                           (заполняют только недостающие языки)
   ▼
chests.json                    итог, коммитится, грузится приложением
npm run build:db    # полная пересборка: ini + decisions → chests.json
npm run review:db   # веб-ревью дублей/обогащений → http://localhost:3210

Канонический ключ

Идентичность замка = pos.join(',') + '|' + отсортированное множество связей. Не зависит от порядка записи правил (B:D-;C:B-C:B-;B:D-) — старый дедуп сравнивал правила строкой и пропускал переупорядоченные дубли. Все слои (overrides, translations, очередь ревью) адресуются этим ключом, поэтому решения переживают любые изменения сырца.

Слой решений — tools/db-decisions.json

{ "v": 1,
  "drops":        [ "<канонический-ключ>" ],
  "overrides":    [ { "key": "", "note": "review: dedup", "entries": [ {} ] } ],
  "additions":    [ { …запись без аналога в ini… } ],
  "translations": { "<key>": { "byId": { "<id>": { "desc": { "de": "" } } } } } }
  • drops — канонические ключи, которые исключаются из вывода (битые/неисправимые данные ini — например, зависимость без указания направления). Так эти записи не вернутся при повторном импорте ini, без правки самого chests.ini (он может приходить извне).
  • overrides — каноническая группа из ini заменяется записанными entries. Так хранятся: bootstrap-заморозка (см. историю ниже), слитые дубли, обогащения.
  • additions — записи, которых нет в ini (например, будущие импорты).
  • translations — переводы по ключу; применяются только в недостающие языки (fill-only: машинный перевод никогда не затирает явное имя из ini или отредактированный тобой мёрж). byId адресует конкретную запись внутри неслитой группы дублей.

Группы-дубли без решения не сливаются молча — попадают в отчёт REVIEW-NEEDED и в очередь ревью. Записи без внятного имени не выбрасываются: идут с name: {} и стабильным id lock-<позиции>, а UI показывает локализованную заглушку «Неизвестный замок».

Формат записи

{
  "id": "swamp-camp-cor-kalom-hut",
  "name": { "ru": "", "en": "", "de": "", "uk": "" },
  "desc": { "ru": "", "en": "", "de": "", "uk": "" },
  "rules": "A:B+,C-;B:A-",
  "pos": [4, 0, 6, 3, 2, 1],
  "tags": ["болотный лагерь", "кор галом"],
  "img": []
}

desc — опциональное описание (локация + лут); пока только хранится, показ в UI — отдельная задача. cells не хранится — считается как pos.length.

Ревью-тулза (npm run review:db)

Веб-страница без зависимостей: один пункт очереди за раз, все записи-кандидаты бок о бок + редактируемый итоговый вариант (id, имена/описания по языкам, теги; позиции/правила read-only). «Принять» немедленно пишет решение в db-decisions.json, «Пропустить» просто листает. Очередь: канонические группы-дубли + внешние предложения из tools/review-queue.json (обогащения, конфликты, новые замки). Пункт гаснет только когда решён его собственный тип — мёрж дублей не прячет висящее обогащение того же замка.

Эвристики предложенного мёржа: id — от «лучшего» кандидата (длина имени + теги), имена/описания — самый длинный текст на язык, теги — объединение без регистро-дублей.

Синхронизация с unlockmyloot (node tools/sync-uml.cjs)

unlockmyloot.com — открытый (AGPL) решатель с курируемым каталогом замков. Тулза качает их ru/en-страницы (или --cache), достаёт короткие имена из хлебных крошек и описания из meta, декодирует конфиг из ?lock=-кода (битовый поток: 3 бита — число плиток−3, 1 бит ориентации, по 3 бита на штифт, по 2 бита на связь; base64url) и классифицирует против нашей базы:

  • enrich — канонический ключ совпал: предложение заполнить наши пустые desc их описаниями (наши имена по умолчанию побеждают);
  • conflict — правила совпали, позиции разошлись: у кого-то опечатка, в note обе версии — вердикт за человеком (обе стороны обычно решаемы, солвер не арбитр);
  • add — неизвестный замок: готовая запись в additions.

Всё идёт через очередь ревью — вслепую не импортируется ничего. Из их репозитория берутся только числовые коды и тексты страниц, код не заимствуется.

Переводы (node tools/translate-gaps.cjs)

Раундтрип через Google Translate + обязательная AI-сверка:

node tools/translate-gaps.cjs --export      # tools/gaps/gaps-<lang>.txt + .map.json
# → вставить txt в Google Translate, результат сохранить построчно
#   как tools/gaps/gaps-<lang>.translated.txt (порядок и число строк 1:1!)
node tools/translate-gaps.cjs --import de   # → staging (translationsPending)
node tools/translate-gaps.cjs --import uk
# → AI-сверка: ассистент сверяет staging с tools/gothic-glossary.json и
#   каноном Gothic (имена, локации, термины), выдаёт нумерованный список
#   подозрений с фиксами; после вердиктов владельца патчит staging
node tools/translate-gaps.cjs --finalize    # staging → translations
npm run build:db

Экспорт кладёт по одному тексту на строку (.map.json — параллельный манифест key/id/field), поэтому Google Translate можно скармливать файл целиком. Staging (translationsPending) существует ровно затем, чтобы машинный перевод не попал в базу до сверки: канон ловит то, что GT не знает — Ravens Turm (имя, не «Rabenturm»), Cor Kalom, Sektierer (не «Kultisten»), Feuerschutzring («кольцо от огня», а не огненное), Burg (не «Schloss»), укр. «шкода» (урон, не «збитки»), «столові прибори» и т.п.

translate.sh (npm run translate:db) — старый батч-перевод имён через claude --print; оставлен как альтернатива для массовых имён.

История миграции (июль 2026)

  1. Bootstrap (tools/bootstrap-decisions.cjs, одноразовый): chests.json успел разойтись с chests.ini — переводы и ручные правки жили только в json, пересборка была разрушительной. Инструмент отдиффил чистую регенерацию против текущего json по каноническим ключам и заморозил все 393 отличающиеся группы как overrides с текущим содержимым. Проверено: пересборка воспроизводит базу 1:1.
  2. Дедуп-ревью: 83 канонические группы дублей (пережившие старый строковый дедуп), 78 слито владельцем через ревью-тулзу — база ужалась 508 → 394 записи с объединением имён/тегов/языков.
  3. Обогащение: sync с unlockmyloot — 38 замков, из них 36 точных совпадений (их база оказалась подмножеством нашей), 0 новых; 34 обогащения принято, 32 замка получили описания (локация + лут) ru/en.
  4. Переводы: 30 описаний прогнаны через Google Translate в de/uk, AI-сверка поймала 16 расхождений с каноном до финализации. Итог: 32 замка с описаниями на всех четырёх языках.

Разработка

# Открыть в браузере
open index.html          # macOS
xdg-open index.html      # Linux

# Запустить тесты
npx playwright test

# Прогон всей базы через решатель (~3 мин, отдельно — за флагом RUN_CORPUS)
npm run test:corpus
CHEST=<часть-id> npm run test:corpus   # один-два сундука с полной диагностикой

# Одноразовая настройка pre-push хука (запускает тесты перед каждым push)
git config core.hooksPath .githooks

Тесты — Playwright, элементы ищутся только по data-test-id. Инварианты, которые раньше держались на внимательности, закреплены отдельными наборами: tests/i18n.spec.js (все три локали имеют один и тот же набор непустых ключей, каждый data-i18n существует) и tests/review-findings.spec.js (по тесту на каждый дефект, найденный полным ревью).

Без сервера, без транспиляции. Все изменения видны после обновления страницы.

Ограничения

  • Количество плашек: 2–8
  • Количество позиций: нечётное, минимум 3
  • Зависимости: steps = 1 (редактирование через UI), больше — только через импорт
  • Циклические зависимости допустимы (первый вызов в цепочке побеждает)

Поддержка браузеров

Chrome/Edge 108+, Firefox 110+, Safari 16.4+. Полифилов нет и не будет — порог задают три конкретные вещи:

Что используется Chrome/Edge Firefox Safari
dvh (высота вьюпорта без скачков от адресной строки) 108 101 15.4
контейнерные запросы (container-type + @container) 105 110 16.0
lookbehind в регулярке ((?<![,\d]), разбор вставки) 62 78 16.4

Первые две деградируют мягко — поедет вёрстка. Lookbehind нет: регулярка проверяется при разборе скрипта, поэтому на Safari старше 16.4 весь блок падает целиком и приложение не запускается.

About

Lockpich for Gothic Remake

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages