make ships claim targets
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
#include "AttackEvaluator.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <QVector2D>
|
||||
@@ -10,57 +12,131 @@
|
||||
#include "EntityAdmin.h"
|
||||
#include "FactionComponent.h"
|
||||
#include "HealthComponent.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, HealthComponent>(
|
||||
[&](entt::entity e, AttackBehavior& attack, const PositionComponent& pos,
|
||||
const FactionComponent& faction, const SensorRangeComponent& sensor,
|
||||
const HealthComponent& health)
|
||||
{
|
||||
const float range = sensor.value_tiles;
|
||||
const float sensorRange_tiles = sensor.value_tiles;
|
||||
|
||||
// Validate current target: still valid, still in range.
|
||||
bool targetValid = false;
|
||||
// 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 float dist =
|
||||
(admin.get<PositionComponent>(t).value - pos.value).length();
|
||||
if (dist <= range) { targetValid = true; }
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire nearest valid target if needed.
|
||||
if (!targetValid)
|
||||
{
|
||||
attack.currentTarget = std::nullopt;
|
||||
float bestDist = range;
|
||||
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 < bestDist)
|
||||
const QVector2D targetPos = admin.get<PositionComponent>(t).value;
|
||||
const float dist = (targetPos - pos.value).length();
|
||||
if (dist <= sensorRange_tiles)
|
||||
{
|
||||
bestDist = dist;
|
||||
attack.currentTarget = c.entity;
|
||||
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; }
|
||||
|
||||
const bool healthy =
|
||||
(health.maxHp > 0.0f)
|
||||
&& (health.hp / health.maxHp >= BehaviorScores::kLowHpFraction);
|
||||
|
||||
Reference in New Issue
Block a user