147 lines
5.7 KiB
C++
147 lines
5.7 KiB
C++
#include "AttackEvaluator.h"
|
|
|
|
#include <algorithm>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
#include <QVector2D>
|
|
|
|
#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<CombatantInfo> 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<entt::entity, float> maxWeaponRange_tiles;
|
|
admin.forEach<WeaponComponent, ModuleOwnerComponent>(
|
|
[&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<entt::entity, int> claimsByTarget;
|
|
admin.forEach<AttackBehavior>(
|
|
[&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<AttackBehavior, PositionComponent, FactionComponent,
|
|
SensorRangeComponent>(
|
|
[&](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<float>(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<float>(
|
|
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<entt::entity> 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<PositionComponent>(t))
|
|
{
|
|
const QVector2D targetPos = admin.get<PositionComponent>(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<float>(m_targeting->hysteresis);
|
|
if (!bestTarget || bestScore <= currentScore * margin)
|
|
{
|
|
keptCurrent = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!keptCurrent)
|
|
{
|
|
attack.currentTarget = bestTarget;
|
|
}
|
|
|
|
attack.score = attack.currentTarget
|
|
? BehaviorScores::kAttack
|
|
: BehaviorScores::kInactive;
|
|
});
|
|
}
|
|
|