302 lines
12 KiB
C#
302 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Cielonos.MainGame.Characters;
|
|
using UnityEngine;
|
|
|
|
namespace Cielonos.MainGame
|
|
{
|
|
public partial class CombatManager
|
|
{
|
|
public partial class EnemySubmodule
|
|
{
|
|
/// <summary>
|
|
/// 索敌评分结果,包含目标引用和各项分数明细。
|
|
/// </summary>
|
|
public struct TargetingScore
|
|
{
|
|
public Enemy target;
|
|
/// <summary>最终评分 = baseScore + bonusScore。</summary>
|
|
public float totalScore;
|
|
/// <summary>基础加权评分(由 GetScoredEnemies 计算,不受后续修正影响)。</summary>
|
|
public float baseScore;
|
|
/// <summary>修正评分累加值(由 ApplyScoreModifier 叠加)。</summary>
|
|
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>
|
|
/// 索敌评分配置,允许外部调整权重和参数。
|
|
/// </summary>
|
|
public TargetingScoreConfig TargetingConfig => _targetingConfig;
|
|
|
|
/// <summary>
|
|
/// 记录最近命中的目标,用于计算粘滞分。
|
|
/// 由攻击系统在命中时调用。
|
|
/// </summary>
|
|
public void SetLastHitTarget(CharacterBase target)
|
|
{
|
|
_lastHitTarget = target;
|
|
_lastHitTime = Time.time;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 综合索敌:在指定半径内对所有敌人评分,返回按分数从高到低排序的列表。
|
|
/// </summary>
|
|
/// <param name="radius">索敌半径</param>
|
|
/// <param name="origin">索敌原点,默认为玩家位置</param>
|
|
public List<TargetingScore> GetScoredEnemies(float radius, Transform origin = null)
|
|
{
|
|
if (origin == null)
|
|
{
|
|
origin = Player.transform;
|
|
}
|
|
|
|
List<Enemy> candidates = GetEnemiesInRadius(origin.position, radius);
|
|
if (candidates.Count == 0) return new List<TargetingScore>();
|
|
|
|
Camera camera = Player.viewSc.playerCamera;
|
|
Vector2 moveInput = Player.inputSc.Move;
|
|
bool hasInput = 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 = Player.viewSc.lockTargetModule.isLocking
|
|
? Player.viewSc.lockTargetModule.lockTarget
|
|
: null;
|
|
|
|
// 粘滞衰减
|
|
float stickyFactor = 0f;
|
|
if (_lastHitTarget != null && !_lastHitTarget.statusSm.isDead)
|
|
{
|
|
float elapsed = Time.time - _lastHitTime;
|
|
stickyFactor = Mathf.Clamp01(1f - elapsed / _targetingConfig.stickyDecayTime);
|
|
}
|
|
|
|
float totalWeight = _targetingConfig.TotalWeight;
|
|
if (totalWeight <= 0f) totalWeight = 1f;
|
|
|
|
// 预计算最大距离用于归一化
|
|
float maxDist = 0f;
|
|
foreach (CharacterBase enemy in candidates)
|
|
{
|
|
float dist = Vector3.Distance(origin.position, enemy.transform.position);
|
|
if (dist > maxDist) maxDist = dist;
|
|
}
|
|
if (maxDist <= 0f) maxDist = 1f;
|
|
|
|
float inputCosThreshold = Mathf.Cos(_targetingConfig.inputConeHalfAngle * Mathf.Deg2Rad);
|
|
float cameraCosThreshold = Mathf.Cos(_targetingConfig.cameraConeHalfAngle * Mathf.Deg2Rad);
|
|
|
|
List<TargetingScore> results = new List<TargetingScore>(candidates.Count);
|
|
|
|
foreach (Enemy enemy in candidates)
|
|
{
|
|
if (enemy == null || enemy.statusSm.isDead) continue;
|
|
|
|
Vector3 toEnemy = enemy.transform.position - origin.position;
|
|
toEnemy.y = 0f;
|
|
float distance = toEnemy.magnitude;
|
|
Vector3 toEnemyDir = distance > 0.01f ? toEnemy / distance : Vector3.zero;
|
|
|
|
TargetingScore score = new TargetingScore { target = enemy };
|
|
|
|
// 1. 距离分:越近越高 [0, 1]
|
|
score.distanceScore = 1f - Mathf.Clamp01(distance / maxDist);
|
|
|
|
// 2. 输入方向分:有输入时按 dot product 计算 [0, 1],无输入时所有目标得满分
|
|
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. 摄像机朝向分 [0, 1]
|
|
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. 粘滞分 [0, 1]
|
|
score.stickyScore = (enemy == _lastHitTarget) ? stickyFactor : 0f;
|
|
|
|
// 5. 锁定目标分 [0 或 1]
|
|
score.lockOnScore = (lockTarget != null && enemy == lockTarget) ? 1f : 0f;
|
|
|
|
// 加权总分
|
|
score.baseScore =
|
|
(_targetingConfig.distanceWeight * score.distanceScore
|
|
+ _targetingConfig.inputDirectionWeight * score.inputDirScore
|
|
+ _targetingConfig.cameraFacingWeight * score.cameraFacingScore
|
|
+ _targetingConfig.stickyWeight * score.stickyScore
|
|
+ _targetingConfig.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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 综合索敌的便捷方法:返回评分最高的单个敌人。
|
|
/// </summary>
|
|
public Enemy GetBestEnemy(float radius, Transform origin = null, Func<Enemy> overrideCandidate = null)
|
|
{
|
|
if (overrideCandidate != null)
|
|
{
|
|
Enemy oc = overrideCandidate();
|
|
if (oc != null)
|
|
{
|
|
return oc;
|
|
}
|
|
}
|
|
|
|
List<TargetingScore> scores = GetScoredEnemies(radius, origin);
|
|
return scores.Count > 0 ? scores[0].target : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 综合索敌的便捷方法:返回评分前 N 的敌人列表。
|
|
/// </summary>
|
|
public List<Enemy> GetBestEnemies(float radius, int count, Transform origin = null)
|
|
{
|
|
List<TargetingScore> scores = GetScoredEnemies(radius, origin);
|
|
List<Enemy> result = new List<Enemy>(Mathf.Min(count, scores.Count));
|
|
for (int i = 0; i < scores.Count && i < count; i++)
|
|
{
|
|
result.Add(scores[i].target);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取最优敌人,优先返回锁定目标(如果在范围内),否则返回综合评分最高的敌人。
|
|
/// </summary>
|
|
public CharacterBase GetBestEnemyWithLockonFirst(float radius)
|
|
{
|
|
return GetBestEnemy(50f, null, ReturnLockon);
|
|
|
|
Enemy ReturnLockon()
|
|
{
|
|
LockTargetSubmodule lockModule = MainGameManager.Player.viewSc.lockTargetModule;
|
|
if (lockModule.isLocking && lockModule.lockTarget != null)
|
|
{
|
|
return lockModule.lockTarget;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
public partial class EnemySubmodule
|
|
{
|
|
public Enemy GetNearestEnemy(float radius, Transform origin = null)
|
|
{
|
|
origin ??= Player.transform;
|
|
List<Enemy> candidates = GetEnemiesInRadius(origin.position, radius);
|
|
return candidates.FirstOrDefault();
|
|
}
|
|
|
|
public List<Enemy> GetNearestEnemies(float radius, int count, Transform origin = null)
|
|
{
|
|
origin ??= Player.transform;
|
|
List<Enemy> candidates = GetEnemiesInRadius(origin.position, radius);
|
|
return candidates.Take(count).ToList();
|
|
}
|
|
}
|
|
}
|
|
|
|
public static class TargetingSystemExtensions
|
|
{
|
|
public static List<CombatManager.EnemySubmodule.TargetingScore> ApplyScoreModifier(
|
|
this List<CombatManager.EnemySubmodule.TargetingScore> scores,
|
|
List<Enemy> 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<CombatManager.EnemySubmodule.TargetingScore> ApplyScoreModifier(
|
|
this List<CombatManager.EnemySubmodule.TargetingScore> scores,
|
|
Predicate<Enemy> 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<CombatManager.EnemySubmodule.TargetingScore> scores)
|
|
{
|
|
return scores.Count > 0 ? scores[0].target : null;
|
|
}
|
|
|
|
public static List<Enemy> BestEnemies(this List<CombatManager.EnemySubmodule.TargetingScore> scores, int count)
|
|
{
|
|
List<Enemy> result = new List<Enemy>(Mathf.Min(count, scores.Count));
|
|
for (int i = 0; i < scores.Count && i < count; i++)
|
|
{
|
|
result.Add(scores[i].target);
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
}
|