#include "AttackEvaluator.h" #include #include #include #include #include "AttackBehavior.h" #include "BehaviorScores.h" #include "BehaviorTargeting.h" #include "EntityAdmin.h" #include "FactionComponent.h" #include "ModuleOwnerComponent.h" #include "PositionComponent.h" #include "SensorRangeComponent.h" #include "tracing.h" #include "WeaponComponent.h" #include "WorldConfig.h" AttackEvaluator::AttackEvaluator(const WorldTargeting& targeting) : m_targeting(&targeting) { } void AttackEvaluator::evaluate(EntityAdmin& admin) { TRACE(); const std::vector combatants = buildCombatants(admin); // Pass A: the maximum weapon range per ship, used to normalise target // distance. Ships without a weapon fall back to their sensor range below. std::unordered_map maxWeaponRange_tiles; admin.forEach( [&maxWeaponRange_tiles](entt::entity /*we*/, const WeaponComponent& weapon, const ModuleOwnerComponent& owner) { float& best = maxWeaponRange_tiles[owner.owner]; best = std::max(best, weapon.range_tiles); }); // Pass B: claim counts, taken from every ship's current target before any // target is reassigned this tick. Each ship reads the previous tick's claim // state and excludes its own contribution when scoring its current target. std::unordered_map claimsByTarget; admin.forEach( [&claimsByTarget, &admin](entt::entity /*e*/, const AttackBehavior& attack) { if (attack.currentTarget && admin.isValid(*attack.currentTarget)) { ++claimsByTarget[*attack.currentTarget]; } }); // Pass C: per-ship target selection. admin.forEach( [&](entt::entity e, AttackBehavior& attack, const PositionComponent& pos, const FactionComponent& faction, const SensorRangeComponent& sensor) { const float sensorRange_tiles = sensor.value_tiles; // Distance normaliser: max weapon range, or sensor range if unarmed. float weaponRange_tiles = sensorRange_tiles; const auto weaponRangeIt = maxWeaponRange_tiles.find(e); if (weaponRangeIt != maxWeaponRange_tiles.end() && weaponRangeIt->second > 0.0f) { weaponRange_tiles = weaponRangeIt->second; } // Scores a single candidate: base desirability from distance, reduced // by the overclaim penalty. selfClaimed subtracts this ship's own claim // so it does not penalise the target it already holds. const auto scoreOf = [&](const QVector2D& candidatePos, entt::entity candidate) -> float { const float dist = (candidatePos - pos.value).length(); const float x = dist / weaponRange_tiles; float base = static_cast(m_targeting->targetScoreFormula.evaluate(x)); base = std::max(base, 0.0f); int claims = 0; const auto claimIt = claimsByTarget.find(candidate); if (claimIt != claimsByTarget.end()) { claims = claimIt->second; } if (attack.currentTarget && candidate == *attack.currentTarget) { --claims; } float penalty = static_cast( m_targeting->overclaimPenaltyFormula.evaluate(claims)); penalty = std::clamp(penalty, 0.0f, 1.0f); return base * penalty; }; // Find the best candidate among in-range enemies. std::optional bestTarget; float bestScore = 0.0f; for (const CombatantInfo& c : combatants) { if (c.entity == e) { continue; } const bool isValidTarget = faction.isEnemy ? !c.isEnemy : c.isEnemy; if (!isValidTarget) { continue; } const float dist = (c.position - pos.value).length(); if (dist > sensorRange_tiles) { continue; } const float score = scoreOf(c.position, c.entity); if (!bestTarget || score > bestScore) { bestScore = score; bestTarget = c.entity; } } // Hysteresis: keep the current target if it is still valid and in // range, unless a challenger beats its score by more than the margin. bool keptCurrent = false; if (attack.currentTarget) { const entt::entity t = *attack.currentTarget; if (admin.isValid(t) && admin.hasAll(t)) { const QVector2D targetPos = admin.get(t).value; const float dist = (targetPos - pos.value).length(); if (dist <= sensorRange_tiles) { const float currentScore = scoreOf(targetPos, t); const float margin = 1.0f + static_cast(m_targeting->hysteresis); if (!bestTarget || bestScore <= currentScore * margin) { keptCurrent = true; } } } } if (!keptCurrent) { attack.currentTarget = bestTarget; } attack.score = attack.currentTarget ? BehaviorScores::kAttack : BehaviorScores::kInactive; }); }