Files
Cielonos/Assets/Scripts/MainGame/Managers/CombatManager/EnemySubmodule/TargetingSystem.cs

749 lines
29 KiB
C#
Raw Normal View History

2026-05-23 08:27:50 -04:00
using System;
using System.Collections.Generic;
using Cielonos.MainGame.Characters;
using UnityEngine;
namespace Cielonos.MainGame
{
public partial class CombatManager
{
public partial class EnemySubmodule
{
/// <summary>
2026-07-18 03:16:20 -04:00
/// 索敌评分结果。保留各分项,方便调试面板和后续权重调参。
2026-05-23 08:27:50 -04:00
/// </summary>
public struct TargetingScore
{
2026-05-26 00:21:27 -04:00
public Enemy target;
2026-05-23 08:27:50 -04:00
public float totalScore;
public float baseScore;
public float bonusScore;
public float distanceScore;
public float inputDirScore;
public float cameraFacingScore;
public float stickyScore;
public float lockOnScore;
}
private CharacterBase _lastHitTarget;
private float _lastHitTime;
private readonly TargetingScoreConfig _targetingConfig = new();
/// <summary>
2026-07-18 03:16:20 -04:00
/// 索敌评分配置。这里控制距离、输入方向、镜头朝向、粘滞目标、锁定目标等评分权重。
2026-05-23 08:27:50 -04:00
/// </summary>
public TargetingScoreConfig TargetingConfig => _targetingConfig;
/// <summary>
2026-07-18 03:16:20 -04:00
/// 记录最近命中的目标,用于让连续攻击在短时间内更倾向同一个敌人。
2026-05-23 08:27:50 -04:00
/// </summary>
public void SetLastHitTarget(CharacterBase target)
{
_lastHitTarget = target;
_lastHitTime = Time.time;
}
/// <summary>
2026-07-18 03:16:20 -04:00
/// 创建一条索敌查询链。默认从玩家位置出发,候选范围为全部活跃敌人。
2026-05-23 08:27:50 -04:00
/// </summary>
2026-07-18 03:16:20 -04:00
public TargetingQuery Query(TargetingScorePreset preset = TargetingScorePreset.Default)
2026-05-23 08:27:50 -04:00
{
2026-07-18 03:16:20 -04:00
return new TargetingQuery(this).UsePreset(preset);
}
/// <summary>
/// 创建指定半径内的索敌查询链。
/// </summary>
public TargetingQuery Query(float radius, TargetingScorePreset preset = TargetingScorePreset.Default)
{
return new TargetingQuery(this).WithinRadius(radius).UsePreset(preset);
}
/// <summary>
/// 创建基于已有候选列表的索敌查询链。
/// </summary>
public TargetingQuery Query(List<Enemy> candidates, TargetingScorePreset preset = TargetingScorePreset.Default)
{
return new TargetingQuery(this).FromCandidates(candidates).UsePreset(preset);
}
private List<TargetingScore> ScoreEnemies(List<Enemy> candidates, Vector3 origin, TargetingScoreConfig config = null)
{
if (candidates == null || candidates.Count == 0)
2026-05-23 08:27:50 -04:00
{
2026-07-18 03:16:20 -04:00
return new List<TargetingScore>();
2026-05-23 08:27:50 -04:00
}
2026-07-18 03:16:20 -04:00
config ??= _targetingConfig;
2026-05-23 08:27:50 -04:00
2026-07-18 03:16:20 -04:00
Camera camera = MainGameManager.Player.viewSc.playerCamera;
Vector2 moveInput = MainGameManager.Player.inputSc.Move;
bool hasInput = MainGameManager.Player.inputSc.IsMoving;
2026-05-23 08:27:50 -04:00
2026-07-18 03:16:20 -04:00
// 将移动输入转成世界方向;有输入时,摇杆方向会成为强索敌意图。
2026-05-23 08:27:50 -04:00
Vector3 inputWorldDir = Vector3.zero;
if (hasInput)
{
float cameraYaw = camera.transform.eulerAngles.y;
inputWorldDir = Quaternion.Euler(0f, cameraYaw, 0f) *
new Vector3(moveInput.x, 0f, moveInput.y).normalized;
}
Vector3 cameraForward = camera.transform.forward;
cameraForward.y = 0f;
cameraForward.Normalize();
2026-07-18 03:16:20 -04:00
CharacterBase lockTarget = MainGameManager.Player.viewSc.lockTargetModule.isLocking
? MainGameManager.Player.viewSc.lockTargetModule.lockTarget
2026-05-23 08:27:50 -04:00
: null;
2026-07-18 03:16:20 -04:00
// 最近命中目标会随时间衰减,避免攻击链在多敌人中频繁抖动换目标。
2026-05-23 08:27:50 -04:00
float stickyFactor = 0f;
if (_lastHitTarget != null && !_lastHitTarget.statusSm.isDead)
{
float elapsed = Time.time - _lastHitTime;
2026-07-18 03:16:20 -04:00
stickyFactor = Mathf.Clamp01(1f - elapsed / config.stickyDecayTime);
2026-05-23 08:27:50 -04:00
}
2026-07-18 03:16:20 -04:00
float totalWeight = config.TotalWeight;
if (totalWeight <= 0f)
{
totalWeight = 1f;
}
2026-05-23 08:27:50 -04:00
float maxDist = 0f;
2026-07-18 03:16:20 -04:00
for (int i = 0; i < candidates.Count; i++)
2026-05-23 08:27:50 -04:00
{
2026-07-18 03:16:20 -04:00
Enemy enemy = candidates[i];
if (enemy == null || enemy.statusSm.isDead) continue;
float dist = GetSurfaceDistance(origin, enemy);
if (dist > maxDist)
{
maxDist = dist;
}
}
if (maxDist <= 0f)
{
maxDist = 1f;
2026-05-23 08:27:50 -04:00
}
2026-07-18 03:16:20 -04:00
float inputCosThreshold = Mathf.Cos(config.inputConeHalfAngle * Mathf.Deg2Rad);
float cameraCosThreshold = Mathf.Cos(config.cameraConeHalfAngle * Mathf.Deg2Rad);
2026-05-23 08:27:50 -04:00
List<TargetingScore> results = new List<TargetingScore>(candidates.Count);
2026-07-18 03:16:20 -04:00
for (int i = 0; i < candidates.Count; i++)
2026-05-23 08:27:50 -04:00
{
2026-07-18 03:16:20 -04:00
Enemy enemy = candidates[i];
2026-05-23 08:27:50 -04:00
if (enemy == null || enemy.statusSm.isDead) continue;
2026-07-18 03:16:20 -04:00
Vector3 toEnemy = enemy.transform.position - origin;
2026-05-23 08:27:50 -04:00
toEnemy.y = 0f;
2026-07-18 03:16:20 -04:00
float centerDistance = toEnemy.magnitude;
float distance = GetSurfaceDistance(origin, enemy);
Vector3 toEnemyDir = centerDistance > 0.01f ? toEnemy / centerDistance : Vector3.zero;
2026-05-23 08:27:50 -04:00
TargetingScore score = new TargetingScore { target = enemy };
2026-07-18 03:16:20 -04:00
// 1. 距离分:越近越高。
2026-05-23 08:27:50 -04:00
score.distanceScore = 1f - Mathf.Clamp01(distance / maxDist);
2026-07-18 03:16:20 -04:00
// 2. 输入方向分:有移动输入时,优先选玩家指向的敌人;无输入时不惩罚任何目标。
2026-05-23 08:27:50 -04:00
if (hasInput && toEnemyDir.sqrMagnitude > 0f)
{
float dot = Vector3.Dot(inputWorldDir, toEnemyDir);
score.inputDirScore = dot >= inputCosThreshold
? Mathf.InverseLerp(inputCosThreshold, 1f, dot)
: 0f;
}
else
{
score.inputDirScore = 1f;
}
2026-07-18 03:16:20 -04:00
// 3. 镜头朝向分:用于辅助无输入或弱输入时的自然索敌。
2026-05-23 08:27:50 -04:00
if (cameraForward.sqrMagnitude > 0f && toEnemyDir.sqrMagnitude > 0f)
{
float dot = Vector3.Dot(cameraForward, toEnemyDir);
score.cameraFacingScore = dot >= cameraCosThreshold
? Mathf.InverseLerp(cameraCosThreshold, 1f, dot)
: 0f;
}
else
{
score.cameraFacingScore = 0f;
}
2026-07-18 03:16:20 -04:00
// 4. 粘滞分:短时间内维持攻击目标,减少目标跳变。
score.stickyScore = enemy == _lastHitTarget ? stickyFactor : 0f;
2026-05-23 08:27:50 -04:00
2026-07-18 03:16:20 -04:00
// 5. 锁定分:锁定目标在常规评分中获得额外倾向,但不会强制覆盖所有查询。
score.lockOnScore = lockTarget != null && enemy == lockTarget ? 1f : 0f;
2026-05-23 08:27:50 -04:00
score.baseScore =
2026-07-18 03:16:20 -04:00
(config.distanceWeight * score.distanceScore
+ config.inputDirectionWeight * score.inputDirScore
+ config.cameraFacingWeight * score.cameraFacingScore
+ config.stickyWeight * score.stickyScore
+ config.lockOnWeight * score.lockOnScore)
2026-05-23 08:27:50 -04:00
/ totalWeight;
score.bonusScore = 0f;
score.totalScore = score.baseScore;
results.Add(score);
}
results.Sort((a, b) => b.totalScore.CompareTo(a.totalScore));
return results;
}
2026-07-18 03:16:20 -04:00
private Enemy GetValidLockonTarget(Vector3 origin, float radius, List<Enemy> candidates)
2026-05-23 08:27:50 -04:00
{
2026-07-18 03:16:20 -04:00
var lockModule = MainGameManager.Player.viewSc.lockTargetModule;
if (!lockModule.isLocking || lockModule.lockTarget == null)
2026-05-23 08:27:50 -04:00
{
2026-07-18 03:16:20 -04:00
return null;
2026-05-23 08:27:50 -04:00
}
2026-07-18 03:16:20 -04:00
Enemy lockTarget = lockModule.lockTarget;
if (candidates != null && !candidates.Contains(lockTarget))
2026-05-23 08:27:50 -04:00
{
2026-07-18 03:16:20 -04:00
return null;
2026-05-23 08:27:50 -04:00
}
2026-07-18 03:16:20 -04:00
return IsEnemyInRadius(lockTarget, origin, radius) ? lockTarget : null;
2026-05-23 08:27:50 -04:00
}
/// <summary>
2026-07-18 03:16:20 -04:00
/// 链式索敌查询。调用侧应尽量通过这套 API 表达索敌意图:
/// CombatManager.EnemySm.Query(4f).PreferDisruptable().Best();
/// Query 会缓存候选列表和评分结果,仅建议在一次决策中临时使用,不要跨帧保存为字段。
2026-05-23 08:27:50 -04:00
/// </summary>
2026-07-18 03:16:20 -04:00
public sealed class TargetingQuery
2026-05-23 08:27:50 -04:00
{
2026-07-18 03:16:20 -04:00
private readonly EnemySubmodule _enemySm;
private readonly List<Predicate<Enemy>> _filters = new List<Predicate<Enemy>>();
private readonly List<ScoreModifier> _scoreModifiers = new List<ScoreModifier>();
private readonly List<Enemy> _excludedTargets = new List<Enemy>();
private Transform _originTransform;
private Vector3 _originPosition;
private bool _hasOriginPosition;
private float _radius = float.MaxValue;
private SortingType _sortingType = SortingType.Nearest;
private TargetingScoreConfig _scoreConfig;
private List<Enemy> _sourceCandidates;
private List<Enemy> _cachedCandidates;
private List<TargetingScore> _cachedScores;
private Vector3 _cachedOrigin;
private bool _hasCachedOrigin;
private int _cachedFrame = -1;
private int _cachedTargetingVersion = -1;
public TargetingQuery(EnemySubmodule enemySm)
{
_enemySm = enemySm;
}
/// <summary>
/// 设置索敌原点为某个 Transform。评分和半径都会使用它的当前位置。
/// </summary>
public TargetingQuery From(Transform origin)
{
_originTransform = origin;
_hasOriginPosition = false;
Invalidate();
return this;
}
/// <summary>
/// 设置索敌原点为固定世界坐标,适合爆炸、弹体、攻击区域等非角色来源。
/// </summary>
public TargetingQuery From(Vector3 origin)
{
_originPosition = origin;
_originTransform = null;
_hasOriginPosition = true;
Invalidate();
return this;
}
/// <summary>
/// 限制候选目标半径。
/// </summary>
public TargetingQuery WithinRadius(float radius)
{
_radius = radius;
Invalidate();
return this;
}
/// <summary>
/// 使用外部已经筛好的候选列表继续索敌。
/// </summary>
public TargetingQuery FromCandidates(List<Enemy> candidates)
{
_sourceCandidates = candidates != null ? new List<Enemy>(candidates) : null;
Invalidate();
return this;
}
/// <summary>
/// 设置候选列表的基础排序,主要用于 Candidates/Nearest。
/// </summary>
public TargetingQuery SortBy(SortingType sortingType)
{
_sortingType = sortingType;
Invalidate();
return this;
}
public TargetingQuery UsePreset(TargetingScorePreset preset)
{
_scoreConfig = TargetingScoreConfig.CreatePreset(preset);
InvalidateScores();
return this;
}
/// <summary>
/// 从候选目标中排除单个敌人。
/// </summary>
public TargetingQuery Exclude(Enemy target)
{
if (target != null && !_excludedTargets.Contains(target))
{
_excludedTargets.Add(target);
Invalidate();
}
2026-05-23 08:27:50 -04:00
2026-07-18 03:16:20 -04:00
return this;
}
/// <summary>
/// 从候选目标中排除一组敌人。
/// </summary>
public TargetingQuery Exclude(IEnumerable<Enemy> targets)
2026-05-23 08:27:50 -04:00
{
2026-07-18 03:16:20 -04:00
if (targets == null)
2026-05-23 08:27:50 -04:00
{
2026-07-18 03:16:20 -04:00
return this;
2026-05-23 08:27:50 -04:00
}
2026-07-18 03:16:20 -04:00
foreach (Enemy target in targets)
{
if (target != null && !_excludedTargets.Contains(target))
{
_excludedTargets.Add(target);
}
}
Invalidate();
return this;
2026-05-23 08:27:50 -04:00
}
2026-07-18 03:16:20 -04:00
/// <summary>
/// 硬过滤候选目标。不符合条件的敌人会直接从 Candidates/Scores/Best 中移除。
/// </summary>
public TargetingQuery Where(Predicate<Enemy> match)
{
if (match == null)
{
return this;
}
_filters.Add(match);
Invalidate();
return this;
}
/// <summary>
/// 只保留存活敌人。默认候选列表已做该过滤,显式调用可增强复杂查询的语义。
/// </summary>
public TargetingQuery OnlyAlive()
{
return Where(enemy => enemy != null && !enemy.statusSm.isDead);
}
/// <summary>
/// 只保留屏幕内可见的敌人。
/// </summary>
public TargetingQuery OnlyInScreen()
{
return Where(IsInScreen);
}
/// <summary>
/// 只保留当前索敌系统认为可见的敌人。目前等同于屏幕内目标,后续可扩展遮挡判定。
/// </summary>
public TargetingQuery OnlyVisible()
{
return OnlyInScreen();
}
/// <summary>
/// 只保留可被指定 Breakthrough 类型打断的敌人。
/// </summary>
public TargetingQuery OnlyDisruptable(Breakthrough.Type breakthroughType = Breakthrough.Type.Disruption)
{
return Where(enemy => _enemySm.IsDisruptable(enemy, breakthroughType));
}
/// <summary>
/// 对符合条件的目标施加额外评分。offset 是固定加分amplifier 是按基础分放大。
/// </summary>
public TargetingQuery Prefer(Predicate<Enemy> match, float offset = 1f, float amplifier = 0f)
{
if (match == null)
{
return this;
}
_scoreModifiers.Add(new ScoreModifier(match, amplifier, offset));
InvalidateScores();
return this;
}
/// <summary>
/// 提高可被 Breakthrough 打断目标的优先级。
/// </summary>
public TargetingQuery PreferDisruptable(float offset = 1f,
Breakthrough.Type breakthroughType = Breakthrough.Type.Disruption, float amplifier = 0f)
{
return Prefer(enemy => _enemySm.IsDisruptable(enemy, breakthroughType), offset, amplifier);
}
/// <summary>
/// 锁定目标优先的终端查询:如果锁定目标在本次候选范围内则直接返回,否则返回 Best()。
/// </summary>
public Enemy LockonFirst()
{
Enemy lockTarget = _enemySm.GetValidLockonTarget(Origin, _radius, ResolveCandidates());
return lockTarget != null ? lockTarget : Best();
}
/// <summary>
/// 返回当前查询的候选敌人列表。列表按 SortBy 指定规则排序,默认最近优先。
/// </summary>
public List<Enemy> Candidates()
{
return new List<Enemy>(ResolveCandidates());
}
/// <summary>
/// 返回完整评分结果,按 totalScore 从高到低排序。
/// </summary>
public List<TargetingScore> Scores()
{
return new List<TargetingScore>(ResolveScores());
}
/// <summary>
/// 返回综合评分最高的敌人。
/// </summary>
public Enemy Best()
{
return ResolveScores().BestEnemy();
}
/// <summary>
/// 返回综合评分最高的前 count 个敌人。
/// </summary>
public List<Enemy> Best(int count)
{
return ResolveScores().BestEnemies(count);
}
/// <summary>
/// 返回距离原点最近的敌人,不进行综合评分。
/// </summary>
public Enemy Nearest()
{
List<Enemy> candidates = ResolveCandidates();
return candidates.Count > 0 ? candidates[0] : null;
}
/// <summary>
/// 返回距离原点最近的前 count 个敌人,不进行综合评分。
/// </summary>
public List<Enemy> Nearest(int count)
{
List<Enemy> candidates = ResolveCandidates();
List<Enemy> result = new List<Enemy>(Mathf.Min(count, candidates.Count));
for (int i = 0; i < candidates.Count && i < count; i++)
{
result.Add(candidates[i]);
}
return result;
}
/// <summary>
/// 当前候选列表中是否存在可被 Breakthrough 打断的敌人。
/// </summary>
public bool AnyDisruptable(Breakthrough.Type breakthroughType = Breakthrough.Type.Disruption)
{
List<Enemy> candidates = ResolveCandidates();
for (int i = 0; i < candidates.Count; i++)
{
if (_enemySm.IsDisruptable(candidates[i], breakthroughType))
{
return true;
}
}
return false;
}
private Vector3 Origin
{
get
{
if (_hasOriginPosition)
{
return _originPosition;
}
return _originTransform != null ? _originTransform.position : MainGameManager.Player.transform.position;
}
}
private List<Enemy> ResolveCandidates()
{
InvalidateIfStale();
if (_cachedCandidates != null)
{
return _cachedCandidates;
}
Vector3 origin = Origin;
List<Enemy> candidates = _sourceCandidates == null
? _enemySm.CollectEnemiesInRadius(origin, _radius, _sortingType)
: FilterSourceCandidates(origin);
if (_excludedTargets.Count > 0)
{
for (int i = candidates.Count - 1; i >= 0; i--)
{
if (_excludedTargets.Contains(candidates[i]))
{
candidates.RemoveAt(i);
}
}
}
ApplyFilters(candidates);
SortCandidates(candidates, origin);
_cachedCandidates = candidates;
_cachedOrigin = origin;
_hasCachedOrigin = true;
_cachedFrame = Time.frameCount;
_cachedTargetingVersion = _enemySm.TargetingVersion;
return _cachedCandidates;
}
private List<TargetingScore> ResolveScores()
{
InvalidateIfStale();
if (_cachedScores != null)
{
return _cachedScores;
}
List<TargetingScore> scores = _enemySm.ScoreEnemies(ResolveCandidates(), Origin, _scoreConfig);
foreach (ScoreModifier modifier in _scoreModifiers)
{
scores.ApplyScoreModifier(modifier.match, modifier.amplifier, modifier.offset);
}
_cachedScores = scores;
_cachedOrigin = Origin;
_hasCachedOrigin = true;
_cachedFrame = Time.frameCount;
_cachedTargetingVersion = _enemySm.TargetingVersion;
return _cachedScores;
}
private List<Enemy> FilterSourceCandidates(Vector3 origin)
{
List<Enemy> candidates = new List<Enemy>(_sourceCandidates.Count);
for (int i = 0; i < _sourceCandidates.Count; i++)
{
Enemy enemy = _sourceCandidates[i];
if (enemy == null || enemy.statusSm.isDead)
{
continue;
}
if (_radius < float.MaxValue && !_enemySm.IsEnemyInRadius(enemy, origin, _radius))
{
continue;
}
candidates.Add(enemy);
}
return candidates;
}
private void ApplyFilters(List<Enemy> candidates)
{
if (_filters.Count == 0)
{
return;
}
for (int i = candidates.Count - 1; i >= 0; i--)
{
Enemy enemy = candidates[i];
for (int j = 0; j < _filters.Count; j++)
{
if (!_filters[j](enemy))
{
candidates.RemoveAt(i);
break;
}
}
}
}
private void SortCandidates(List<Enemy> candidates, Vector3 origin)
{
if (_sortingType == SortingType.Nearest)
{
candidates.Sort((a, b) =>
_enemySm.GetSurfaceDistance(origin, a)
.CompareTo(_enemySm.GetSurfaceDistance(origin, b)));
}
else if (_sortingType == SortingType.Farthest)
{
candidates.Sort((a, b) =>
_enemySm.GetSurfaceDistance(origin, b)
.CompareTo(_enemySm.GetSurfaceDistance(origin, a)));
}
}
private static bool IsInScreen(Enemy enemy)
{
if (enemy == null || enemy.statusSm.isDead)
{
return false;
}
Vector3 screenPos = MainGameManager.Player.viewSc.playerCamera.WorldToScreenPoint(enemy.CenterPoint.position);
return screenPos.z > 0f &&
screenPos.x > 0f && screenPos.x < Screen.width &&
screenPos.y > 0f && screenPos.y < Screen.height;
}
private void Invalidate()
{
_cachedCandidates = null;
_cachedScores = null;
_hasCachedOrigin = false;
_cachedFrame = -1;
_cachedTargetingVersion = -1;
}
private void InvalidateScores()
{
_cachedScores = null;
}
private void InvalidateIfStale()
{
if (_cachedFrame < 0)
{
return;
}
if (_cachedFrame == Time.frameCount &&
_cachedTargetingVersion == _enemySm.TargetingVersion &&
(!_hasCachedOrigin || _cachedOrigin == Origin))
{
return;
}
_cachedCandidates = null;
_cachedScores = null;
_hasCachedOrigin = false;
_cachedFrame = -1;
_cachedTargetingVersion = -1;
}
private readonly struct ScoreModifier
{
public readonly Predicate<Enemy> match;
public readonly float amplifier;
public readonly float offset;
public ScoreModifier(Predicate<Enemy> match, float amplifier, float offset)
{
this.match = match;
this.amplifier = amplifier;
this.offset = offset;
}
}
2026-05-23 08:27:50 -04:00
}
}
}
public static class TargetingSystemExtensions
{
public static List<CombatManager.EnemySubmodule.TargetingScore> ApplyScoreModifier(
this List<CombatManager.EnemySubmodule.TargetingScore> scores,
2026-05-26 00:21:27 -04:00
List<Enemy> boostTargets, float amplifier, float offset = 0)
2026-05-23 08:27:50 -04:00
{
return scores.ApplyScoreModifier(Predicate, amplifier, offset);
2026-05-26 00:21:27 -04:00
bool Predicate(Enemy target)
2026-05-23 08:27:50 -04:00
{
return boostTargets != null && boostTargets.Contains(target);
}
}
public static List<CombatManager.EnemySubmodule.TargetingScore> ApplyScoreModifier(
this List<CombatManager.EnemySubmodule.TargetingScore> scores,
2026-05-26 00:21:27 -04:00
Predicate<Enemy> match, float amplifier, float offset = 0)
2026-05-23 08:27:50 -04:00
{
bool changed = false;
for (int i = 0; i < scores.Count; i++)
{
if (!match(scores[i].target)) continue;
CombatManager.EnemySubmodule.TargetingScore s = scores[i];
s.bonusScore += s.baseScore * amplifier + offset;
s.totalScore = s.baseScore + s.bonusScore;
scores[i] = s;
changed = true;
}
if (changed)
{
scores.Sort((a, b) => b.totalScore.CompareTo(a.totalScore));
}
2026-07-18 03:16:20 -04:00
2026-05-23 08:27:50 -04:00
return scores;
}
2026-05-26 00:21:27 -04:00
public static Enemy BestEnemy(this List<CombatManager.EnemySubmodule.TargetingScore> scores)
2026-05-23 08:27:50 -04:00
{
return scores.Count > 0 ? scores[0].target : null;
}
2026-07-18 03:16:20 -04:00
2026-05-26 00:21:27 -04:00
public static List<Enemy> BestEnemies(this List<CombatManager.EnemySubmodule.TargetingScore> scores, int count)
2026-05-23 08:27:50 -04:00
{
2026-05-26 00:21:27 -04:00
List<Enemy> result = new List<Enemy>(Mathf.Min(count, scores.Count));
2026-05-23 08:27:50 -04:00
for (int i = 0; i < scores.Count && i < count; i++)
{
result.Add(scores[i].target);
}
2026-07-18 03:16:20 -04:00
2026-05-23 08:27:50 -04:00
return result;
}
}
}