squad-proto/src/ai/lane_registry.cpp

56 lines
2.3 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 "ai/lane_registry.h"
void LaneRegistry::Add(int shooter, Vec2 from, Vec2 to, float reserve)
{
if (count >= NUM_AGENTS) return;
lanes[count++] = FireLane{from, to, shooter, reserve};
}
Vec2 LaneRegistry::AvoidanceForce(int agentIndex, Vec2 pos, float bodyRadius, float maxSpeed) const
{
// Резерв шире, чем «тело уже в линии»: агент должен отходить ЗАРАНЕЕ,
// иначе он успевает влезть под пулю, которая уже летит.
const float wide = BlockRadius(bodyRadius) * g_tune.laneReserveScale;
Vec2 force{};
for (int i = 0; i < count; ++i)
{
const FireLane& lane = lanes[i];
if (lane.shooter == agentIndex) continue;
const float threshold = (lane.reserve > 0.0f) ? (bodyRadius + lane.reserve) : wide;
const float d = DistancePointToSegment(pos, lane.from, lane.to);
if (d >= threshold) continue;
const Vec2 axis = Normalized(lane.to - lane.from);
if (LengthSq(axis) < 0.5f) continue;
// В сторону ближайшего края линии.
const Vec2 n = Perp(axis);
const float side = Dot(pos - lane.from, n);
Vec2 away = (side >= 0.0f) ? n : Vec2{-n.x, -n.y};
if (std::fabs(side) < 1e-4f) // ровно на оси — разводим детерминированно
away = (agentIndex & 1) ? n : Vec2{-n.x, -n.y};
// Чем ближе к оси, тем сильнее; laneAvoidStrength делает отклик резче.
const float strength = Clampf((1.0f - d / threshold) * g_tune.laneAvoidStrength, 0.0f, 1.0f);
force += away * (maxSpeed * strength);
}
return force;
}
int LaneRegistry::CountBlocked(int exceptShooter, Vec2 pos, float bodyRadius,
bool shootingOnly) const
{
const float narrow = BlockRadius(bodyRadius);
int n = 0;
for (int i = 0; i < count; ++i)
{
if (lanes[i].shooter == exceptShooter) continue;
if (shootingOnly && lanes[i].reserve > 0.0f) continue; // это сектор клинка
const float threshold = (lanes[i].reserve > 0.0f) ? (bodyRadius + lanes[i].reserve) : narrow;
if (DistancePointToSegment(pos, lanes[i].from, lanes[i].to) < threshold) ++n;
}
return n;
}