# Сервер, общая карта и Last Survivor

Статус на10.11.2026: co-op1–4 и Last Survivor2–4 реализованы и проверены локально; см.53_COOP_SHARED_BOARD.md и54_LAST_SURVIVOR.md. Публичный сервер и настоящая почта ещё не подключены. Ниже — полная целевая спецификация.

## 1. Оригинал и сознательные изменения

В GemTD2.0 co-op: общий замок и team_gold, отдельная пятёрка каждого строителя, общий сохранённый gemtd_pool, сохранённый owner башни и совместное управление. Волна ждёт готовности занятых слотов. Начальные 32/16/8/0 булыжников для 1/2/3/4. Базовые HP-коэффициенты 0.69/2.91/5.17/7.81 и speed0.69/1.01/1.19/1.42. Число врагов не умножается наP. Прежняя сложность в найденном обработчике не снижается при уходе игрока.

В исходном Race игроки размещены в собственных квадрантах; сохранение шаблона использует 17×17. Это **не** доказательство, что нам нужно уменьшить отдельную веб-карту до 17×17. По текущему требованию берём одинаковые независимые solo-поля 37×37; original Race сохраняем в сравнительной матрице, а не незаметно смешиваем с новым Last Survivor.

## 2. Кто отвечает за матч

**Единственный авторитет — сервер.** Клиент отправляет намерения и показывает результат. Он не определяет HP, добычу, RNG, победителя, рейтинг, баланс покупок или владельца объекта. Offline остаётся доступным отдельно; изменённый localStorage нельзя загрузить как рейтинговый результат.

