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