# Статистика, рейтинги, история и сезоны

Статус на10.11.2026: сервер хранит историю последних100 матчей, один командный итог и личные отчёты co-op. Глобальная статистика, сезоны, рейтинг, история авторов ингредиентов и вклад поддержки ещё не реализованы. Локальные сохранения не принимаются как рейтинговые результаты. Ниже — целевая спецификация.

## 1. Разделить понятия

**Качество минерала** — стадии 1–6. **MVP-усиление башни** — отдельные 0–10 уровней оригинала. **Рейтинг игрока** — результат соревнования. **Уровень профиля/достижения** — прогресс активности. Их нельзя показывать одним числом «грейд».

В исходнике DamageStat суммирует округлённый положительный входящий урон фильтра, обновляется примерно раз в секунду и сбрасывается по волне; это не подтверждённый lifetime effective damage. Co-op отправляет Level/Kills/TimeCost/Seed/Towers/Gold/MazeLength и другое, Race — DeathStack/Duration/Wave/Seed; MMR-формула внешнего backend неизвестна. Наша система ниже — самостоятельная спецификация.

## 2. Источник истины: серверные события

Общий envelope: `eventId=(matchId,logicalSequence), leaseEpoch, schemaVersion, serverTick, type, actorId?, boardId, sourceEntityId?, payload`. Идентификатор логического события стабилен при восстановлении и replay; новый leaseEpoch не создаёт второй экземпляр того же факта. Sequence не переиспользуется для другого факта. Авторитетный журнал команд/событий и финальный hash подписываются сервером. Публичный клиент присылает только запрос действия, не `stats:add`.

| Событие | Ключевые поля | Зачем |
| --- | --- | --- |
| MatchStarted/Ended |mode, roster, versions, season, validity, reason | Разделить режимы и доказать итог |
| WaveStarted/Settled |wave/cycle, lives, ticks, seed branch | Прогресс, длительность и graph |
| EnemySpawned/Killed/Escaped |enemyId, wave, killer, weightedScore, leak | Число тел отдельно от очков |
| DamageApplied |rawDamage, mitigated, shieldDamage, hpLoss, overkill, damageType, attacker/owner | Точный фактический урон и причина |
| GoldCredited/Debited |ledgerEntryId, amount, reason, balanceAfter, recipient/team | Доход, расходы, остаток без смешения |
| DraftGenerated/Resolved |family/quality/ordinal, choice | Что выпало и что оставили |
| TowerCreated/Upgraded/Crafted/Removed |definition, grade, owner, ingredientIds, outputEntityId, reason | Коллекция и история синтеза |
| MvpAwarded |tower, levelBefore/After, wave | Усиление отдельно от качества |
| ControlTransferred/PlayerDisconnected |old/new owner/control, cause | Корректный вклад и история |

Гостевую отладочную телеметрию не считать достижениями. Полные damage-события могут быть объёмны: хранить для replay/проверки в сжатых чанках, а обычные query обслуживать по агрегатам. Срок хранения полных логов — отдельная политика; долгоживущие проверенные итоги не исчезают вместе с детальным replay.

## 3. Как считать урон и командный вклад

Основной показатель урона = **реальная потеряHP врага** после защиты, capped егоHP перед событием. Урон щиту показывать отдельно. Overkill и rawDamage можно открыть в деталях, но не добавлять к основному. Регенерация даёт возможность повторно нанести effective damage: это допустимый боевой факт, но не свободные рейтинговые очки.

DoT хранит sourceTower/sourceOwner при наложении; поздний синтез/смена владельца не переписывает уже нанесённый урон. Для текущего остаточногоDoT политика lineage фиксируется ruleset. Исходная башня может исчезнуть, её статистическая запись остаётся. Один удар splash даёт несколько событий по разным врагам, а не двойной учёт одной потериHP.

Support: время замедления/снятия брони/ускорения башен и assisted damage показывать отдельными величинами с описанной методикой. Нельзя складывать «поддержку в процентах» и урон в одну сумму. Килл у последнего удара, assist у contributors; суммарное количество убитых команды не равно сумме kills+assists.

Co-op: результат прежде всего командный. Урон принадлежит владельцу атакующей башни в момент события; дополнительно сохраняется `builtBy`. Составной камень хранит авторов ингредиентов. Золото команды начисляется один раз; в личном экране отдельно «создано действий/нанесено урона/поддержка», а не копии общего дохода, которые потом ошибочно суммируются в 4 раза. Переход управления не меняет автора создания.

## 4. Какие срезы нужны

| Экран | Обязательные показатели |
| --- | --- |
| Во время игры |Волна/жизни/золото; компактная сводка команды, раскрываемая по нажатию: урон/поддержка/потери/готовность. У башни — качество, MVP, DPS текущего окна и суммарный effective damage |
| Итог матча |Место/победа/clear/endless, причина завершения, версии/seed, длительность, очки, золото earned/spent/final, урон, kills/escapes, самые сильные башни, полная постройка и lineage |
| По волнам |Жизни и утечки, заработано/потрачено, damage by type, лучший камень, длительность, изменения лабиринта |
| Личная история |Фильтры режима/сезона/сложности, последние матчи, лучшие результаты, коллекция и прогресс |
| За всё время |Матчи/wins/clears, лучшие волны, золото заработано/потрачено, урон, построенные/созданные минералы по видам/качествам, MVP, время игры |
| Глобально |Отдельные таблицы навыка, рекордов, очков, золота, урона и коллекции с объяснением способа подсчёта |

На телефоне не держать всё открытым. В матче компактный счетчик/иконка → sheet со списком; закрытие возвращает максимум площади полю. Графики в итогах, не поверх пути врагов. Интервальные показатели обновлять 1–2Hz, не перестраивать таблицу каждый render.