Предложение: общее чистое ядро TypeScript/ESM, сервер Node LTS, комнаты поверх WebSocket; Colyseus — кандидат на транспорт/комнаты/синхронизацию, а не замена нашей симуляции. Официальные документы подтверждают серверные изменения состояния и полный snapshot при успешном reconnect: [state](https://docs.colyseus.io/state), [reconnection](https://docs.colyseus.io/room/reconnection). Совместимость версии и измерения проверить в отдельном сетевом образце, закрепить зависимости lock-файлом.

Текущий Sites — статический клиент. Долгоживущую Node-комнату нельзя просто положить в его статический dist: нужен отдельно развёрнутый игровой сервис и HTTPS API. Альтернатива — отдельное исследование Workers/Durable Objects; не вести две серверные реализации одновременно. Профиль/итоги: PostgreSQL как источник истины; Redis опционально для краткоживущих очередей, rate limits и presence. Нельзя хранить единственную копию результатов только в Redis.

Компоненты: browser → authenticated API → lobby/matchmaker → room worker → journal/checkpoints → result finalizer → PostgreSQL/outbox → statistics projector. Сервер комнаты не выполняет платёжные запросы и отправку почты внутри игрового tick.

## 3. Модель состояния и команды

`Room`: id, mode, state(lobby/loading/running/recovering/finished/aborted), region, seed, ruleset/content/map versions, seasonId, ranked, tick, revision, leaseEpoch, roster, difficultyProfile, startedAt, resultId.

`PlayerSlot`: playerId, slotId, displayNameSnapshot, role, connected, reconnectDeadline, ready, draftState, ownerColor, contribution, eliminatedTick, surrenderState. Email и сессионные секреты не входят в snapshot матча.

`BoardState`: boardId, mapVersion, structures, route, lives, goldLedgerRef, waveState, RNGStreams. Co-op имеет один BoardState; Last Survivor — по одному на слот, с единым room clock.

Конверт команды: `protocolVersion, roomId, matchEpoch, commandId, clientSeq, expectedRevision, type, payload`. actorId берётся из проверенной сессии соединения. Ответ: `commandId, accepted/rejected, reasonCode, appliedTick, revision`. Старое/повторное намерение возвращает тот же исход без повторного применения. Ввод клиента не содержит доверенного serverTick.

Команды: place/remove/resolveDraft/merge/craft/moveStone/ready/unready/surrender/ping. Сервер проверяет фазу, координаты, права, ресурсы, рецепт, занятость и маршрут. В co-op две команды на одну клетку выполняются последовательно в одном порядке: первая принята, вторая отклонена; частично списанного золота быть не должно. Изменение карты и проверка пути — одна атомарная операция.

Симуляция 30Hz как нынешнее ядро; сетевые дельты стартово 10Hz, UI-сводки 1–2Hz, полный snapshot при входе/расхождении. Клиент интерполирует движение между подтверждёнными точками. Не слать тысячи HP/частиц каждый кадр; визуальные частицы не являются сетевыми сущностями. Компрессия/формат — после измерения, с явным пределом размера сообщения и очереди клиента.

## 4. Co-op: полный игровой цикл

1. Лобби по ссылке/коду,1–4 слота, nickname/ready, выбор режима и публичности. Только до старта: параметры сложности, исходных способностей, общей передачи управления, endless. Нет обязательного чата на первом этапе: достаточно пингов и быстрых сообщений.
2. Все загрузили версии контента → сервер фиксирует состав и seed → создаёт общую карту, замок и соответствующие исходные камни. Не начинать волну для игрока, ещё не загрузившего поле.
3. Каждому выдаётся своя пятёрка. Свободные клетки общие. Строитель не может перекрыть маршрут и «застолбить» чужую текущую пятёрку без явного разрешения.
4. Каждый выбирает результат своей пятёрки. Готовность ставится только после завершения выбора. Пустые слоты не блокируют. Кнопка запуска показывает «готовы 2/3», а не начинает бой раньше остальных.
5. Все готовы → короткий общий countdown → одна очередь врагов по общему маршруту. Никаких локальных пауз/скоростей в ranked. В casual возможна общая пауза по согласованным правилам.
6. Разрешённые фазой синтез/удаление/перенос выполняются сервером. Последний враг завершает волну; сохраняются статистика и checkpoint; далее новая стройка.

### Владение и помощь союзника

Отложенное уточнение8 ноября2026 — **COOP-UI-01**: первое нажатие выделяет клетку/область; затем доступны независимые действия — повторное нажатие на ту же клетку для строительства либо кнопка «Подсветить» для союзников. Подсветка передаёт временную метку с обозначением автора и не расходует камень/золото, не запускает бросок, не меняет маршрут и не резервирует клетку. Сервер проверяет координаты и ограничивает частоту пингов. При позднейшей постройке позиция проверяется заново: чужая подсветка не даёт исключительных прав на клетку. Цель — координация без голосового или текстового чата. **Реализация отложена по просьбе пользователя; сейчас изменения в игру не вносятся.**

У каждой башни остаются `createdBy`, `ownerId`, `controlledByPolicy`, `ingredientLineage`. По умолчанию любой может осмотреть и поставить пинг. Изменять/поглощать чужую сохранённую башню можно при включённом общем управлении либо по одноразовому запросу её владельцу. Черновую пятёрку завершает только её владелец. Это предложенная защита для веба; оригинальное общее управление шире.

Общее золото — один ledger, не копии суммы у каждого. Потратить его может разрешённая роль в пределах лимита; дорогое командное действие показывает расход и получателя до принятия. Выбор стратегии хранения владельцев и общего управления фиксируется в лобби. Нельзя продавать союзнику способность незаметно поглощать чужие результаты.

### Пропорциональная сложность

Для нового `coop-proportional-v1` стартовое предложение: `HP(P)=soloHP * P`; скорость, число врагов, жизни и штрафы за пропуск остаются solo. С каждымP команда получаетP результатов пятёрки, поэтому это понятная стартовая пропорция, **не доказанный окончательный баланс**. Ауры/общие рецепты усиливают синергию; проверить 1/2/3/4 сценариями одинакового уровня игроков. Нельзя затем незаметно добавить ещёP к count и speed.

Оригинальная нелинейная таблица — отдельный `coop-classic2-v1`, со своим рейтингом и контентными условиями. В правилах матча отображать название профиля и действующие коэффициенты. Размер команды входит в ключ лидерборда.

`difficultySlots` фиксируется на старте матча. Disconnect не уменьшает сложность и не даёт переиграть более лёгкую волну. В casual до начала следующей волны возможна явная пересборка состава с новым difficulty profile; такой матч получает `rosterChanged=true`, не сравнивается с неизменным ranked-составом. Игровой hotjoin в ranked после старта первоначально запрещён; зритель может войти отдельно позднее.

### Уход, восстановление, зависшая готовность

Предложение:90 сек grace для возвращения. Во время боя сервер продолжает ход. В стройке countdown готовности останавливается на ограниченное окно восстановления, а не навсегда. Если игрок не вернулся: разрешённая передача его управления союзнику; незавершённую пятёрку завершить по заранее выбранной детерминированной политике с записью `autoResolve`. Не тратить общее золото автоматически. Повторный вход восстанавливает slot, draft и права; не создаёт второго участника.

Grace/pause — параметры ruleset. В ranked выход не возвращает расход/вклад и не уменьшает сложность; умышленный выход фиксируется отдельно от технического обрыва. Если все отключились — хранить комнату ограниченное время, затем завершить `abandoned`, не выдавая победу без игры.

## 5. Last Survivor: одинаковые условия и последний выживший

Два–четыре игрока, отдельные карты, одинаковые жизни/экономика/контент/seed/сложность. Нет отправки врагов или покупных помех в первой версии. Один серверный tick управляет всеми досками. Урон, золото и башни не смешиваются между досками.

Камни генерировать потоком `(matchSeed, wave, playerDraftOrdinal, drawSlot)`, **одинаковым для всех игроков**; не общим изменяемым RNG, где порядок чужих кликов съедает броски. Волновые варианты/элита тоже одинаковые по wave/spawnOrdinal. Позиция постройки не должна менять бросок. Приватные предстоящие камни не рассылать противникам.

Темп ranked: общие раунды. Стройка имеет заранее объявленный лимит (предложение 60 сек с доступным casual-вариантом без спешки), все готовы могут сократить ожидание. Бой стартует одновременно. Новая волна после завершения текущей на всех живых досках; быстрая очистка не даёт лишнюю пятёрку раньше других. На стройке по timeout — объявленный deterministic auto-resolve без бонусной переброски. Уточнить UX на телефоне до включения рейтинга.

Игрок выбывает при lives≤0 либо подтверждённой сдаче. Последний живой выигрывает сразу. Если несколько последних погибли в одном server tick, фиксировать sharedPlace/ничью, а не порядок обработки слотов или ping. Предыдущие места — по `eliminatedTick`, одновременное выбывание даёт общее место. Кампания 50 не завершает соревнование, пока живы несколько: они переходят в endless.

Если правила endless или технический лимит не позволяют определить победителя, матч получает `draw/technical_limit`; не выдумывать победителя по донату или случайному порядку. Сданный слот не возрождается после reconnect. После выбывания доступны итог и наблюдение за оставшимися с ограничением приватных данных. Защита от затягивания — серверный таймер стройки и версионированный enrage, не скрытое увеличение угрозы одному игроку.

## 6. Надёжность и эксплуатация

Единственный активный writer комнаты имеет lease+epoch; устаревший процесс не пишет события после восстановления. Долговечный checkpoint на границе волн и стартово каждые 5–10 сек боя, между ними журнал принятых команд и значимых событий. Snapshot содержит все RNG-состояния, очередь spawn/projectiles/statuses и время, но не визуальные GLB. При падении восстановить checkpoint и replay до подтверждённой границы; результат дублироваться не должен.

Сервер не подтверждает durable-команду как окончательно сохранённую до записи журнала; видимое принятие вRAM и гарантированное сохранение — разные ack. При потере неподтверждённого хвоста клиенты получают `recovering` и новый epoch. Не обещать нулевую потерю без теста отказа хранилища.

Проверять лимит частоты команд, размер координат/чисел, допустимые enum, последовательность, права slot, Origin/соединение и срок билета. Комната получает короткоживущий одноразовый join ticket от API. Секреты не писать в replay/log. Клиентский fps и local clock не влияют на исход. Предусмотреть отдельную очередь unrated для отладки, bots не попадают в обычный рейтинг.

Цели первого измерения: p95 tick<20 мс при бюджете 33.3 мс, p99<33 мс; при превышении — уменьшать число комнат на worker, а не замедлять отдельного соперника. Проверять RTT50/150/300 мс, jitter,1–5% потерь, мобильное сворачивание, обрыв 30/90 сек, медленный клиент. Канал планировать стартово до 50KB/s down на игрока,5KB/s up; это цель нагрузочного теста, не измеренное обещание.

Метрики: tick lag, route calculation time, event queue, memory/room, reconnect success, result finalization lag, command rejection, snapshot duration. Алерты на зависшую волну/невозможный баланс/двойной lease. Масштабирование по room count и нагрузке, draining при обновлении: новые матчи идут на новую версию, старые завершаются на своей.

## 7. Пакеты работ и приёмка

| ID | Пакет | Обязательная проверка |
| --- | --- | --- |
| N1 |Порты чистого ядра и протокола | Node и browser дают одинаковый hash после журнала |
| N2 |Серверная solo-комната | Клиент не может прислать себе золото/HP/рейтинг |
| N3 |Co-op2, затем 4 | Одна клетка/общий расход/разные пятёрки/согласованный синтез |
| N4 |Ready/отключение/reconnect | Никто не блокирует навсегда; слот не дублируется |
| N5 |Snapshot+replay+failover | Kill worker в бою: восстановление и ровно один результат |
| N6 |Last Survivor | Одинаковые броски независимо от кликов; общий tick; ничья последнего tick |
| N7 |Нагрузочная сессия | Заявленный лимит одновременных комнат измерен, записаны конфигурация и p95/p99 |
| N8 |Границы доверия | Fuzz-команды/повторы/чужой owner/истёкший ticket отклонены |

Нельзя принимать онлайн по скриншоту четырёх вкладок. Нужны независимые клиенты, искусственные задержки, перезапуск сервера и проверка журнала/итогов.
