Frontend — чистый HTML/CSS/JS без сборки (резюме, проекты, игра-заглушки). Gameserver — C++ на uWebSockets (CMake FetchContent), протокол hello/lobby.list_rooms/error проверен end-to-end.
11 KiB
ТЗ: cactoz.su
1. Общее
Личный сайт-визитка на домене cactoz.su. Три страницы, единая Matrix-стилистика.
Тема: чёрный фон, зелёный и серебро, моноширинный шрифт (JetBrains Mono / Share Tech Mono), строго и технично — без лишних декоративных элементов.
Ник cacto (кактус) — сквозной мотив:
- Пиксельный кактус (16×16 или 32×32 спрайт, зелёный/серебро на чёрном) как лого в шапке и favicon.
- ASCII-арт кактуса в hero-блоке главной страницы.
- Терминальные префиксы вместо обычных заголовков/навигации:
cacto@matrix:~$ whoami,cacto@matrix:~$ ls projects/,cacto@matrix:~$ ./battlecity. - Названия комнат в лобби генерируются в формате
cactus-room-01,cactus-room-02, ... - Характер, а не просто ник: живучесть, минимум ресурсов, "колючий" стиль текста в отдельных местах (например, error-сообщения).
2. Страницы
| # | Файл | Назначение |
|---|---|---|
| 1 | index.html |
Резюме — PHP/Go backend, опыт КИПиА/АСУТП |
| 2 | projects.html |
Карточки проектов со ссылками на репозитории |
| 3 | game.html |
BattleCity: лобби + игра |
3. Структура проекта
cactoz.su/
├── docker-compose.yml
├── nginx/
│ ├── Dockerfile
│ └── nginx.conf
├── frontend/ # чистый HTML/CSS/JS, без сборки
│ ├── index.html
│ ├── projects.html
│ ├── game.html
│ ├── favicon.ico
│ ├── css/
│ │ ├── theme.css # переменные темы, общий сброс
│ │ ├── resume.css
│ │ ├── projects.css
│ │ └── game.css
│ ├── js/
│ │ ├── matrix-rain.js # canvas-эффект цифрового дождя
│ │ ├── projects.js # рендер карточек проектов из data
│ │ └── game/
│ │ ├── network.js # обёртка над WebSocket, очередь сообщений
│ │ ├── lobby.js # UI лобби: список комнат, создание/вход
│ │ ├── protocol.js # константы типов сообщений, (де)сериализация
│ │ ├── entities.js # клиентские представления Tank/Bullet/Wall для Quintus
│ │ └── main.js # инициализация Quintus, игровой цикл рендера
│ └── vendor/
│ └── quintus/ # Quintus.js, подключается напрямую <script>
├── gameserver/ # C++ / uWebSockets
│ ├── CMakeLists.txt
│ ├── Dockerfile
│ ├── third_party/ # uWebSockets, uSockets, nlohmann/json (submodules)
│ └── src/
│ ├── main.cpp # запуск uWS::App, роутинг /ws
│ ├── Protocol.hpp # структуры сообщений + (де)сериализация JSON
│ ├── Player.hpp # id, nickname, ws-handle, room_id
│ ├── Lobby.hpp / .cpp # реестр комнат, create/join/leave/list
│ ├── Room.hpp / .cpp # жизненный цикл комнаты (waiting/playing/finished)
│ ├── Game.hpp / .cpp # игровой цикл тика, применение input, коллизии
│ ├── Map.hpp # тайловые карты (кирпич/сталь/вода/куст), парсинг
│ ├── Tank.hpp
│ ├── Bullet.hpp
│ └── Ai.hpp / .cpp # AI противника для coop-режима
└── TZ.md
4. Docker
Два контейнера, как договорились:
- nginx — раздаёт статику из
frontend/, проксирует/wsнаgameserver:9001(сUpgrade/Connectionзаголовками для WebSocket). При необходимости терминирует TLS (certbot/сертификаты монтируются volume'ом). - gameserver — multi-stage
Dockerfile: build-стадия (g++/cmake, сборка uWebSockets из submodule), финальный минимальный runtime-образ с одним бинарником.
docker-compose.yml связывает оба сервиса общей сетью; наружу открыт только nginx (80/443), gameserver доступен только изнутри docker-сети.
5. WebSocket-протокол
Один WS-канал на клиента на всё: hello → лобби → комната → игра. Формат сообщений — JSON-конверт:
{ "type": "namespace.action", "payload": { ... } }
Client → Server
| type | payload | Когда |
|---|---|---|
hello |
{ nickname: string } |
Сразу после открытия соединения |
lobby.list_rooms |
{} |
При входе в лобби / по запросу обновления |
lobby.create_room |
{ mode: "coop" | "pvp" } |
Создание комнаты (имя генерируется сервером: cactus-room-NN) |
lobby.join_room |
{ room_id: string } |
Присоединение к существующей комнате |
lobby.leave_room |
{} |
Выход из комнаты обратно в лобби |
game.ready |
{} |
Игрок готов к старту матча |
game.input |
{ seq: number, up: bool, down: bool, left: bool, right: bool, fire: bool } |
При каждом изменении состояния клавиш |
Server → Client
| type | payload | Когда |
|---|---|---|
hello.ack |
{ player_id: string } |
Ответ на hello |
error |
{ code: string, message: string } |
Любая ошибка протокола/логики |
lobby.rooms |
{ rooms: [{ id, name, mode, players: number, max_players: 2, state }] } |
Ответ на lobby.list_rooms, и при любом изменении списка (broadcast всем в лобби) |
lobby.room_joined |
{ room_id, name, mode, players: [{ id, nickname, slot }] } |
Игроку, который вошёл в комнату |
lobby.room_updated |
{ players: [...] } |
Broadcast остальным в комнате при join/leave |
game.start |
{ map: Tile[][], mode, players: [...], tick_rate } |
Когда оба игрока готовы (или для coop — один готов + таймаут) |
game.state |
{ tick, tanks: [...], bullets: [...], base?: {...}, enemies?: [...] } |
Каждый серверный тик (полный снапшот, без дельт — упрощение для MVP) |
game.over |
{ result: "win" | "lose" | "draw", reason } |
Конец матча |
Снапшоты, а не дельты — сознательное упрощение ради простоты клиента и отладки; при необходимости оптимизации это единственное место, которое переделывается (дельты/бинарный формат), протокол на это не завязан жёстко.
6. Игровая логика на сервере
- Тик 20 Гц (50 мс), authoritative-симуляция: клиент не считает физику, только рендерит
game.stateи шлётgame.input. - Сущности:
Tank {id, x, y, direction, lives, fire_cooldown},Bullet {id, owner_id, x, y, direction, speed}, тайлы карты (кирпич — разрушается, сталь — нет, вода — препятствие для танков/не для пуль, куст — визуально скрывает). - Движение — по классике, сеточное/пошаговое (проще для коллизий и портфолио, чем free-form пиксельная физика).
- Коллизии: пуля-стена (разрушает кирпич), пуля-танк (урон/уничтожение), танк-стена/танк-танк (блокировка).
- Режим coop: сервер спавнит AI-противников волнами (спавн-точки сверху карты, простая логика: движение к базе + случайный огонь), оба игрока защищают базу (орла). Поражение — база или оба игрока уничтожены; победа — все враги волны уничтожены.
- Режим pvp: без AI, симметричная карта, у каждого игрока пул жизней (например, 3), поражение — жизни закончились.
- Жизненный цикл комнаты:
waiting(сбор игроков/ready) →playing(активен игровой цикл) →finished(отправленgame.over, комната закрывается через короткий таймаут). - Дисконнект: при обрыве соединения во время матча — пауза с таймаутом (например, 30 сек), затем техническое поражение отключившегося.
7. Лобби
Отдельного REST нет — всё через тот же WS-канал (см. таблицы сообщений в разделе 5). Список комнат обновляется broadcast'ом при любом изменении (создание/удаление/смена состава), не только по запросу клиента.
8. MVP-этапы (предложение)
- Каркас: nginx + gameserver в Docker, статика отдаётся,
/wsecho-проверка. - Вёрстка страниц 1–2 (резюме, проекты) в теме Matrix/cacto.
- Лобби: список/создание/присоединение комнат (без игровой логики).
- Игровая логика PvP (проще двух режимов) → затем coop + AI.
- Полировка: спрайты танков, звук (опционально), доп. карты.
Открытые вопросы, требующие решения до реализации: количество и вид карт для MVP (сколько уровней/волн для coop), спрайты (рисовать самим/найти свободные ассеты в стиле Matrix), таймаут ready-фазы в лобби.