squad-proto/src/sim/target.cpp

97 lines
3.7 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 "sim/target.h"
namespace
{
// Кучки: 5 на арене (рядом со стартом), 3 в северной комнате,
// 3 в западной, 1 одиночная в восточной. Всего 12.
//
// Северная кучка стоит НАПРОТИВ прохода x=30..31: из коридора до неё есть
// чистая линия. Это тот самый тест-кейс из спека — в узком проходе правило
// «не стрелять сквозь своих» упирается в геометрию, и отряд обязан вытянуться
// в колонну: стреляет передний, задние молчат.
//
// Кучка обязана быть В ПРЕДЕЛАХ ДАЛЬНОСТИ от середины прохода (y≈14), иначе
// стрелять из коридора не по чему и тест-кейс проверяет пустоту. При weaponRange
// 7.2 ближняя мишень стоит в ~6.5 юнита: запас есть, но небольшой — если менять
// дальность, эти координаты надо пересчитывать вместе с ней.
const float kTargetSpots[NUM_TARGETS][2] = {
{30.5f, 20.5f}, {31.5f, 22.5f}, {29.5f, 23.5f}, {32.5f, 21.5f}, {30.5f, 24.5f},
{29.5f, 8.5f}, {31.5f, 7.5f}, {27.5f, 10.5f},
{ 7.5f, 40.5f}, { 9.5f, 42.5f}, { 6.5f, 43.5f},
{43.5f, 35.5f},
};
// Сопротивление отбросу по слотам. Не случайное: в каждой кучке есть кто-то
// тяжёлый, кого клинком не сдвинуть, и кто-то лёгкий, кто улетает от первого
// же удара. Иначе «отброс» и «сопротивление» — два числа, которых не видно.
const float kTargetKnockResist[NUM_TARGETS] = {
0.00f, 0.65f, 0.15f, 0.40f, 0.00f,
0.55f, 0.10f, 0.30f,
0.20f, 0.70f, 0.05f,
0.85f,
};
} // namespace
void SpawnTargets(ecs::World& world)
{
for (int i = 0; i < NUM_TARGETS; ++i)
{
Target t;
t.self = world.Create();
t.pos = {kTargetSpots[i][0], kTargetSpots[i][1]};
t.home = t.pos;
t.vel = {};
t.hp = g_tune.targetHP;
t.maxHp = g_tune.targetHP;
t.radius = g_tune.targetRadius;
t.respawnTimer = 0.0f;
t.alive = true;
t.knockResist = kTargetKnockResist[i];
world.Add<Target>(t.self, t);
}
}
void KnockTarget(Target& t, Vec2 dir, float speed)
{
t.vel += dir * (speed * t.KnockScale());
}
void UpdateTargets(std::vector<Target>& targets, float dt)
{
// Отдача гасится и мишень возвращается на своё место: расстановка уровня
// захардкожена, и «уползание» мишеней ломало бы все замеры разом.
const float damp = std::exp(-g_tune.targetKnockDamp * dt);
const float pull = 1.0f - std::exp(-g_tune.targetReturnRate * dt);
for (Target& t : targets)
{
t.radius = g_tune.targetRadius;
t.maxHp = g_tune.targetHP;
t.pos += t.vel * dt;
t.vel *= damp;
t.pos += (t.home - t.pos) * pull;
if (t.alive) continue;
t.respawnTimer -= dt;
if (t.respawnTimer <= 0.0f)
{
t.alive = true;
t.hp = g_tune.targetHP;
t.pos = t.home;
t.vel = {};
}
}
}
int CountAliveTargets(const std::vector<Target>& targets)
{
int n = 0;
for (const Target& t : targets)
if (t.alive) ++n;
return n;
}