Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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>
104 lines
3.4 KiB
C++
104 lines
3.4 KiB
C++
#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;
|
||
}
|