Files
SoulliesOfficial b5cb6152ff MusicBeat
2026-05-26 00:21:27 -04:00

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