#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]; if (!ally.alive) continue; // упавший больше не перекрывает линию 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 // ----------------------------------------------------------------------------- // Приоритет цели: обстановка идёт последней // ----------------------------------------------------------------------------- bool EnemyNearby(Vec2 from, const SolverContext& ctx) { const float r = g_tune.propYieldRadius; for (const Target& t : *ctx.targets) { if (!t.alive || t.IsProp()) continue; if (Distance(from, t.pos) <= r) return true; } return false; } // ----------------------------------------------------------------------------- // 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]; if (!ally.alive) continue; 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& targets = *ctx.targets; int best = -1; float bestD = searchRadius; for (int i = 0; i < int(targets.size()); ++i) { if (!targets[i].alive) continue; if (targets[i].IsProp()) 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& 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; } // --- авто: ближайшая цель, до которой линия чиста --- // Два прохода, и порядок между ними — это и есть приоритет. Сначала твари; // обстановка рассматривается только когда противника в поле зрения нет // вообще, иначе боец бросал бы врага ради ящика за его спиной. auto nearestClear = [&](bool wantProps) { int best = -1; float bestD = BIG_F; for (int i = 0; i < int(targets.size()); ++i) { if (!targets[i].alive) continue; if (targets[i].IsProp() != wantProps) 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; } return best; }; int best = nearestClear(false); const bool onProp = (best < 0 && !EnemyNearby(a.pos, ctx)); if (onProp) best = nearestClear(true); if (best >= 0) { a.currentTargetId = best; a.hasLane = true; // Линия к хламу приоритетной не бывает: приоритет запрещает решателю // искать позицию получше, а ради ящика боец переступать обязан. a.priorityLane = (ctx.assignedTargetId < 0) && !onProp; 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& 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; // Покрытие считается по ПРОТИВНИКАМ. Позиция, с которой видно три // ящика, огневой не является, а «слишком близко» к бочке — не штраф. if (targets[i].IsProp()) 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& 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 (!ctx.agents[k].alive) 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; }