#include "ai/melee.h" namespace { constexpr float BIG_F = 1e18f; // Угловой полуразмер тела радиуса r, видимого с расстояния dist. // Вплотную союзник перекрывает почти весь сектор — так и должно быть. float AngularHalfSize(float dist, float r) { return std::asin(Clampf(r / std::max(dist, r), 0.0f, 1.0f)); } // Годится ли мишень как цель ближнего боя: жива, внутри СВОЕГО круга бойца, // не дальше meleeSeekRadius от него самого и видна (за стену не бегаем — // путей в прототипе нет, а обход угла даёт слой wallAvoidance). bool MeleeCandidate(const Agent& a, const SolverContext& ctx, int id) { const Target& t = (*ctx.targets)[id]; if (!t.alive) return false; if (Distance(t.pos, ctx.anchor) > g_tune.meleeCohesionRadius) return false; if (Distance(t.pos, a.pos) > g_tune.meleeSeekRadius) return false; if (ctx.map->RaycastBlocked(a.pos, t.pos)) return false; return true; } } // namespace void EvaluateMeleeTarget(Agent& a, int index, const SolverContext& ctx) { const std::vector& targets = *ctx.targets; // Линии огня у бойца нет и быть не может — обнуляем всю стрелковую часть, // иначе он попадёт и в реестр линий, и в FIRE UPTIME. a.hasLane = false; a.priorityLane = false; a.laneIntent = false; a.targetBlockedByAlly = false; a.targetBlockedByWall = false; MeleeWeapon& m = a.melee; // Пока идёт замах или удар, цель не переназначается: ось уже зафиксирована. if (m.Committed() && m.targetId >= 0 && targets[m.targetId].alive) { a.currentTargetId = m.targetId; a.laneEnd = targets[m.targetId].pos; m.inReach = Distance(a.pos, targets[m.targetId].pos) <= m.reach + targets[m.targetId].radius; return; } m.targetId = -1; m.inReach = false; // Назначенная игроком цель приоритетна и здесь (6.8), но только если она // проходит те же условия — иначе боец убежал бы через всю карту. int best = -1; if (ctx.assignedTargetId >= 0 && ctx.assignedTargetId < int(targets.size()) && MeleeCandidate(a, ctx, ctx.assignedTargetId)) { best = ctx.assignedTargetId; } else { float bestCost = BIG_F; for (int i = 0; i < int(targets.size()); ++i) { if (!MeleeCandidate(a, ctx, i)) continue; // Цена = дистанция + штраф за каждого своего, уже вцепившегося // в эту мишень. Ближняя цель всё ещё выигрывает, но не любой ценой: // вдвоём на одну — это оборванные взмахи друг об друга и ноль урона. float cost = Distance(a.pos, targets[i].pos); for (int k = 0; k < ctx.agentCount; ++k) { if (k == index) continue; if (ctx.agents[k].IsMelee() && ctx.agents[k].melee.targetId == i) cost += g_tune.meleeClaimPenalty; } if (cost >= bestCost) continue; best = i; bestCost = cost; } } m.targetId = best; a.currentTargetId = best; if (best < 0) return; a.laneEnd = targets[best].pos; m.inReach = Distance(a.pos, targets[best].pos) <= m.reach + targets[best].radius; m.arcBlockedByAlly = m.inReach && !MeleeArcClear(a, index, AngleOf(targets[best].pos - a.pos), ctx); } Vec2 MeleeGoal(const Agent& a, const SolverContext& ctx, Vec2 fallbackSlot) { const MeleeWeapon& m = a.melee; if (m.targetId < 0) return fallbackSlot; const Target& t = (*ctx.targets)[m.targetId]; // Встаём вплотную, но не внутрь: reach считается от центра тела, поэтому // с такой дистанции удар достаёт с запасом. const float stand = t.radius + a.bodyRadius + g_tune.meleeGap; const Vec2 away = Normalized(a.pos - t.pos, FromAngle(a.facing + PI_F)); return t.pos + away * stand; } bool MeleeArcClear(const Agent& a, int index, float axisAngle, const SolverContext& ctx) { const MeleeWeapon& m = a.melee; const float half = m.HalfArc(); const float reach = m.reach + g_tune.meleeAllyMargin; // Взмах живёт windup + strike секунд, и всё это время союзник продолжает // идти. Проверять только текущее положение — значит начинать удары, которые // всё равно придётся обрывать. Это ровно та же поправка, что и // AllyInterceptsShot для пули: правило не ослабляется, оно смотрит вперёд. const float horizon = (m.windup + m.strike) * g_tune.meleeStartPredict; for (int i = 0; i < ctx.agentCount; ++i) { if (i == index) continue; const Agent& ally = ctx.agents[i]; const Vec2 probe[2] = {ally.pos, ally.pos + ally.vel * horizon}; for (const Vec2& q : probe) { const Vec2 d = q - a.pos; const float dist = Length(d); if (dist > reach + ally.bodyRadius) continue; if (dist < 1e-4f) return false; // стоим в одной точке — бить нельзя const float pad = AngularHalfSize(dist, ally.bodyRadius + g_tune.meleeAllyMargin); if (std::fabs(AngleDiff(axisAngle, AngleOf(d))) <= half + pad) return false; } } return true; } bool BladeSweepHits(Vec2 from, float angPrev, float angCur, float bladeHalf, float reach, Vec2 p, float pRadius) { const Vec2 d = p - from; const float dist = Length(d); if (dist > reach + pRadius) return false; // Тела перекрываются — попадает любой взмах, и это честно: цель обнимает // рукоять. Случай редкий, отсечён отдельно, чтобы ниже не делить на ноль. if (dist <= pRadius) return true; // Дальше — ТОЧНАЯ геометрия: лезвие это ОТРЕЗОК от бойца длиной reach, // повернувшийся за шаг с angPrev на angCur. // // Раньше здесь стоял угловой допуск asin(r/dist), и он вырождался: на // контактной дистанции (dist -> pRadius) asin даёт 90 градусов, «сектор» // разрастался в полуплоскость, и боец рубил за спину. В фазе рывка он как // раз проезжает вплотную — отсюда и брались удары «на 360 градусов». // Отрезок такого не допускает: для цели позади ближайшая точка лезвия — // рукоять, а до неё ровно dist, что больше pRadius. const float ang = AngleOf(d); const float sweep = AngleDiff(angPrev, angCur); // короткая дельта за шаг const float rel = AngleDiff(angPrev, ang); // где цель относительно старта // Направление на цель попало в заметённую за шаг полосу — значит лезвие // прошло через неё (дистанцию уже проверили сверху). const float lo = std::min(0.0f, sweep) - bladeHalf; const float hi = std::max(0.0f, sweep) + bladeHalf; if (rel >= lo && rel <= hi) return true; // Иначе цель могла быть задета краем: проверяем оба крайних положения. const float ends[2] = {angPrev, angCur}; for (float a : ends) if (DistancePointToSegment(p, from, from + FromAngle(a) * reach) <= pRadius) return true; return false; } Vec2 MeleeStrikeEnd(const Agent& a, const SolverContext& ctx) { const MeleeWeapon& m = a.melee; // На замахе и ударе ось уже зафиксирована, до него — направление на цель. float axis = m.dir; if (!m.Committed()) { if (m.targetId < 0) return a.pos; axis = AngleOf((*ctx.targets)[m.targetId].pos - a.pos); } return a.pos + FromAngle(axis) * m.reach; }