343 lines
14 KiB
C++
343 lines
14 KiB
C++
|
|
#include "ai/firing_solver.h"
|
|||
|
|
|
|||
|
|
namespace
|
|||
|
|
{
|
|||
|
|
|
|||
|
|
constexpr float BIG_F = 1e18f;
|
|||
|
|
|
|||
|
|
// Дуло: muzzleOffset юнита от точки from в сторону цели.
|
|||
|
|
inline Vec2 MuzzleAt(const Agent& shooter, Vec2 from, Vec2 targetPos)
|
|||
|
|
{
|
|||
|
|
return from + Normalized(targetPos - from) * shooter.gun.def.muzzleOffset;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Полураствор конуса выстрела, выраженный как тангенс: боковое отклонение
|
|||
|
|
// пули на расстоянии L от дула равно tan * L.
|
|||
|
|
inline float SpreadTan(const Agent& shooter)
|
|||
|
|
{
|
|||
|
|
return std::tan(std::min(shooter.SpreadRad(), 1.2f));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Свободен ли конус «дуло -> цель» от своих.
|
|||
|
|
//
|
|||
|
|
// Раньше проверялся отрезок: пуля летела ровно по нему, и этого хватало.
|
|||
|
|
// Теперь у выстрела есть разброс, и проверять надо ровно ту область, куда
|
|||
|
|
// пуля может уйти, — конус. Ослабления правила 6.3 тут нет, наоборот: зона
|
|||
|
|
// проверки стала шире отрезка ровно настолько, насколько шире стал выстрел.
|
|||
|
|
// Расширение растёт вдоль линии (у дула конус нулевой), поэтому союзник,
|
|||
|
|
// стоящий вплотную к стрелку сбоку, лишнего запрета не создаёт.
|
|||
|
|
bool ConeClearOfAllies(const Agent& shooter, Vec2 muzzle, int shooterIndex, Vec2 targetPos,
|
|||
|
|
const SolverContext& ctx)
|
|||
|
|
{
|
|||
|
|
const float half = shooter.gun.def.laneHalfWidth + g_tune.safetyMargin;
|
|||
|
|
const float segLen = Distance(muzzle, targetPos);
|
|||
|
|
const float k = SpreadTan(shooter) * segLen;
|
|||
|
|
|
|||
|
|
for (int i = 0; i < ctx.agentCount; ++i)
|
|||
|
|
{
|
|||
|
|
if (i == shooterIndex) continue;
|
|||
|
|
const Agent& ally = ctx.agents[i];
|
|||
|
|
float t = 0.0f;
|
|||
|
|
const float d = DistancePointToSegment(ally.pos, muzzle, targetPos, &t);
|
|||
|
|
if (d < ally.bodyRadius + half + k * t) return false;
|
|||
|
|
}
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Почему линии нет: свой или стена (для F2-оверлея).
|
|||
|
|
void ClassifyBlock(Vec2 from, int shooterIndex, Vec2 targetPos, const SolverContext& ctx,
|
|||
|
|
bool& byAlly, bool& byWall)
|
|||
|
|
{
|
|||
|
|
const Agent& shooter = ctx.agents[shooterIndex];
|
|||
|
|
const Vec2 muzzle = MuzzleAt(shooter, from, targetPos);
|
|||
|
|
|
|||
|
|
byAlly = !ConeClearOfAllies(shooter, muzzle, shooterIndex, targetPos, ctx);
|
|||
|
|
byWall = ctx.map->RaycastBlocked(muzzle, targetPos);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Куда союзник МОЖЕТ уехать за время t. Обычно это линейная экстраполяция
|
|||
|
|
// скорости, но боец ближнего боя на замахе стоит почти неподвижно и через
|
|||
|
|
// мгновение выстреливает рывком по УЖЕ ИЗВЕСТНОЙ оси удара — скорость об этом
|
|||
|
|
// ещё ничего не говорит. Поэтому для него проверяются обе точки: где он есть
|
|||
|
|
// и куда он прыгнет. Иначе выстрел «мимо стоящего» находит его в полёте.
|
|||
|
|
int PredictAllyPositions(const Agent& ally, float t, Vec2 out[2])
|
|||
|
|
{
|
|||
|
|
out[0] = ally.pos + ally.vel * t;
|
|||
|
|
if (!ally.IsMelee() || !ally.melee.Committed()) return 1;
|
|||
|
|
|
|||
|
|
out[1] = ally.pos + FromAngle(ally.melee.dir) * (g_tune.meleeLungeSpeed * t);
|
|||
|
|
return 2;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} // namespace
|
|||
|
|
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
// 6.3 LaneClear
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
bool LaneClearFrom(Vec2 from, int shooterIndex, Vec2 targetPos, const SolverContext& ctx)
|
|||
|
|
{
|
|||
|
|
const Agent& shooter = ctx.agents[shooterIndex];
|
|||
|
|
const Vec2 muzzle = MuzzleAt(shooter, from, targetPos);
|
|||
|
|
|
|||
|
|
// 1. свои — по конусу разброса, а не по осевому отрезку
|
|||
|
|
if (!ConeClearOfAllies(shooter, muzzle, shooterIndex, targetPos, ctx)) return false;
|
|||
|
|
|
|||
|
|
// 2. геометрия
|
|||
|
|
if (ctx.map->RaycastBlocked(muzzle, targetPos)) return false;
|
|||
|
|
|
|||
|
|
// 3. дистанция — своя у каждого ствола
|
|||
|
|
if (Distance(muzzle, targetPos) > shooter.gun.def.range) return false;
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bool AllyInterceptsShot(Vec2 muzzle, Vec2 targetPos, int shooterIndex, const SolverContext& ctx)
|
|||
|
|
{
|
|||
|
|
const Agent& shooter = ctx.agents[shooterIndex];
|
|||
|
|
const Vec2 dir = Normalized(targetPos - muzzle);
|
|||
|
|
const float speed = std::max(0.001f, shooter.gun.def.bulletSpeed);
|
|||
|
|
const float tImpact = Distance(muzzle, targetPos) / speed;
|
|||
|
|
// Пуля летит внутри конуса, поэтому её «толщина» растёт с пройденным путём.
|
|||
|
|
const float spreadK = SpreadTan(shooter);
|
|||
|
|
|
|||
|
|
// Идём по времени полёта: где будет пуля и где будет союзник.
|
|||
|
|
// Радиус безопасности растёт со временем — союзник может и повернуть,
|
|||
|
|
// а линейная экстраполяция скорости это не ловит.
|
|||
|
|
constexpr int SAMPLES = 6;
|
|||
|
|
for (int k = 0; k <= SAMPLES; ++k)
|
|||
|
|
{
|
|||
|
|
const float t = tImpact * (float(k) / float(SAMPLES));
|
|||
|
|
const Vec2 bulletPos = muzzle + dir * (speed * t);
|
|||
|
|
|
|||
|
|
for (int i = 0; i < ctx.agentCount; ++i)
|
|||
|
|
{
|
|||
|
|
if (i == shooterIndex) continue;
|
|||
|
|
const Agent& ally = ctx.agents[i];
|
|||
|
|
const float r = ally.bodyRadius + shooter.gun.def.bulletRadius + g_tune.safetyMargin +
|
|||
|
|
g_tune.shotPredictSlack * MaxAgentSpeed() * t +
|
|||
|
|
spreadK * speed * t;
|
|||
|
|
|
|||
|
|
Vec2 predicted[2];
|
|||
|
|
const int n = PredictAllyPositions(ally, t, predicted);
|
|||
|
|
for (int q = 0; q < n; ++q)
|
|||
|
|
if (DistanceSq(bulletPos, predicted[q]) < r * r) return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
int NearestAliveTarget(Vec2 from, const SolverContext& ctx, float searchRadius)
|
|||
|
|
{
|
|||
|
|
const std::vector<Target>& targets = *ctx.targets;
|
|||
|
|
int best = -1;
|
|||
|
|
float bestD = searchRadius;
|
|||
|
|
|
|||
|
|
for (int i = 0; i < int(targets.size()); ++i)
|
|||
|
|
{
|
|||
|
|
if (!targets[i].alive) continue;
|
|||
|
|
const float d = Distance(from, targets[i].pos);
|
|||
|
|
if (d < bestD)
|
|||
|
|
{
|
|||
|
|
bestD = d;
|
|||
|
|
best = i;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return best;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
// Выбор цели: 6.4 (ближайшая с чистой линией) + 6.8 (приоритет назначенной).
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
void EvaluateAgentLane(Agent& a, int index, const SolverContext& ctx)
|
|||
|
|
{
|
|||
|
|
const std::vector<Target>& targets = *ctx.targets;
|
|||
|
|
|
|||
|
|
a.hasLane = false;
|
|||
|
|
a.priorityLane = false;
|
|||
|
|
a.laneIntent = false;
|
|||
|
|
a.currentTargetId = -1;
|
|||
|
|
a.targetBlockedByAlly = false;
|
|||
|
|
a.targetBlockedByWall = false;
|
|||
|
|
|
|||
|
|
// --- ручной режим: сначала назначенная цель ---
|
|||
|
|
if (ctx.assignedTargetId >= 0 && ctx.assignedTargetId < int(targets.size()) &&
|
|||
|
|
targets[ctx.assignedTargetId].alive)
|
|||
|
|
{
|
|||
|
|
const Vec2 tp = targets[ctx.assignedTargetId].pos;
|
|||
|
|
a.laneEnd = tp;
|
|||
|
|
|
|||
|
|
if (LaneClearFrom(a.pos, index, tp, ctx))
|
|||
|
|
{
|
|||
|
|
a.currentTargetId = ctx.assignedTargetId;
|
|||
|
|
a.hasLane = true;
|
|||
|
|
a.priorityLane = true;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
ClassifyBlock(a.pos, index, tp, ctx, a.targetBlockedByAlly, a.targetBlockedByWall);
|
|||
|
|
a.laneIntent = a.targetBlockedByAlly && !a.targetBlockedByWall &&
|
|||
|
|
Distance(a.pos, tp) <= a.gun.def.range + a.gun.def.muzzleOffset;
|
|||
|
|
|
|||
|
|
// Вторичные цели — только в движении и только при включённом флаге (6.8).
|
|||
|
|
if (!(a.state == AgentState::ADVANCING && g_tune.freeFireSecondary)) return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- авто: ближайшая цель, до которой линия чиста ---
|
|||
|
|
int best = -1;
|
|||
|
|
float bestD = BIG_F;
|
|||
|
|
for (int i = 0; i < int(targets.size()); ++i)
|
|||
|
|
{
|
|||
|
|
if (!targets[i].alive) continue;
|
|||
|
|
const float d = Distance(a.pos, targets[i].pos);
|
|||
|
|
if (d >= bestD || d > a.gun.def.range + a.gun.def.muzzleOffset) continue;
|
|||
|
|
if (!LaneClearFrom(a.pos, index, targets[i].pos, ctx)) continue;
|
|||
|
|
best = i;
|
|||
|
|
bestD = d;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (best >= 0)
|
|||
|
|
{
|
|||
|
|
a.currentTargetId = best;
|
|||
|
|
a.hasLane = true;
|
|||
|
|
a.priorityLane = (ctx.assignedTargetId < 0);
|
|||
|
|
a.laneEnd = targets[best].pos;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Линии нет — для оверлея показываем ближайшую цель и причину.
|
|||
|
|
if (ctx.assignedTargetId < 0)
|
|||
|
|
{
|
|||
|
|
const int nearest = NearestAliveTarget(a.pos, ctx, a.gun.def.range);
|
|||
|
|
if (nearest >= 0)
|
|||
|
|
{
|
|||
|
|
a.laneEnd = targets[nearest].pos;
|
|||
|
|
ClassifyBlock(a.pos, index, targets[nearest].pos, ctx,
|
|||
|
|
a.targetBlockedByAlly, a.targetBlockedByWall);
|
|||
|
|
// Линию перекрыл только свой -> намерение стрелять остаётся, и линия
|
|||
|
|
// всё равно резервируется (6.6), иначе перекрывшего ничто не сдвинет.
|
|||
|
|
a.laneIntent = a.targetBlockedByAlly && !a.targetBlockedByWall;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
// 6.5 Оценка кандидата
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
float ScoreCandidate(Vec2 cand, const Agent& a, int index, const SolverContext& ctx,
|
|||
|
|
int bestTargetId)
|
|||
|
|
{
|
|||
|
|
const std::vector<Target>& targets = *ctx.targets;
|
|||
|
|
float score = 0.0f;
|
|||
|
|
|
|||
|
|
if (bestTargetId >= 0 && targets[bestTargetId].alive &&
|
|||
|
|
LaneClearFrom(cand, index, targets[bestTargetId].pos, ctx))
|
|||
|
|
score += g_tune.sLaneClear;
|
|||
|
|
|
|||
|
|
int shootable = 0;
|
|||
|
|
int total = 0;
|
|||
|
|
float nearestDist = BIG_F;
|
|||
|
|
for (int i = 0; i < int(targets.size()); ++i)
|
|||
|
|
{
|
|||
|
|
if (!targets[i].alive) continue;
|
|||
|
|
++total;
|
|||
|
|
nearestDist = std::min(nearestDist, Distance(cand, targets[i].pos));
|
|||
|
|
if (LaneClearFrom(cand, index, targets[i].pos, ctx)) ++shootable;
|
|||
|
|
}
|
|||
|
|
if (total > 0) score += g_tune.sCoverage * float(shootable) / float(total);
|
|||
|
|
|
|||
|
|
if (ctx.lanes)
|
|||
|
|
score -= g_tune.sBlockAlly * float(ctx.lanes->CountBlocked(index, cand, a.bodyRadius));
|
|||
|
|
|
|||
|
|
score -= g_tune.sDistCost * Distance(cand, a.pos);
|
|||
|
|
score -= g_tune.sCohesionCost *
|
|||
|
|
std::max(0.0f, Distance(cand, ctx.anchor) - g_tune.cohesionRadius);
|
|||
|
|
|
|||
|
|
if (nearestDist < g_tune.minEngageDist) score -= g_tune.sTooClose;
|
|||
|
|
|
|||
|
|
return score;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
// 6.5 Решатель: 3 кольца x 12 направлений + текущая позиция, коммит с гистерезисом
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
bool SolveFiringPosition(Agent& a, int index, const SolverContext& ctx, SolveResult& out)
|
|||
|
|
{
|
|||
|
|
const std::vector<Target>& targets = *ctx.targets;
|
|||
|
|
|
|||
|
|
out.count = 0;
|
|||
|
|
out.bestIndex = -1;
|
|||
|
|
out.committed = false;
|
|||
|
|
|
|||
|
|
// Цель решателя: назначенная (ручной режим) либо ближайшая достижимая.
|
|||
|
|
int bestTargetId = -1;
|
|||
|
|
if (ctx.assignedTargetId >= 0 && ctx.assignedTargetId < int(targets.size()) &&
|
|||
|
|
targets[ctx.assignedTargetId].alive)
|
|||
|
|
bestTargetId = ctx.assignedTargetId;
|
|||
|
|
else
|
|||
|
|
bestTargetId = NearestAliveTarget(a.pos, ctx, a.gun.def.range + g_tune.ring3);
|
|||
|
|
|
|||
|
|
out.bestTargetId = bestTargetId;
|
|||
|
|
if (bestTargetId < 0) return false; // стрелять не по чему — стоим
|
|||
|
|
|
|||
|
|
const float rings[SOLVER_RINGS] = {g_tune.ring1, g_tune.ring2, g_tune.ring3};
|
|||
|
|
|
|||
|
|
auto push = [&](Vec2 p, bool forceValid) {
|
|||
|
|
if (out.count >= SOLVER_CANDIDATES) return;
|
|||
|
|
const int i = out.count++;
|
|||
|
|
out.candidates[i] = p;
|
|||
|
|
out.valid[i] = true;
|
|||
|
|
out.scores[i] = 0.0f;
|
|||
|
|
|
|||
|
|
if (!forceValid)
|
|||
|
|
{
|
|||
|
|
// Отбраковка: вне карты / в стене / за поводком от диска /
|
|||
|
|
// слишком близко к другому агенту.
|
|||
|
|
if (p.x < 1.0f || p.y < 1.0f || p.x > float(MAP_W) - 1.0f || p.y > float(MAP_H) - 1.0f)
|
|||
|
|
out.valid[i] = false;
|
|||
|
|
else if (Distance(p, ctx.anchor) > g_tune.cohesionRadius + g_tune.postLeash)
|
|||
|
|
out.valid[i] = false;
|
|||
|
|
else if (ctx.map->CircleHitsWall(p, a.bodyRadius))
|
|||
|
|
out.valid[i] = false;
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
for (int k = 0; k < ctx.agentCount; ++k)
|
|||
|
|
{
|
|||
|
|
if (k == index) continue;
|
|||
|
|
if (Distance(p, ctx.agents[k].pos) < g_tune.candMinAlly)
|
|||
|
|
{
|
|||
|
|
out.valid[i] = false;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (out.valid[i]) out.scores[i] = ScoreCandidate(p, a, index, ctx, bestTargetId);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
push(a.pos, true); // индекс 0 — текущая позиция, для сравнения
|
|||
|
|
out.currentScore = out.scores[0];
|
|||
|
|
|
|||
|
|
for (int r = 0; r < SOLVER_RINGS; ++r)
|
|||
|
|
for (int k = 0; k < SOLVER_DIRS; ++k)
|
|||
|
|
push(a.pos + FromAngle(float(k) * (TWO_PI_F / float(SOLVER_DIRS))) * rings[r], false);
|
|||
|
|
|
|||
|
|
out.bestScore = out.currentScore;
|
|||
|
|
for (int i = 1; i < out.count; ++i)
|
|||
|
|
{
|
|||
|
|
if (!out.valid[i]) continue;
|
|||
|
|
if (out.scores[i] > out.bestScore)
|
|||
|
|
{
|
|||
|
|
out.bestScore = out.scores[i];
|
|||
|
|
out.bestIndex = i;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Коммит с гистерезисом — иначе агент дёргается между двумя точками.
|
|||
|
|
if (out.bestIndex > 0 && out.bestScore > out.currentScore + g_tune.hysteresis)
|
|||
|
|
{
|
|||
|
|
a.postPos = out.candidates[out.bestIndex];
|
|||
|
|
a.commitTimer = g_tune.commitTime;
|
|||
|
|
out.committed = true;
|
|||
|
|
}
|
|||
|
|
return true;
|
|||
|
|
}
|