squad-proto/src/debug/metrics.h
z.kirill 7d6266aaf3 Tile2D: движок отдельным продуктом, игра — проектом в нём (M11-M20)
Десять вех одним состоянием: разделять их пофайлово нечем, потому что
src/game.cpp менялся почти в каждой. Собирается начисто без предупреждений,
все проверки проходят.

ДВИЖОК (engine/, цель tile2d)

  Каталог свой, и решение «это движок» принимается тем, куда положен файл.
  Компилятор нашёл три зависимости движка от игры, и все сняты: размеры кадра
  и карты уехали в engine/config.h, потолок населения этажа стал своим
  (MAX_LEVEL_SPAWNS против игровой MAX_TARGETS), запасная точка старта стала
  центром карты вместо позиции отряда.

  engine/projection.*  единственное место, где движок знает про изометрию;
                       видов сразу два — абстракция с одной реализацией ничем
                       не проверена и обычно оказывается неправильной
  engine/project.*     манифест проекта: пути, вид, кривая глубины
  engine/app.*         окно, кадр, fixed timestep — вместо двух копий цикла
  engine/tilemap_draw  движок научился показывать свою карту

РЕДАКТОР (editor/, цель tile2d_editor)

  Открывает любой проект и ни одной игры по имени не знает.
  --tool  одиннадцать безоконных команд: комнаты, каталог, кривая, сборка
          этажа, ASCII-карта, проверка набора. Правку делают файлами,
          последствия читают командой (docs/19-agent.md)
  терминал снизу: те же инструменты, git, сборка и запросы к claude/codex,
          открытый проект уходит потомкам через TILE2D_PROJECT

ПРИМЕР (samples/walk, цель tile2d_walk)

  Второй потребитель движка. Игра и редактор самостоятельность движка
  проверить не могут: редактор мира не рисует, а игра — то место, куда общий
  код и уезжает незаметно.

ИГРА (src/)

  Экстракшен: вес и два порога переноски, конечный боезапас, расходники
  применяются сами, общая сумка отряда, трата опыта на улучшения.

  Каждое из правил проверяется прогоном, а не глазами, и проверки поймали:
  вес дважды не связывал ничего (мерил по бинтам прогона, а не по добыче);
  боец с клинком не тратил найденные патроны при запасном стволе в сумке;
  массив виджетов экрана переполнился и молча съел хвост схрона; проверка
  улучшений сама ломала забег, вызывая NewRun посреди спуска.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 21:07:19 +03:00

82 lines
4.1 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#pragma once
// -----------------------------------------------------------------------------
// Метрики корректности (спек 7):
// FRIENDLY_HITS — накопительный счётчик попаданий по своим (главная метрика);
// FIRE UPTIME — доля времени с валидной линией за последние 10 сек;
// SETTLE TIME — от перехода в HOLDING до момента, когда >=4 агента в бою.
//
// С появлением ближнего боя два уточнения, иначе цифры врут:
// * FIRE UPTIME усредняется ТОЛЬКО по стрелкам. У бойца с клинком линии огня
// не бывает по определению, и его ноль занижал бы метрику стрельбы;
// * «в бою» (для SETTLE TIME) = стрелок при линии ИЛИ клинок при цели.
// Отдельно считается MELEE UPTIME — доля времени в зоне удара.
// -----------------------------------------------------------------------------
#include "sim/squad.h"
constexpr int UPTIME_SAMPLES = 600; // 10 сек при 60 Гц
// Журнал попаданий по своим: без него FRIENDLY_HITS != 0 нечем объяснить.
struct FriendlyHitEvent
{
float time = 0.0f;
int shooter = -1;
int victim = -1;
Vec2 pos{};
bool victimAdvancing = false;
};
constexpr int HIT_LOG_MAX = 16;
struct Metrics
{
int friendlyHits = 0;
int targetsKilled = 0;
int shotsFired = 0;
// ближний бой
int meleeSwings = 0; // взмахов начато
int meleeHits = 0; // попаданий лезвием
int meleeAborts = 0; // взмахов оборвано из-за своего в дуге
// Сколько взмахов пришлось на каждый удар связки. Если серия не
// прокручивается, здесь будет ноль во всех ячейках кроме первой.
int meleeComboSwings[MELEE_COMBO_STEPS] = {};
// На сколько градусов попадание лезвия вышло за РАЗРЕШЁННЫЙ сектор этого
// взмаха (полусектор + полуширина полосы). Должно быть ~0: если лезвие
// достаёт за спину, здесь вырастет десятки градусов. Ровно этим и выглядел
// баг «бьёт на 360».
float meleeMaxHitExcessDeg = 0.0f;
int reloads = 0; // сколько раз стрелки уходили в перезарядку
// Сколько расходников потрачено за прогон. Ноль при раненом отряде и полных
// сумках означает, что расходники снова стали мёртвым грузом.
int itemsUsed = 0;
FriendlyHitEvent hitLog[HIT_LOG_MAX];
int hitLogCount = 0;
float clock = 0.0f; // время симуляции, для журнала
void LogFriendlyHit(int shooter, int victim, Vec2 pos, bool victimAdvancing);
// FIRE UPTIME: кольцевой буфер + инкрементальные счётчики
unsigned char ring[NUM_AGENTS][UPTIME_SAMPLES] = {};
int ringPos = 0;
int ringFill = 0;
int laneCount[NUM_AGENTS] = {};
float uptimePct[NUM_AGENTS] = {};
float uptimeAvg = 0.0f; // только по стрелкам
float meleeUptimeAvg = 0.0f; // только по клинкам: доля времени в зоне удара
// SETTLE TIME
bool settleArmed = false;
float settleClock = 0.0f;
float lastSettle = -1.0f;
// производительность (заполняется главным циклом)
float simMs = 0.0f;
float renderMs = 0.0f;
AgentState prevState = AgentState::HOLDING;
void Reset();
void Sample(const Squad& squad, float dt);
};