Для башен хранить отдельно: `draftedCount`, `keptCount`, `createdByCraftCount`, `upgradedCount`, `peakConcurrent`, `finalCount`, `damage`. РецептA+B→C увеличивает craftedC и расходует ингредиенты, но не стирает исторические keptA/keptB. Повторный swap не считается новой постройкой. Самый высокий грейд не заменяет распределение всех шести.

## 5. Очки и разные виды рейтинга

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

1. **Рекорд кампании/бесконечности:** отдельный ключ(mode,ruleset,map,difficulty,P,season). Сравнение сначала по очищенной волне, затем remainingLives, затем меньшим combatTicks при одинаковом прогрессе. Предел/тайбрейк объяснены публично; время меню/паузы не награждает.
2. **Last Survivor skill:** места и состав соперников, рейтинг до/после и неопределённость. Предложение — отдельная серверная версия модели навыка для многопользовательских мест; подобрать по симуляции, не копировать неизвестную формулуDota. До выбора модели ранняя beta имеет только места и win rate, без выдуманногоMMR.
3. **Очки партии:** предварительная прозрачная формула `100*clearedWaves + 500*campaignClear + sum(weightedKills) - 10*leakDamage`, не ниже 0. Это проектный старт и `scoreVersion`, не оригинальные очки. Endless-очки только в своей таблице; окончательную формулу заморозить перед сезоном. Золото/урон не добавлять прямо к skill.
4. **Накопительные таблицы:** total effective damage, goldEarned, crystalsCrafted, highestGrade, clears. Это статистика активности, не доказательство мастерства. Отдельно показывать лучший один матч/среднее/всего и достаточную выборку.

Кумулятивный endless можно фармить регенерацией и временем; поэтому для глобального total хранить признак rankedVerified и режим, а награды ограничивать правилами сезона. Матчи с отладкой, custom mod, guest tampering, bots или платным усилением не попадают в основной skill-board. Разные размеры co-op не конкурируют в одной таблице.

Разрешённый неизменный seed-челлендж имеет отдельную таблицу. При равенстве всех публичных критериев — одинаковое место, стабильная внутренняя сортировка лишь для пагинации. Нельзя тайно использовать email/возраст аккаунта для выигрыша тайбрейка.

## 6. Хранение и надёжная агрегация

Таблицы: `matches`, `match_players`, `match_boards`, `match_wave_stats`, `match_tower_stats`, `tower_lineage`, `gold_ledger`, `result_events`, `player_aggregate`, `player_gem_aggregate`, `ratings`, `rating_history`, `seasons`, `season_memberships`, `leaderboard_entries`, `outbox`. PK/FK, unique(matchId,resultVersion), unique(eventId), индексы поplayer/mode/season/date; сырой журнал — chunk storage с checksum.

Finalizer в одной транзакции проверяет завершённость, записывает неизменяемый результат+outbox. Projector читает outbox с повторами; unique projectionKey не позволяет удвоить lifetime. Это доставка at-least-once с идемпотентным применением, не магическое «ровно один раз» сети. Агрегаты восстанавливаются из версионированных результатов; итог матча не меняется при переименовании игрока.

Схема статистики не требует храненияemail в матчах. Публичный профиль содержит только разрешённое пользователем имя/id/статистику. Скрытый профиль, удаление/обезличивание и экспорт должны иметь описанную политику; отдельно хранить необходимые финансовые записи. Не публиковать IP/сессионные данные.

API: `GET /me/stats`, `/me/matches?cursor`, `/matches/:id`, `/matches/:id/waves`, `/matches/:id/towers`, `/leaderboards?mode&season&metric&cursor`, `/players/:publicId`. Cursor-пагинация, ограничения range/limit и доступ к чужому приватному матчу. В ответах — dataFreshness, metricDefinitionVersion, counts, notAvailableReason. До агрегации показывать «результат обрабатывается», не нули вместо потерянной статистики.

## 7. Сезоны

`SeasonConfig`: id/name, startAt/endAtUTC, allowedRulesetHashes, modes, mapPool, seedPolicy, modifiers, rankedEntitlementsPolicy, qualification, scoringVersion, rewardTable, status(draft/published/active/frozen/settled/archived).

Матч получаетseasonId на старте. Партия, начатая до закрытия, завершает старые условия в ограниченном grace-window, например 2 часа; более длинный endless сохраняется в lifetime/режимный рекорд, но не бесконечно задерживает награды. Это предложение, которое нужно явно опубликовать до сезона. Новый патч баланса не меняет уже запущенную комнату. Существенный хотфикс открывает новую категорию или прекращает ranked-вход, не перемешивает результаты.

Рейтинговый reset/soft reset, placement-матчи и награды описывать публично. Lifetime-коллекция и купленная косметика сохраняются; сезонные места архивируются с правилами/версией. Новые условия сезона не означают переписать историю старого. Награды — один grant с ключом(season,player,reward), повтор settlement безопасен, отмена имеет аудит.

## 8. Задания и приёмка

S1 — определения метрик и golden fixtures: удар по 1HP на 100damage учитывает 1effective,99overkill; poison/splash/shield/regen проверены. S2 — серверный ledger/lineage: синтез не стирает прошлый вклад, swap не добавляет созданный камень. S3 — live UI и итоги на телефоне. S4 — durable finalizer/outbox,10 повторов одного результата дают один вклад. S5 — lifetime/публичные рейтинги, rebuild из событий равен сохранённым агрегатам. S6 — seasons: матч на границе, sharedPlace, возврат награды, повтор settlement, смена правил. S7 — злоупотребления: подкрученный localStorage, повторresult, необычный урон, сговор/фарм помечаются и не меняют основной рейтинг без серверной валидации.
