292 lines
12 KiB
C++
292 lines
12 KiB
C++
|
|
#include "sim/squad.h"
|
|||
|
|
|
|||
|
|
#include "ai/lane_registry.h"
|
|||
|
|
|
|||
|
|
namespace
|
|||
|
|
{
|
|||
|
|
|
|||
|
|
// Локальные слоты строя: x = вперёд, y = вправо. Множатся на formationSpacing.
|
|||
|
|
const float kWedge[NUM_AGENTS][2] = {
|
|||
|
|
{ 1.0f, 0.0f},
|
|||
|
|
{ 0.0f, -0.9f},
|
|||
|
|
{ 0.0f, 0.9f},
|
|||
|
|
{-1.0f, -1.8f},
|
|||
|
|
{-1.0f, 1.8f},
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const float kLine[NUM_AGENTS][2] = {
|
|||
|
|
{0.0f, -2.0f},
|
|||
|
|
{0.0f, -1.0f},
|
|||
|
|
{0.0f, 0.0f},
|
|||
|
|
{0.0f, 1.0f},
|
|||
|
|
{0.0f, 2.0f},
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
} // namespace
|
|||
|
|
|
|||
|
|
const char* FormationName(Formation f)
|
|||
|
|
{
|
|||
|
|
switch (f)
|
|||
|
|
{
|
|||
|
|
case Formation::WEDGE: return "WEDGE";
|
|||
|
|
case Formation::LINE: return "LINE";
|
|||
|
|
case Formation::RING: return "RING";
|
|||
|
|
default: return "?";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Squad::Init(ecs::World& world, Vec2 at)
|
|||
|
|
{
|
|||
|
|
anchor = at;
|
|||
|
|
anchorPrev = at;
|
|||
|
|
anchorVel = {};
|
|||
|
|
forward = {1.0f, 0.0f};
|
|||
|
|
state = AgentState::HOLDING;
|
|||
|
|
idleTimer = g_tune.holdDelay;
|
|||
|
|
holdElapsed = 0.0f;
|
|||
|
|
urgency = 0.0f;
|
|||
|
|
sprinting = false;
|
|||
|
|
formSpacing = 1.0f;
|
|||
|
|
formStretch = 1.0f;
|
|||
|
|
|
|||
|
|
meleeCount = std::min(std::max(meleeCount, 0), NUM_AGENTS);
|
|||
|
|
pistolCount = std::min(std::max(pistolCount, 0), NUM_AGENTS - meleeCount);
|
|||
|
|
|
|||
|
|
// Сначала создаём все сущности бойцов, и только потом берём указатель на
|
|||
|
|
// плотный массив пула: до конца заполнения он может переехать.
|
|||
|
|
ecs::Entity ids[NUM_AGENTS];
|
|||
|
|
for (int i = 0; i < NUM_AGENTS; ++i)
|
|||
|
|
{
|
|||
|
|
ids[i] = world.Create();
|
|||
|
|
world.Add<Agent>(ids[i], Agent{});
|
|||
|
|
}
|
|||
|
|
agents.p = world.All<Agent>().data();
|
|||
|
|
agents.n = NUM_AGENTS;
|
|||
|
|
|
|||
|
|
for (int i = 0; i < NUM_AGENTS; ++i)
|
|||
|
|
{
|
|||
|
|
Agent& a = agents[i];
|
|||
|
|
a = Agent{};
|
|||
|
|
a.self = ids[i];
|
|||
|
|
a.slotIndex = i;
|
|||
|
|
|
|||
|
|
// Клинки занимают первые слоты: в клине это остриё и левое крыло,
|
|||
|
|
// то есть ровно те, кто и так упирается в противника первым.
|
|||
|
|
// Пистолеты — последние слоты: короткий ствол уезжает в хвост клина.
|
|||
|
|
const bool isMelee = (i < meleeCount);
|
|||
|
|
const bool isPistol = !isMelee && (i >= NUM_AGENTS - pistolCount);
|
|||
|
|
|
|||
|
|
a.cls = isMelee ? WeaponClass::MELEE : WeaponClass::RANGED;
|
|||
|
|
a.gunId = isPistol ? FirearmId::PISTOL : FirearmId::RIFLE;
|
|||
|
|
a.weaponEntity = SpawnWeapon(world, a.cls, a.gunId);
|
|||
|
|
|
|||
|
|
a.melee.sweepSign = (i & 1) ? -1.0f : 1.0f;
|
|||
|
|
a.pos = SlotWorld(i);
|
|||
|
|
a.prevPos = a.pos;
|
|||
|
|
a.postPos = a.pos;
|
|||
|
|
a.facing = 0.0f;
|
|||
|
|
a.state = state;
|
|||
|
|
a.currentTargetId = -1;
|
|||
|
|
a.commitTimer = 0.0f;
|
|||
|
|
// Фазы решателя разведены: за кадр решает максимум один агент (6.5).
|
|||
|
|
a.solveTimer = g_tune.solveInterval * (float(i) / float(NUM_AGENTS));
|
|||
|
|
|
|||
|
|
SyncLoadout(world, a);
|
|||
|
|
a.gun.ammo = a.gun.def.magazine; // выходим с полным магазином
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Vec2 Squad::SlotLocal(int i) const
|
|||
|
|
{
|
|||
|
|
// Интервал и вытяжение — это ТЕКУЩЕЕ состояние строя, а не константа:
|
|||
|
|
// на стоянке отряд собирается, в беге растягивается вдоль движения.
|
|||
|
|
const float s = g_tune.formationSpacing * formSpacing;
|
|||
|
|
const float fwd = s * formStretch;
|
|||
|
|
|
|||
|
|
switch (formation)
|
|||
|
|
{
|
|||
|
|
case Formation::LINE:
|
|||
|
|
return {kLine[i][0] * fwd, kLine[i][1] * s};
|
|||
|
|
case Formation::RING:
|
|||
|
|
{
|
|||
|
|
const float ang = float(i) * (TWO_PI_F / float(NUM_AGENTS));
|
|||
|
|
const Vec2 d = FromAngle(ang) * 1.5f;
|
|||
|
|
return {d.x * fwd, d.y * s};
|
|||
|
|
}
|
|||
|
|
case Formation::WEDGE:
|
|||
|
|
default:
|
|||
|
|
return {kWedge[i][0] * fwd, kWedge[i][1] * s};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Vec2 Squad::SlotWorld(int i) const
|
|||
|
|
{
|
|||
|
|
const Vec2 local = SlotLocal(i);
|
|||
|
|
const Vec2 right = Perp(forward);
|
|||
|
|
return anchor + forward * local.x + right * local.y;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Vec2 Squad::GoalFor(int i) const
|
|||
|
|
{
|
|||
|
|
// Боец ближнего боя ведёт себя одинаково в обоих состояниях: пока цель
|
|||
|
|
// внутри его круга, он идёт к ней, а не в слот. Иначе клинок оказывался бы
|
|||
|
|
// полезен только на стоянке — а он нужен как раз в движении.
|
|||
|
|
// Но в спринте цель отменяется: бежим, а не деремся.
|
|||
|
|
if (agents[i].IsMelee() && !sprinting) return agents[i].postPos;
|
|||
|
|
if (agents[i].IsMelee()) return SlotWorld(i);
|
|||
|
|
|
|||
|
|
// В движении — слот строя, на месте — выбранная решателем огневая позиция.
|
|||
|
|
return (state == AgentState::ADVANCING) ? SlotWorld(i) : agents[i].postPos;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Vec2 Squad::Centroid() const
|
|||
|
|
{
|
|||
|
|
Vec2 c{};
|
|||
|
|
for (int i = 0; i < NUM_AGENTS; ++i) c += agents[i].pos;
|
|||
|
|
return c / float(NUM_AGENTS);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Squad::UpdateAnchorAndState(Vec2 moveWorldDir, bool sprint, const Tilemap& map, float dt)
|
|||
|
|
{
|
|||
|
|
anchorPrev = anchor;
|
|||
|
|
|
|||
|
|
const bool moving = LengthSq(moveWorldDir) > 1e-6f;
|
|||
|
|
const AgentState prevState = state;
|
|||
|
|
const Vec2 c = Centroid();
|
|||
|
|
|
|||
|
|
// --- натяжение поводка ----------------------------------------------------
|
|||
|
|
// Поводок УПРУГИЙ, а не жёсткий. За мягкой границей курсор продолжает
|
|||
|
|
// уходить, но идёт всё тяжелее, и до жёсткого предела не доходит никогда.
|
|||
|
|
//
|
|||
|
|
// Жёсткий поводок здесь не годился принципиально: он мгновенно упирается,
|
|||
|
|
// дистанция намертво встаёт в одно значение, и по ней уже нельзя отличить
|
|||
|
|
// «подправил направление» от «тяну отряд вперёд изо всех сил». Упругий
|
|||
|
|
// поводок превращает дистанцию в непрерывную величину: чем сильнее тянешь,
|
|||
|
|
// тем дальше уезжает курсор и тем быстрее бегут за ним.
|
|||
|
|
const float soft = std::max(0.2f, g_tune.anchorLeash);
|
|||
|
|
const float hard = std::max(soft + 0.2f, g_tune.anchorLeashMax);
|
|||
|
|
const float start = soft * Clampf(g_tune.urgeStartFrac, 0.0f, 0.95f);
|
|||
|
|
const float dist = Distance(anchor, c);
|
|||
|
|
|
|||
|
|
float pull = 0.0f;
|
|||
|
|
if (moving) pull = Clampf((dist - start) / (hard - start), 0.0f, 1.0f);
|
|||
|
|
if (moving && sprint) pull = 1.0f; // Shift — приказ бежать, без уговоров
|
|||
|
|
|
|||
|
|
const float rate = (pull > urgency) ? g_tune.urgeAttack : g_tune.urgeRelease;
|
|||
|
|
urgency += (pull - urgency) * (1.0f - std::exp(-std::max(0.01f, rate) * dt));
|
|||
|
|
urgency = Clampf(urgency, 0.0f, 1.0f);
|
|||
|
|
// Бег без стрельбы — либо по прямому приказу, либо когда курсор реально
|
|||
|
|
// уехал: отряд догоняет и ему не до целей.
|
|||
|
|
sprinting = (moving && sprint) || (urgency >= g_tune.sprintAt);
|
|||
|
|
|
|||
|
|
// --- сам курсор -----------------------------------------------------------
|
|||
|
|
// У него есть разгон и торможение: мгновенная смена скорости — это и есть
|
|||
|
|
// «топорно». Отпустил клавиши — курсор доезжает, а не втыкается в грунт.
|
|||
|
|
const float over = Clampf((dist - soft) / (hard - soft), 0.0f, 1.0f);
|
|||
|
|
const float dragK = std::max(0.08f, 1.0f - Clampf(g_tune.leashDrag, 0.0f, 1.0f) * over);
|
|||
|
|
const float speed = (sprint ? g_tune.anchorSpeedFast : g_tune.anchorSpeed) * dragK;
|
|||
|
|
|
|||
|
|
const Vec2 want = moving ? Normalized(moveWorldDir, forward) * speed : Vec2{};
|
|||
|
|
const Vec2 dv = want - anchorVel;
|
|||
|
|
anchorVel += ClampLength(dv, std::max(0.1f, g_tune.anchorAccel) * dt);
|
|||
|
|
anchor += anchorVel * dt;
|
|||
|
|
|
|||
|
|
if (moving) forward = Normalized(moveWorldDir, forward);
|
|||
|
|
|
|||
|
|
// Жёсткий предел. Сопротивление обычно не даёт до него дойти, но отряд может
|
|||
|
|
// и застрять в стене — тогда курсор без предела уехал бы за полкарты.
|
|||
|
|
const Vec2 d = anchor - c;
|
|||
|
|
const float len = Length(d);
|
|||
|
|
if (len > hard && len > 1e-5f)
|
|||
|
|
{
|
|||
|
|
const Vec2 n = d / len;
|
|||
|
|
anchor = c + n * hard;
|
|||
|
|
const float vn = Dot(anchorVel, n);
|
|||
|
|
if (vn > 0.0f) anchorVel -= n * vn;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
map.ResolveCircle(anchor, g_tune.anchorRadius);
|
|||
|
|
|
|||
|
|
// --- состояние отряда ------------------------------------------------------
|
|||
|
|
if (moving)
|
|||
|
|
{
|
|||
|
|
idleTimer = 0.0f;
|
|||
|
|
state = AgentState::ADVANCING;
|
|||
|
|
holdElapsed = 0.0f;
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
idleTimer += dt;
|
|||
|
|
if (idleTimer >= g_tune.holdDelay) state = AgentState::HOLDING;
|
|||
|
|
if (state == AgentState::HOLDING) holdElapsed += dt;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- дыхание строя ---------------------------------------------------------
|
|||
|
|
const float wantSpacing = (state == AgentState::HOLDING)
|
|||
|
|
? g_tune.holdSpacing
|
|||
|
|
: Lerpf(1.0f, g_tune.sprintSpacing, urgency);
|
|||
|
|
const float wantStretch = Lerpf(1.0f, g_tune.sprintStretch, urgency);
|
|||
|
|
const float k = 1.0f - std::exp(-std::max(0.01f, g_tune.formMorphRate) * dt);
|
|||
|
|
formSpacing += (wantSpacing - formSpacing) * k;
|
|||
|
|
formStretch += (wantStretch - formStretch) * k;
|
|||
|
|
|
|||
|
|
if (state != prevState)
|
|||
|
|
{
|
|||
|
|
for (int i = 0; i < NUM_AGENTS; ++i)
|
|||
|
|
{
|
|||
|
|
Agent& a = agents[i];
|
|||
|
|
a.commitTimer = 0.0f;
|
|||
|
|
// Встали — остаёмся на месте, пока решатель не найдёт лучше (6.5).
|
|||
|
|
if (state == AgentState::HOLDING) a.postPos = a.pos;
|
|||
|
|
}
|
|||
|
|
if (state == AgentState::HOLDING) holdElapsed = 0.0f;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (int i = 0; i < NUM_AGENTS; ++i) agents[i].state = state;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Squad::UpdateMovement(const Tilemap& map, const LaneRegistry* lanes,
|
|||
|
|
const std::vector<Target>& targets, float dt)
|
|||
|
|
{
|
|||
|
|
// Скорость — не тумблер «шаг/рывок», а непрерывная величина от натяжения.
|
|||
|
|
const float baseSpeed = MoveSpeed();
|
|||
|
|
|
|||
|
|
for (int i = 0; i < NUM_AGENTS; ++i)
|
|||
|
|
{
|
|||
|
|
Agent& a = agents[i];
|
|||
|
|
a.prevPos = a.pos;
|
|||
|
|
|
|||
|
|
const Vec2 goal = GoalFor(i);
|
|||
|
|
const bool lunging = a.IsMelee() && a.melee.state == MeleeState::STRIKE;
|
|||
|
|
|
|||
|
|
// --- рывок клинка --------------------------------------------------
|
|||
|
|
// В активном окне удара ось уже зафиксирована, и боец едет по ней.
|
|||
|
|
// Seek/separation/расступание тут молчат: удар — это обязательство,
|
|||
|
|
// а не предложение. Стены остаются: сквозь них не рвётся никто.
|
|||
|
|
if (lunging)
|
|||
|
|
{
|
|||
|
|
const float speed = g_tune.meleeLungeSpeed;
|
|||
|
|
const Vec2 dir = FromAngle(a.melee.dir);
|
|||
|
|
Vec2 desired = dir * speed;
|
|||
|
|
desired += WallAvoidForce(a, map, dir, speed) * g_tune.wWallAvoid;
|
|||
|
|
IntegrateAgent(a, desired, speed, dt, map, g_tune.meleeLungeAccel);
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- обычные слои, порядок 6.7 --------------------------------------
|
|||
|
|
const float maxSpeed = baseSpeed * a.SpeedScale();
|
|||
|
|
|
|||
|
|
Vec2 desired = SeekForce(a, goal, maxSpeed) * g_tune.wSeek;
|
|||
|
|
desired += SeparationForce(a, agents.data(), NUM_AGENTS, maxSpeed) * g_tune.wSeparation;
|
|||
|
|
// Замах — это обязательство. Выдёргивать бойца из чужой линии огня
|
|||
|
|
// посреди него бессмысленно: стрелок всё равно не выстрелит (правило
|
|||
|
|
// 6.3 запрещает), а удар сорвётся. Расступаться боец будет до и после.
|
|||
|
|
if (lanes && !(a.IsMelee() && a.melee.Committed()))
|
|||
|
|
desired += lanes->AvoidanceForce(i, a.pos, a.bodyRadius, maxSpeed) * g_tune.wLaneAvoid;
|
|||
|
|
desired += TargetPushForce(a, targets, maxSpeed) * g_tune.wTargetPush;
|
|||
|
|
desired += WallAvoidForce(a, map, Normalized(goal - a.pos), maxSpeed) * g_tune.wWallAvoid;
|
|||
|
|
|
|||
|
|
IntegrateAgent(a, desired, maxSpeed, dt, map);
|
|||
|
|
}
|
|||
|
|
}
|