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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|