Quintus слушает клавиатуру на самом канвасе, а не на document/window;
без autoFocus и явного focus() канвас не получал фокус (особенно после
переприкрепления к DOM на реванш) — WASD/стрелки/space никуда не долетали.
Клик по кнопке D-pad тоже уводил фокус — возвращаем его на канвас.
Дуло танка было почти того же цвета, что фон (#0a0e0c на чёрном) — не
видно. Сделал ярко-зелёным с тёмной обводкой, читается на любом корпусе.
Над блоком кнопок управления — подсказка, что WASD/стрелки тоже работают.
Клиент: ники других игроков рендерились через innerHTML без экранирования — XSS.
Добавлен escapeHtml() для nickname и в списке игроков комнаты.
Сервер:
- Санитайзинг ника и на бэкенде (обрезка длины, вырезание control/HTML
символов) — защита не только для этого клиента, но и для любых будущих
потребителей (логи, другие рендеры).
- onMessage обёрнут в try/catch: payload.value<T>() кидает json::type_error
на несовпадении типа поля — одно кривое сообщение могло уронить процесс
со всеми активными играми.
- Лимит на общее число комнат (200) — иначе клиент, спамящий
lobby.create_room, разгонял rooms_ до исчерпания памяти.
- Fix: одна из раскладок coop-карты клала укрытие поверх стартовой позиции
игрока — танк рождался внутри кирпича и не мог выехать. Тайл под каждым
стартовым танком теперь расчищается после построения карты.
Docker: gameserver теперь работает от непривилегированного пользователя,
а не от root.
nginx: rate-limiting на /ws (5 подключений/с на IP, до 20 одновременных) —
защита от флуда соединениями/комнатами; CSP и security-заголовки
(X-Content-Type-Options, X-Frame-Options, Referrer-Policy).
Клиент: audio.js синтезирует эффекты без внешних файлов (выстрел,
взрыв, разрушение кирпича, победа/поражение) — вписывается в
"чистый HTML/CSS/JS" стек. AudioContext разблокируется по клику
"готов" (браузеры блокируют автовоспроизведение без жеста).
Сервер: 3 варианта раскладки coop-карты (случайный выбор при старте
матча) вместо одной фиксированной.
Fix: сервер разрушал кирпичи в своей копии карты, но никогда не сообщал
об этом клиенту — стена оставалась видимой навсегда, хотя сквозь неё уже
можно было стрелять. Теперь game.state включает destroyed_tiles,
клиент убирает соответствующий спрайт стены.
Сервер (Game.hpp/cpp):
- coop-режим: AI-противники (волна 8, до 3 одновременно), простое
преследование базы с рандомизацией, база с укрытием из кирпича.
- Комната стартует соло (1 игрок), второй может подключиться позже.
- Fix: хитбокс пули по базе брал радиус танка вместо пули.
- Fix: AI при столкновении со стеной теперь сразу перерешает направление,
а не долбит в одну точку до 3с — без этого база падала за секунды.
Клиент:
- Танк: гусеницы, башня, заметный ствол (было — незаметный нубик).
- Рендер базы (укрытие/флаг), AI-танки — отдельным оттенком зелёного.
- Экран победы/поражения — крупный glitch-текст на весь оверлей вместо
строки текста; кнопки "играть ещё" (без reload) и "в лобби".
- Блок кнопок управления (D-pad + огонь) справа от игрового поля.
- Лобби: coop стартует по кнопке "готов" в одиночку.
nginx: try_files с fallback на .html — чистые URL (/projects, /game)
без расширения, старые /projects.html и т.п. продолжают работать.
В pvp-комнате с одним игроком кнопка "готов" ничего не делала (сервер
не стартует матч без 2 игроков) без единого объяснения. Теперь кнопка
задизейблена и показывается статус, пока не зайдёт второй игрок.
Сервер: Game — authoritative-тик 20 Гц через uSockets timer, движение/коллизии
танков и пуль, разрушаемые кирпичные стены, respawn, победа/поражение/ничья,
технический проигрыш при дисконнекте. game.ready запускает матч, когда оба
игрока в pvp-комнате готовы.
Клиент: заведён Quintus.js (MIT, vendored) — Tank/Bullet/Wall спрайты,
рендер по снапшотам game.state, keyboard-инпут (WASD/стрелки/space),
экран результата матча.
Найден и исправлен use-after-free: колбэк onFinished_ синхронно удалял
Game изнутри его же метода — перенесено на uWS::Loop::defer.
actions/checkout тянет свой код с github.com и берёт URL из внутреннего
server_url — оба варианта требуют сети, которой у джоб-контейнера
раннера нет вообще (ни наружу, ни туда). Ручной git clone по
http://gitea:3000/... остаётся внутри docker-сети раннера/Gitea.
actions/checkout использует внутренний server_url Gitea (http://gitea:3000/...),
который джоб-контейнер раннера не резолвит (не в docker-сети Gitea).
Явный git clone по https://git.cactoz.su/... не зависит от этой внутренней сети.
.gitea/workflows/deploy.yml: пуш в main -> build+up прямо на runner'е
(docker.sock смонтирован, registry и SSH не нужны).
docker-compose.prod.yml: внешняя сеть caddy_net, container_name cactoz-nginx
для reverse_proxy из Caddy.
gameserver/Dockerfile: зависимости (uSockets/json) вынесены в отдельный
кэшируемый слой — пересборка после правки src/ упала с ~6.5 мин до ~9 сек.