squad-proto/src/debug/metrics.cpp

104 lines
3.4 KiB
C++
Raw Normal View History

Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (raylib только окно, ввод и финальный блит). Ядро (спек, раздел 6): - LaneClear: проверка линии огня по своим, стенам и дальности; - решатель огневых позиций с гистерезисом и коммитом; - резервирование линий огня и расступание из чужих секторов; - пули-снаряды, дружественный урон физически возможен. Хоррор-слой (render-only): свет и туман войны с памятью карты, светящиеся трассеры, кровь, виньетка, зерно, напряжение. Ближний бой: сектор удара, три фазы, связка из трёх ударов, своя дисциплина «не бить сквозь своего». Проверка: squad_proto.exe --accept прогоняет критерии приёмки раздела 11 плюс блок M7 по ближнему бою — все PASS, FRIENDLY_HITS = 0 за 3 минуты боя. Сборка: cmake -S . -B build && cmake --build build --config Release raylib 6.0 подтягивается через FetchContent. Документация — CLAUDE.md как оглавление, содержание в docs/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
#include "debug/metrics.h"
void Metrics::LogFriendlyHit(int shooter, int victim, Vec2 pos, bool victimAdvancing)
{
++friendlyHits;
if (hitLogCount < HIT_LOG_MAX)
hitLog[hitLogCount++] = FriendlyHitEvent{clock, shooter, victim, pos, victimAdvancing};
}
void Metrics::Reset()
{
friendlyHits = 0;
hitLogCount = 0;
clock = 0.0f;
targetsKilled = 0;
shotsFired = 0;
meleeSwings = 0;
meleeHits = 0;
meleeAborts = 0;
for (int& v : meleeComboSwings) v = 0;
meleeMaxHitExcessDeg = 0.0f;
reloads = 0;
for (int i = 0; i < NUM_AGENTS; ++i)
{
laneCount[i] = 0;
uptimePct[i] = 0.0f;
for (int k = 0; k < UPTIME_SAMPLES; ++k) ring[i][k] = 0;
}
ringPos = 0;
ringFill = 0;
uptimeAvg = 0.0f;
meleeUptimeAvg = 0.0f;
settleArmed = true; // старт — отряд уже стоит, замеряем первое расхождение
settleClock = 0.0f;
lastSettle = -1.0f;
prevState = AgentState::HOLDING;
simMs = 0.0f;
renderMs = 0.0f;
}
void Metrics::Sample(const Squad& squad, float dt)
{
clock += dt;
// --- FIRE UPTIME (скользящее окно) ---
const int window = std::min(UPTIME_SAMPLES,
std::max(1, int(g_tune.uptimeWindow / FIXED_DT)));
if (ringPos >= window) ringPos = 0;
// «В бою» для стрелка — валидная линия огня, для клинка — цель в зоне удара.
// Считаются они одинаково, а усредняются раздельно: смешивать доли времени
// двух разных занятий в одно число бессмысленно.
int readyNow = 0;
for (int i = 0; i < NUM_AGENTS; ++i)
{
const Agent& a = squad.agents[i];
const bool engaged = a.IsMelee() ? a.melee.inReach : a.hasLane;
const unsigned char v = engaged ? 1u : 0u;
laneCount[i] -= ring[i][ringPos];
ring[i][ringPos] = v;
laneCount[i] += v;
// Для SETTLE TIME клинку достаточно ИМЕТЬ цель: он к ней ещё идёт,
// но отряд уже разобрал обстановку, а именно это метрика и меряет.
if (a.IsMelee() ? (a.melee.targetId >= 0) : a.hasLane) ++readyNow;
}
ringPos = (ringPos + 1) % window;
ringFill = std::min(ringFill + 1, window);
float rangedSum = 0.0f;
float meleeSum = 0.0f;
for (int i = 0; i < NUM_AGENTS; ++i)
{
uptimePct[i] = (ringFill > 0) ? (100.0f * float(laneCount[i]) / float(ringFill)) : 0.0f;
if (squad.agents[i].IsMelee()) meleeSum += uptimePct[i];
else rangedSum += uptimePct[i];
}
const int ranged = squad.RangedCount();
uptimeAvg = (ranged > 0) ? rangedSum / float(ranged) : 0.0f;
meleeUptimeAvg = (squad.meleeCount > 0) ? meleeSum / float(squad.meleeCount) : 0.0f;
// --- SETTLE TIME ---
if (squad.state == AgentState::HOLDING && prevState == AgentState::ADVANCING)
{
settleArmed = true;
settleClock = 0.0f;
}
if (squad.state == AgentState::ADVANCING) settleArmed = false;
if (settleArmed)
{
settleClock += dt;
if (float(readyNow) >= g_tune.settleQuorum)
{
lastSettle = settleClock;
settleArmed = false;
}
}
prevState = squad.state;
}