No description
  • TypeScript 99.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-25 23:53:38 +05:00
.github/workflows Adopt Biome for lint and format with tuned config 2026-09-18 10:09:41 +05:00
campaign-editor Редактор кампаний вынесен в отдельное приложение campaign-editor (vite :5179): починены мёртвые кнопки — el() присваивал onClick/oninput как DOM-свойства, vanilla DOM их игнорирует, теперь on*-пропсы вешаются через addEventListener; убрана зависимость от отсутствующих ключей i18n; UI из оверлея стал самостоятельным приложением с кнопкой «Удалить», переименованием по id при сейве, Ctrl+S, Esc и beforeunload-guard; из editor/ удалены оверлей, кнопка КАМПАНИИ и кампанийные функции api; roundtrip:campaigns переехал в новый workspace, корневой package.json и AGENTS.md обновлены 2026-09-25 09:27:57 +05:00
campaigns Кампания этап 0: манифесты campaigns/*.json (shared parse/derive/логика замка), серверный campaignStore + GET /api/campaigns и /api/campaign/:id (прогресс под токеном владельца), таблица campaign_progress в SQLite-профиле, опция комнаты campaign:<id> с записью прогресса и campaign-блоком в missionResult; миграция формата карт: campaignOrder упразднён (толерантный игнор в парсере), debrief добавлен 2026-09-25 04:13:19 +05:00
client Разрешить пересечение surface-регионов с правилом «сильнейший побеждает» 2026-09-25 23:53:38 +05:00
deploy Stage 4: deploy & lobby - colyseus matchmake + static client on one port, Caddy/Docker/PM2 configs, room list lobby (metadata: name/players/wave), create with name, join via ?join= invite link with copy, room name metadata, reconnect overlay countdown with nick/class persistence, per-message rate limit with spam kick, server-per process room limit, lobby/limits/spam tests 2026-09-09 17:16:51 +05:00
desktop Make desktop:dev self-contained: start Colyseus server alongside vite and electron 2026-09-18 22:41:53 +05:00
docs Поправлены ссылки на перенесённые доки: 39 вхождений старых путей docs/*.md заменены на docs/archive/* и docs/ideas/* в комментариях кода (client/server/shared), README.md, maps/AGENTS.md, client/AGENTS.md, maps/map.schema.json и во взаимных ссылках внутри самих перенесённых файлов; разделительные комментарии в profileStore.ts и profile_store.test.ts подтянуты к ширине 79. Описание campaignOrder в map.schema.json отредактировано вручную — оно не генерируется из scriptSchema.ts (проверено), gen:schema его не затирает. Server 822 tests зелёные (94 файла), client tsc --noEmit + build зелёные, map.schema.json валиден. 2026-09-25 16:42:49 +05:00
editor Разрешить пересечение surface-регионов с правилом «сильнейший побеждает» 2026-09-25 23:53:38 +05:00
maps Разрешить пересечение surface-регионов с правилом «сильнейший побеждает» 2026-09-25 23:53:38 +05:00
server Разрешить пересечение surface-регионов с правилом «сильнейший побеждает» 2026-09-25 23:53:38 +05:00
shared Разрешить пересечение surface-регионов с правилом «сильнейший побеждает» 2026-09-25 23:53:38 +05:00
.dockerignore Stage 4: deploy & lobby - colyseus matchmake + static client on one port, Caddy/Docker/PM2 configs, room list lobby (metadata: name/players/wave), create with name, join via ?join= invite link with copy, room name metadata, reconnect overlay countdown with nick/class persistence, per-message rate limit with spam kick, server-per process room limit, lobby/limits/spam tests 2026-09-09 17:16:51 +05:00
.env.example Событийная система пространственного спавна врагов: спавнеры, триггеры и SpawnManager с контролем видимости и бюджетом; поддержка в редакторе карт, ADMIN_TOKEN через .env, debug-оверлей и стрелки триггер→спавнеры 2026-09-19 09:40:00 +05:00
.gitignore Профили игроков: SQLite-хранилище XP/уровней с владением по токену (playerToken/profileToken, x-profile-token), серверное начисление опыта за миссию (в т.ч. за провал, песочница исключена), кривая до 25 ур. (~50 успешных карт), уровни в ростере лобби и XP/level-up в итогах; shared progression.ts, тесты store/award, опциональный better-sqlite3 с деградацией 2026-09-25 01:01:57 +05:00
AGENTS.md Редактор кампаний вынесен в отдельное приложение campaign-editor (vite :5179): починены мёртвые кнопки — el() присваивал onClick/oninput как DOM-свойства, vanilla DOM их игнорирует, теперь on*-пропсы вешаются через addEventListener; убрана зависимость от отсутствующих ключей i18n; UI из оверлея стал самостоятельным приложением с кнопкой «Удалить», переименованием по id при сейве, Ctrl+S, Esc и beforeunload-guard; из editor/ удалены оверлей, кнопка КАМПАНИИ и кампанийные функции api; roundtrip:campaigns переехал в новый workspace, корневой package.json и AGENTS.md обновлены 2026-09-25 09:27:57 +05:00
biome.json Сданы пункты низкого приоритета ideas-code: отдельный shared/test/ (103 юнита чистой логики контрактов — прогрессия/кампании parse+logic/протокол/геометрия+реестр MAPS/лейблы/профили атак боссов — со своим coverage-ратчетом, подключён в корневой test:coverage); branch-пол сервера поднят 70→75 (новые tickSwitchHooks + minigame_controller.test.ts: гварды open, диспетч result, троттлинг progress, дедлайн-тик, interrupt/cancelAll); чистая логика из HUD.ts вынесена в client/src/ui/hudFormat.ts (secs/objectiveReadout/objectiveColor/dim) с тестами; editor round-trip стал vitest-сьютом editor/test/roundtrip.test.ts (18 тестов) в CI — попутно устранён предсуществующий дрейф: Biome-формат 47ffb14 инлайнил массивы объектов в maps/*.json вопреки канон-принтеру, 15 карт и фикстура перепечатаны принтером, maps/ и editor/testdata/ выведены из-под Biome, editor переведён на vite 7 (партнёр vitest 5); permessage-deflate threshold поднят 0→64 по замеру реальных дельт view-канала (ws-compression-bench.ts: avg 66B/p50 15B, <64B дают ratio 1.13x при ~25мкс — оверхед Z_SYNC_FLUSH съедает выгоду, ~-7мс/с CPU при 360 msg/s), тюнится env WS_COMPRESS_THRESHOLD; CI lint/format уже был сдан ранее (Biome) — отмечено. Server 821 tests зелёные, shared 103, client 336, editor 18, build+lint зелёные, coverage-ratchet не нарушен. 2026-09-25 15:54:25 +05:00
Dockerfile Fix Docker build: copy editor manifest and align node 22 with @types/node 2026-09-18 03:12:14 +05:00
ecosystem.config.cjs Stage 4: deploy & lobby - colyseus matchmake + static client on one port, Caddy/Docker/PM2 configs, room list lobby (metadata: name/players/wave), create with name, join via ?join= invite link with copy, room name metadata, reconnect overlay countdown with nick/class persistence, per-message rate limit with spam kick, server-per process room limit, lobby/limits/spam tests 2026-09-09 17:16:51 +05:00
package-lock.json Сданы пункты низкого приоритета ideas-code: отдельный shared/test/ (103 юнита чистой логики контрактов — прогрессия/кампании parse+logic/протокол/геометрия+реестр MAPS/лейблы/профили атак боссов — со своим coverage-ратчетом, подключён в корневой test:coverage); branch-пол сервера поднят 70→75 (новые tickSwitchHooks + minigame_controller.test.ts: гварды open, диспетч result, троттлинг progress, дедлайн-тик, interrupt/cancelAll); чистая логика из HUD.ts вынесена в client/src/ui/hudFormat.ts (secs/objectiveReadout/objectiveColor/dim) с тестами; editor round-trip стал vitest-сьютом editor/test/roundtrip.test.ts (18 тестов) в CI — попутно устранён предсуществующий дрейф: Biome-формат 47ffb14 инлайнил массивы объектов в maps/*.json вопреки канон-принтеру, 15 карт и фикстура перепечатаны принтером, maps/ и editor/testdata/ выведены из-под Biome, editor переведён на vite 7 (партнёр vitest 5); permessage-deflate threshold поднят 0→64 по замеру реальных дельт view-канала (ws-compression-bench.ts: avg 66B/p50 15B, <64B дают ratio 1.13x при ~25мкс — оверхед Z_SYNC_FLUSH съедает выгоду, ~-7мс/с CPU при 360 msg/s), тюнится env WS_COMPRESS_THRESHOLD; CI lint/format уже был сдан ранее (Biome) — отмечено. Server 821 tests зелёные, shared 103, client 336, editor 18, build+lint зелёные, coverage-ratchet не нарушен. 2026-09-25 15:54:25 +05:00
package.json Сданы пункты низкого приоритета ideas-code: отдельный shared/test/ (103 юнита чистой логики контрактов — прогрессия/кампании parse+logic/протокол/геометрия+реестр MAPS/лейблы/профили атак боссов — со своим coverage-ратчетом, подключён в корневой test:coverage); branch-пол сервера поднят 70→75 (новые tickSwitchHooks + minigame_controller.test.ts: гварды open, диспетч result, троттлинг progress, дедлайн-тик, interrupt/cancelAll); чистая логика из HUD.ts вынесена в client/src/ui/hudFormat.ts (secs/objectiveReadout/objectiveColor/dim) с тестами; editor round-trip стал vitest-сьютом editor/test/roundtrip.test.ts (18 тестов) в CI — попутно устранён предсуществующий дрейф: Biome-формат 47ffb14 инлайнил массивы объектов в maps/*.json вопреки канон-принтеру, 15 карт и фикстура перепечатаны принтером, maps/ и editor/testdata/ выведены из-под Biome, editor переведён на vite 7 (партнёр vitest 5); permessage-deflate threshold поднят 0→64 по замеру реальных дельт view-канала (ws-compression-bench.ts: avg 66B/p50 15B, <64B дают ratio 1.13x при ~25мкс — оверхед Z_SYNC_FLUSH съедает выгоду, ~-7мс/с CPU при 360 msg/s), тюнится env WS_COMPRESS_THRESHOLD; CI lint/format уже был сдан ранее (Biome) — отмечено. Server 821 tests зелёные, shared 103, client 336, editor 18, build+lint зелёные, coverage-ratchet не нарушен. 2026-09-25 15:54:25 +05:00
README.md Поправлены ссылки на перенесённые доки: 39 вхождений старых путей docs/*.md заменены на docs/archive/* и docs/ideas/* в комментариях кода (client/server/shared), README.md, maps/AGENTS.md, client/AGENTS.md, maps/map.schema.json и во взаимных ссылках внутри самих перенесённых файлов; разделительные комментарии в profileStore.ts и profile_store.test.ts подтянуты к ширине 79. Описание campaignOrder в map.schema.json отредактировано вручную — оно не генерируется из scriptSchema.ts (проверено), gen:schema его не затирает. Server 822 tests зелёные (94 файла), client tsc --noEmit + build зелёные, map.schema.json валиден. 2026-09-25 16:42:49 +05:00
tuning.json Единый слой тюнинга баланса: tuning.json с хот-релоадом по образцу bootstrapMaps 2026-09-16 17:43:27 +05:00
tuning.schema.json Normalize codebase formatting with Biome 2026-09-18 10:09:48 +05:00

Swarm 2D — кооп-шутер про волны (веб, 2–4 игрока)

Top-down кооперативный шутер с волнами: Phaser 3 (клиент) + Colyseus (авторитарный сервер, 30 Гц, предикшн + реконнект) + общая TypeScript-логика (shared/). Работается на npm-workspaces.

Структура

game3/
├── PLAN.md               # план закрытой беты (этапы 4–7)
├── maps/                 # внешние JSON-карты (грузятся сервером, отдаются клиенту по API)
├── shared/               # общие константы/математика карты и оружия; формат+реестр карт
├── server/               # Colyseus-сервер: комнаты, бой, волны; прод-вход + статика
├── client/               # Phaser-клиент: лобби, игра, HUD
├── deploy/               # docker-compose + Caddyfile (TLS)
├── ecosystem.config.cjs  # PM2-конфиг (альтернатива Docker)
└── Dockerfile            # сервер + собранный клиент, один образ

Разработка

npm ci
npm run dev        # сервер :2567 + vite-клиент :5173

Клиент доступен на http://localhost:5173, сервер — ws://localhost:2567. Тесты сервера: npm test.

Деплой одной командой (Docker + Caddy)

На хосте с Docker и свободными 80/443:

SITE_ADDRESS=game.example.com docker compose -f deploy/docker-compose.yml up -d --build
  • Один origin: Caddy терминирует TLS (wss автоматически) и проксирует на игровой сервер: страница, matchmake HTTP (/matchmake/*) и WebSocket — порт 2567, отдельных настроек ws-порта нет.
  • Клиент сам берёт адрес сервера из того же origin (client/src/net.ts); для dev это :5173 → :2567, overrides — ?server=ws://хост:порт в адресной строке.
  • Если домен с Cloudflare/другим прокси — терминируйте TLS там и укажите SITE_ADDRESS=:80.

Логи: docker compose -f deploy/docker-compose.yml logs -f game.

PM2 без Docker

npm ci && npm run build && npx pm2 start ecosystem.config.cjs

Сервер сам раздаёт client/dist и пишет логи в logs/server.log (LOG_FILE). Перед ним нужен любой reverse-proxy с TLS на 127.0.0.1:2567.

Переменные окружения сервера

Переменная По умолчанию Значение
PORT 2567 HTTP+WS порт
HOST 0.0.0.0 bind-адрес
PUBLIC_ADDRESS авто адрес процесса для matchmake (нужен только при >1 процессах)
MAX_ROOMS 8 лимит игровых комнат на процесс; сверх лимита создание отклоняется
LOG_FILE — дописывать логи в файл
STATIC_DIR авто-поиск папка статики (по умолчанию собранный client/dist)
REDIS_URL — Redis для matchmaker (driver+presence); без него — in-memory (один процесс)

Один процесс Node держит несколько комнат (лимит MAX_ROOMS). Для горизонтального масштаба — несколько процессов за балансировщиком с общим Redis (см. ниже).

Горизонтальный масштаб (Redis)

Matchmaker (список и резервирование комнат) переключается на Redis по env REDIS_URL (напр. redis://127.0.0.1:6379); без него остаётся прежний in-memory (один процесс). Старт — fail-fast: если Redis недоступен, сервер падает с [boot] fatal: Redis unreachable…, а не работает полу-настроенным. В логе старта видно бэкенд: [boot] matchmaker backend: redis (<host>) либо [boot] matchmaker backend: in-memory.

Каждому процессу — свой PORT и свой PUBLIC_ADDRESS (публичные хост:порт, куда клиент реально ходит через балансировщик). Пример с PM2 (по мотивам ecosystem.config.cjs):

REDIS_URL=redis://127.0.0.1:6379 PORT=2567 PUBLIC_ADDRESS=game.example.com:2567 \
  npx pm2 start server/dist/index.js --name g0
REDIS_URL=redis://127.0.0.1:6379 PORT=2568 PUBLIC_ADDRESS=game.example.com:2568 \
  npx pm2 start server/dist/index.js --name g1

Балансировщик обязан держать WS-сессию на том же процессе (sticky по IP/cookie): резервация matchmake и реконнект привязаны к процессу-владельцу комнаты. TLS терпит Caddy/nginx перед процессами, а PUBLIC_ADDRESS = публичный адрес конкретного процесса.

Игрокам

Игрокам достаточно ссылки https://<SITE_ADDRESS>/ — клиент раздаётся тем же сервером, ничего ставить не нужно; вход через лобби или по invite-ссылке ?join=<roomId>.

Как играть

  • Лобби: список открытых комнат (название, игроки, волна), создание комнаты с названием.
  • Ссылка-приглашение: в игре кнопка «КОПИРОВАТЬ ССЫЛКУ» (?join=<roomId>) — игрок из другой сети открывается сразу комнату и сразу попадает в бой.
  • Управление: WASD — движение, мышь — прицел, ЛКМ — огонь, R — перезарядка, F — полноэкранный режим.
  • Игра занимает всё окно браузера (масштаб RESIZE, HUD и меню перестраиваются при ресайзе).
  • Классы: стрелок / тяжёлый / гранатомёт. Волны, терминал-цель, жизни и респауны.
  • Миссия «Улей» (выбор карты в лобби): большой полигон без волн — непрерывное давление врагов нарастает со временем, высадка в одном углу, прорыв к терминалу и его финальная защита с боссом (25 с удержания) до победы.
  • Миссия «Пепелище» (выбор карты в лобби): литейный двор с боссом-гигантом ЖАРОВИКОМ (r=118, 7200 HP). Он не догоняет — он отнимает территорию: огненный веер, кислотный навес, пылающий таран и токсичный вал оставляют на полу горящие и ядовитые зоны, так что арена под ногами кончается быстрее, чем у босса садится здоровье. Лобовая броня держит 80% пуль — бить надо во фланг, тыл или в открытое брюхо во время вала; на 66% и 33% HP босс впадает в ярость и вызывает подкрепление.
  • Комната: до 4 игроков; комнаты создаются игроками и живут, пока в них кто-то есть; полная комната из лобби скрывается, пустая — удаляется сервером.
  • Ретраггер связи: оверлей «Переподключение… N c» с попытками реконнекта, затем возврат в лобби с сохранёнными ником и классом. За флуд пакетами сервер кикает.

Формат и загрузка карт

Карты не захардкожены в коде. Они лежат во внешней папке maps/ (по одному JSON-файлу на карту: maps/<id>.json) и редактируются отдельно — в текстовом редакторе или в будущем в визуальном редакторе карт. Схема — maps/map.schema.json ("$schema": "./map.schema.json" в каждом файле даёт автодополнение и валидацию).

Поток данных:

  • Сервер при старте сканирует maps/ (папка задаётся env MAPS_DIR), валидирует каждый файл и регистрирует карты в общем реестре (server/src/mapStore.ts::bootstrapMaps()).
  • HTTP API на том же порту: GET /api/maps — лёгкие карточки для лобби (id, name, режим, тема, босс, брифинг, число вышек), GET /api/maps/:id — полное тело конкретной карты.
  • Клиент не знает список заранее: лобби тянет GET /api/maps и строит кнопки, а тело карты догружается по GET /api/maps/:id непосредственно перед входом на неё (client/src/mapStore.ts::ensureMapLoaded()), затем кэшируется.

Формат formatVersion: 1:

  • id — стабильный идентификатор (совпадает с именем файла), name — отображаемое имя.
  • order — позиция карты в реестре.
  • size: {cols, rows} — размер в тайлах (по умолчанию 50×38).
  • mode — "waves" (волны) | "mission" (непрерывное давление + защита терминала) | "sandbox" (тестовая арена).
  • theme — визуальная тема ("snow" — холодная палитра + вьюга), darkness — кромешная тьма.
  • boss — вид босса карты ("crusher", "rotmaw", "gloom", "kraken", "weaver", …).
  • objective: {tile: [tx, ty], holdSeconds} — терминал цели и время удержания до победы.
  • obstacles: [{x, y, w, h}, …] — прямоугольники стен в тайлах.
  • enemySpawns / playerSpawns / generators / relays / lamps — списки точек [tx, ty].
  • weapons: [{x, y, kind?}, …] — подбираемое оружие на карте; kind — основной ствол, без него случайный.
  • stages: {gorge, switchboard, door, terminal} — якоря 4-этапной миссии.
  • pressure: {squads: [[enemyKind, fromSeconds], …]} — кастомная кривая состава сквадов.

Валидация — shared/src/maps.ts::parseMapFile() (бросает ошибку с указанием поля при несоответствии схеме). Чтобы добавить/поменять карту — правится JSON в maps/ и перезапускается сервер; пересборки клиента не требуется.

Масштабирование и адаптивный UI

  • Фиксированная виртуальная высота: canvas занимает 100% окна в реальных пикселях (Phaser.Scale.RESIZE; на 4K — честные 3840×2160), игровая камера масштабируется зумом zoom = высота окна / 1080 (client/src/scale.ts). По вертикали всегда видно 1080 мировых единиц, горизонталь расширяется/сужается по соотношению сторон. Точка отсчёта 16:9 — 1920×1080.
  • UI — в отдельной сцене UIScene (собственная камера, zoom = 1, screen space): текст рендерится в нативных пикселях экрана и остаётся чётким на 4K, в отличие от world-объектов, замасштабленных зумом.
  • Раскладка интерфейса пересчитывается на Phaser.Scale.Events.RESIZE: базовая раскладка — 1080p, отступы от краёв экрана, размеры панелей и шрифты умножаются на h / 1080 (client/src/ui/HUD.ts, client/src/ui/UIScene.ts; меню пересобирается целиком в MenuScene.ts, включая HTML-инпуты).
  • Мир: cameras.main.setZoom() + setBounds карты; UI-сцена стартует/останавливается вместе с игровой сценой, камера мира не влияет на текст HUD.

Защита и лимиты

  • Rate-limit сообщений: токен-бакет (~80 msg/s sustained, burst 160), кик за спам (код 4001).
  • Лимит входа: 4 клиента на комнату (maxClients), очереди ожидания нет — комната заполняется.
  • Враги расталкивают друг друга в толпе (pairwise-раздвижение с проверкой стен) — у цели не слипаются в одну точку.
  • Логирование боевого ядра и киков — stdout сервера + опционально файл.

Диагностика и наблюдаемость

  • Серверные логи миссий: [mission] room=… — старт каждой волны, финал WON/LOST (время, киллы), итог при закрытии комнаты; краши комнаты (onUncaughtException) и процесса (uncaughtException/unhandledRejection) со стеком. С LOG_FILE всё дублируется в файл.
  • Клиентские падения (window.onerror, unhandledrejection) шлются POST /diag на сервер (тот же origin, до 10 репортов на сессию) и пишутся в лог как [client-error]. Sentry при желании подключается поверх того же интерфейса (reportError в client/src/diagnostics.ts).

Нагрузочный профиль и баланс (headless-прогоны)

npm run profile    # 4 бота + волна 5 на 30 Гц: размеры снапшотов и стоимость тика
npm run balance    # полная миссия 4 ботами: время зачистки волн, смерти — кривая сложности

Текущая база (из npm run profile, волна 5 = 27 врагов, 4 бота, 45 с): полный снапшот ~2.2–2.3 KiB, дельта avg ~0.07–0.10 KiB/тик (p95 ~0.26–0.34 KiB, ≈2.3–3.1 KiB/s на клиента), симуляция ~0.10–0.11 мс/тик (p95 ~0.23–0.26 мс) — на процесс с MAX_ROOMS запас большой.

Что изменилось в профиле (работа над 1000+ свармеров, docs/archive/perf-swarmer-tz.md):

  • Синк врагов — per-client AOI (T7 @view) на 15 Гц с 1px-квантованием позиции/угла (T6 mirrorEnemies): state.enemies закрыт @view(), клиент получает только своих врагов из StateView. Профилировщик и perf.test.ts меряют именно VIEW-канал (encodeView/encodeAllView), а не мёртвый no-view encode() (он больше не отдаёт врагов). На стресс-кольце дельта пер-клиента: ~2.4 KiB/тик @400, ~8.6 KiB/тик @1200, ~10.9 KiB/тик @2000 (против ~23.3 KiB/тик до T6).
  • Расталкивание удешевлено (T5): separateEnemies с капом соседей — ~0.2–0.45 мс на 400 врагов в куче (без капа было бы квадратично).
  • Клиентский рендер слоями (T1–T4): cull за экраном, HP-бары одним слоем, атлас роя, переиспользование вью из пула — ниже стоимость отрисовки роя.

Эти же метрики зафиксированы как регрессионный тест server/test/perf.test.ts (входит в npm test): бюджет тика, размер снапшотов (view-канал) и стресс- расталкивание 400 врагов с порогами с запасом ~2–5× от базы — новые механики не должны пройти незамеченными. После каждого крупного босса прогонять npm run profile заново. Константы баланса (shared/src/constants.ts/weapons.ts/enemies.ts: составы волн, HP/speed-кривые, урон) крутятся по выводам npm run balance; вражеские view и пули на клиенте переиспользуются из пула вместо создания/уничтожения каждую волну.

Оптимизации сервера (см. ideas/02-performance.md):

  • separateEnemies — пространственный хеш (грид по TILE), ~O(n) вместо O(n²);
  • pursue — flow-field от тайла цели (один BFS на всех, кэш с TTL 500 мс) вместо per-enemy findTilePath.

Как устроены тесты

npm test поднимает реальный сервер и гоняет смоук-тесты: неткод/движение, бой, волны, реконнект, лобби (метаданные, лимит комнат, join-by-id, кик за спам).