feat(EliteSoldier): 精锐士兵重构为刺客定位技能体系 - 完全重写EliteSoldierSkill.cs,将职业定位从大剑狂战改为刺客 - 实现1级【裂空突袭】:飞天斩击敌方后排,造成200%物理暴击伤害 - 添加3级【弱点锁定】:伤害提升至250%,暴击率+25%,落地移速+35% - 添加6级【影袭破防】:目标眩晕1.2秒且物理易伤35% - 添加10级【死斗连环】:击杀后连斩最多3次,无缝腾空攻击下一个后排 - 优化动画系统:飞跃期间关闭root motion和nav mesh agent避免位移冲突 - 更新动画控制器override controller的clip引用 - 在预制体中添加isStunImmune字段用于10级霸体效果 - 更新游戏说明文档中的技能描述和开发日志 ```
400 lines
15 KiB
C#
400 lines
15 KiB
C#
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
/// <summary>
|
||
/// 城堡法师技能:爆裂火球体系(1/3/6/10 级完整阶梯)
|
||
/// 1级【爆裂火球】:发射强力火球,对目标区域造成 140% 范围伤害(触发 MagicExplosionFire 爆破)
|
||
/// 3级【强化爆裂】:火球伤害提升至 175%,爆炸范围 +25%
|
||
/// 6级【灼烧火海】:爆裂处留下一片火海,对经过敌人造成 3 秒持续燃烧伤害
|
||
/// 10级【双发爆裂】:施法时连续轰出 2 颗爆裂火球,第二颗伤害提升 50% 且范围扩大
|
||
/// </summary>
|
||
public class CastleMageSkill : UnitSkill
|
||
{
|
||
[Header("火球配置")]
|
||
[Tooltip("火球飞行速度")]
|
||
public float projectileSpeed = 16f;
|
||
|
||
[Tooltip("爆裂基础半径")]
|
||
public float baseExplosionRadius = 2.5f;
|
||
|
||
[Header("特效资源")]
|
||
public GameObject fireballMissilePrefab; // 火球飞弹 (FireballSharpMissileFire)
|
||
public GameObject explosionVfxPrefab; // 爆破特效 (MagicExplosionFire)
|
||
public GameObject burnFieldVfxPrefab; // 灼烧火海地面特效 (FireFieldRed)
|
||
|
||
private bool isSkillPending = false;
|
||
|
||
private void EnsureUnit()
|
||
{
|
||
if (unit == null)
|
||
{
|
||
unit = GetComponent<UnitBase>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 当能量蓄满时触发:播放法师技能施法动作
|
||
/// </summary>
|
||
public override void ReleaseSkill()
|
||
{
|
||
EnsureUnit();
|
||
if (unit == null) return;
|
||
|
||
WorldHealthBar healthBar = unit.GetComponent<WorldHealthBar>();
|
||
if (healthBar == null || healthBar.IsDead) return;
|
||
|
||
if (unit.Target == null) return;
|
||
|
||
unit.FaceCurrentTarget(0f);
|
||
isSkillPending = true;
|
||
|
||
// 播放法师施法动作,由 OnSkillHit1 帧触发火球发射
|
||
unit.PlaySkillAnimation(OnSkillAnimationFinished, 1.8f);
|
||
Debug.Log($"<color=orange>城堡法师释放技能【爆裂火球】</color>,等待 OnSkillHit1 施法帧!");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 动画命中帧方法:在法杖前挥法术成型的瞬间触发 (OnSkillHit1)
|
||
/// </summary>
|
||
public void OnSkillHit1()
|
||
{
|
||
if (!isSkillPending) return;
|
||
isSkillPending = false;
|
||
|
||
EnsureUnit();
|
||
if (unit == null || unit.Target == null) return;
|
||
|
||
int level = unit.level;
|
||
|
||
// 1级 140%,3级+ 175%
|
||
float damageMultiplier = level >= 3 ? 1.75f : 1.40f;
|
||
float radius = level >= 3 ? baseExplosionRadius * 1.25f : baseExplosionRadius;
|
||
|
||
// 发射第一颗爆裂火球
|
||
LaunchFireball(unit.Target, damageMultiplier, radius, level >= 6);
|
||
|
||
// 10级大招【双发爆裂】:间隔 0.2 秒追加第二颗强化火球
|
||
if (level >= 10)
|
||
{
|
||
StartCoroutine(LaunchSecondFireballDelayed(unit.Target, damageMultiplier * 1.50f, radius * 1.30f, level >= 6));
|
||
}
|
||
|
||
Debug.Log($"<color=orange>【爆裂火球】发射!</color> 伤害倍率: {damageMultiplier * 100:F0}%, 范围: {radius:F1}m (等级: {level})");
|
||
}
|
||
|
||
private IEnumerator LaunchSecondFireballDelayed(GameObject primaryTarget, float damageMultiplier, float radius, bool createBurnField)
|
||
{
|
||
yield return new WaitForSeconds(0.25f);
|
||
if (unit == null) yield break;
|
||
|
||
// 优先打目标后方的敌人;如果只有一个怪或没有更后方的人,直接打同一个目标,绝不打空
|
||
GameObject secondTarget = FindBacklineTarget(primaryTarget);
|
||
|
||
if (secondTarget != null && secondTarget.activeInHierarchy)
|
||
{
|
||
LaunchFireball(secondTarget, damageMultiplier, radius, createBurnField);
|
||
bool isSameTarget = secondTarget == primaryTarget;
|
||
Debug.Log($"<color=red>【双发爆裂】追加第二发火球!</color> 目标: {secondTarget.name} ({(isSameTarget ? "单目标连续轰炸" : "后排敌群轰炸")}),伤害倍率: {damageMultiplier * 100:F0}%");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 寻找位于当前目标后方的敌人。若无后排敌人,回退返回当前目标。
|
||
/// </summary>
|
||
private GameObject FindBacklineTarget(GameObject currentTarget)
|
||
{
|
||
if (unit == null) return currentTarget;
|
||
|
||
UnitTeam myTeam = unit.GetComponent<UnitTeam>();
|
||
if (myTeam == null) return currentTarget;
|
||
|
||
// 如果原目标已失效/死亡,回退找任意当前有效目标
|
||
if (currentTarget == null || !currentTarget.activeInHierarchy)
|
||
{
|
||
return unit.FindNearestEnemyForSkill();
|
||
}
|
||
|
||
Vector3 magePos = unit.transform.position;
|
||
Vector3 targetPos = currentTarget.transform.position;
|
||
Vector3 forwardDir = (targetPos - magePos).normalized;
|
||
if (forwardDir.sqrMagnitude < 0.001f) forwardDir = unit.transform.forward;
|
||
|
||
var allTeams = Object.FindObjectsOfType<UnitTeam>();
|
||
GameObject bestBacklineTarget = null;
|
||
float bestDistance = float.MaxValue;
|
||
|
||
foreach (var t in allTeams)
|
||
{
|
||
if (t == null || !t.gameObject.activeInHierarchy) continue;
|
||
if (!myTeam.IsEnemy(t.gameObject)) continue;
|
||
if (t.gameObject == currentTarget || t.transform.root == currentTarget.transform.root) continue;
|
||
|
||
// 检查敌方单位是否存活
|
||
var targetUnit = t.GetComponentInParent<UnitBase>();
|
||
if (targetUnit != null && targetUnit.IsDead) continue;
|
||
var bar = t.GetComponentInParent<WorldHealthBar>();
|
||
if (bar != null && bar.IsDead) continue;
|
||
|
||
GameObject enemyGo = targetUnit != null ? targetUnit.gameObject : t.gameObject;
|
||
|
||
// 计算该敌人相对当前目标的深度 (沿法师射向的距离)
|
||
Vector3 offset = enemyGo.transform.position - targetPos;
|
||
float depth = Vector3.Dot(offset, forwardDir);
|
||
|
||
// 只要位于目标后方 (深度 > 0.8米)
|
||
if (depth > 0.8f)
|
||
{
|
||
// 优先选择距离当前目标最近的后排敌人(紧邻的后排敌群),形成层次分明的前后封锁
|
||
float dist = offset.magnitude;
|
||
if (dist < bestDistance)
|
||
{
|
||
bestDistance = dist;
|
||
bestBacklineTarget = enemyGo;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果找到了后排敌人,打后排敌人;如果场上只有一个怪或无后排敌人,打同一个目标
|
||
return bestBacklineTarget != null ? bestBacklineTarget : currentTarget;
|
||
}
|
||
|
||
private void LaunchFireball(GameObject target, float damageMultiplier, float radius, bool createBurnField)
|
||
{
|
||
GameObject missilePrefab = fireballMissilePrefab;
|
||
if (missilePrefab == null && unit != null)
|
||
{
|
||
missilePrefab = unit.projectilePrefab;
|
||
}
|
||
#if UNITY_EDITOR
|
||
if (missilePrefab == null)
|
||
{
|
||
missilePrefab = UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>("Assets/TJGeneratorLibEffects/Prefabs/Combat/Missiles/FireballSharp/FireballSharpMissileFire.prefab");
|
||
}
|
||
#endif
|
||
if (missilePrefab == null || target == null || unit == null) return;
|
||
|
||
Vector3 spawnPos = unit.projectileSpawnPoint != null
|
||
? unit.projectileSpawnPoint.position
|
||
: unit.transform.position + Vector3.up * 1.2f + unit.transform.forward * 0.5f;
|
||
|
||
Vector3 dir = (target.transform.position - spawnPos).normalized;
|
||
GameObject missileGo = Instantiate(missilePrefab, spawnPos, Quaternion.LookRotation(dir));
|
||
|
||
float totalDamage = unit.attackPower * damageMultiplier;
|
||
|
||
// 使用自定义弧线/追踪脚本,并在命中时引发范围爆炸
|
||
var runner = missileGo.AddComponent<MageSkillFireballRunner>();
|
||
runner.Initialize(target, totalDamage, radius, unit.gameObject, projectileSpeed, explosionVfxPrefab, burnFieldVfxPrefab, createBurnField);
|
||
}
|
||
|
||
private void OnSkillAnimationFinished()
|
||
{
|
||
isSkillPending = false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 负责爆裂火球的飞行、落地爆炸伤害计算与灼烧火海生成
|
||
/// </summary>
|
||
public class MageSkillFireballRunner : MonoBehaviour
|
||
{
|
||
private GameObject target;
|
||
private float damage;
|
||
private float explosionRadius;
|
||
private GameObject owner;
|
||
private float speed;
|
||
private GameObject explosionVfx;
|
||
private GameObject burnFieldVfx;
|
||
private bool createBurnField;
|
||
private bool hasExploded = false;
|
||
|
||
public void Initialize(GameObject targetObj, float dmg, float radius, GameObject ownerObj, float moveSpeed, GameObject expVfx, GameObject burnVfx, bool burnField)
|
||
{
|
||
target = targetObj;
|
||
damage = dmg;
|
||
explosionRadius = radius;
|
||
owner = ownerObj;
|
||
speed = moveSpeed;
|
||
explosionVfx = expVfx;
|
||
burnFieldVfx = burnVfx;
|
||
createBurnField = burnField;
|
||
|
||
// 屏蔽原有 Projectile,接管飞行与碰撞
|
||
var defaultProj = GetComponent<Projectile>();
|
||
if (defaultProj != null) defaultProj.enabled = false;
|
||
|
||
Destroy(gameObject, 4.0f);
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
if (hasExploded) return;
|
||
|
||
if (target == null)
|
||
{
|
||
ExplodeAtPosition(transform.position);
|
||
return;
|
||
}
|
||
|
||
Vector3 targetPos = target.transform.position + Vector3.up * 0.8f;
|
||
Vector3 toTarget = targetPos - transform.position;
|
||
float step = speed * Time.deltaTime;
|
||
|
||
if (toTarget.sqrMagnitude <= step * step || toTarget.sqrMagnitude <= 0.15f)
|
||
{
|
||
ExplodeAtPosition(targetPos);
|
||
return;
|
||
}
|
||
|
||
transform.rotation = Quaternion.LookRotation(toTarget.normalized);
|
||
transform.position += toTarget.normalized * step;
|
||
}
|
||
|
||
private void ExplodeAtPosition(Vector3 pos)
|
||
{
|
||
if (hasExploded) return;
|
||
hasExploded = true;
|
||
|
||
// 1. 生成爆炸视觉特效
|
||
GameObject expPrefab = explosionVfx;
|
||
#if UNITY_EDITOR
|
||
if (expPrefab == null)
|
||
{
|
||
expPrefab = UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>("Assets/TJGeneratorLibEffects/Prefabs/Combat/Explosions/MagicExplosion/MagicExplosionFire.prefab");
|
||
}
|
||
#endif
|
||
if (expPrefab != null)
|
||
{
|
||
var expGo = Instantiate(expPrefab, pos, Quaternion.identity);
|
||
expGo.transform.localScale = Vector3.one * (explosionRadius / 1.8f);
|
||
Destroy(expGo, 3.0f);
|
||
}
|
||
|
||
// 2. 结算范围伤害
|
||
ApplyAreaDamage(pos, explosionRadius, damage);
|
||
|
||
// 3. 6级解锁:生成持续 3 秒的灼烧火海(带有地面燃烧火焰特效)
|
||
if (createBurnField)
|
||
{
|
||
GameObject burnArea = new GameObject("BurnField");
|
||
burnArea.transform.position = new Vector3(pos.x, 0.05f, pos.z);
|
||
var area = burnArea.AddComponent<MageBurnArea>();
|
||
|
||
GameObject fieldPrefab = burnFieldVfx;
|
||
#if UNITY_EDITOR
|
||
if (fieldPrefab == null)
|
||
{
|
||
fieldPrefab = UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>("Assets/TJGeneratorLibEffects/Prefabs/Environment/Fire/Cartoon/Field Intense/FireFieldIntenseRed.prefab");
|
||
}
|
||
#endif
|
||
area.Initialize(owner, damage * 0.15f, explosionRadius, 3.0f, fieldPrefab);
|
||
}
|
||
|
||
Destroy(gameObject);
|
||
}
|
||
|
||
private void ApplyAreaDamage(Vector3 center, float radius, float dmg)
|
||
{
|
||
if (owner == null) return;
|
||
UnitTeam myTeam = owner.GetComponent<UnitTeam>();
|
||
if (myTeam == null) return;
|
||
|
||
float sqrRadius = radius * radius;
|
||
var allUnits = Object.FindObjectsOfType<UnitTeam>();
|
||
|
||
foreach (var t in allUnits)
|
||
{
|
||
if (t == null || !t.gameObject.activeInHierarchy || !myTeam.IsEnemy(t.gameObject)) continue;
|
||
|
||
Vector3 flatOffset = t.transform.position - center;
|
||
flatOffset.y = 0f;
|
||
if (flatOffset.sqrMagnitude <= sqrRadius)
|
||
{
|
||
var targetUnit = t.GetComponentInParent<UnitBase>();
|
||
if (targetUnit != null)
|
||
{
|
||
targetUnit.ReceiveDamage(dmg, owner);
|
||
}
|
||
else
|
||
{
|
||
var bar = t.GetComponentInParent<WorldHealthBar>();
|
||
if (bar != null) bar.TakeDamage(dmg, owner);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 6 级【灼烧火海】:在地面持续 3 秒,每秒对进入范围的敌人造成持续燃烧伤害
|
||
/// </summary>
|
||
public class MageBurnArea : MonoBehaviour
|
||
{
|
||
private GameObject owner;
|
||
private float dps;
|
||
private float radius;
|
||
private float lifetime;
|
||
|
||
public void Initialize(GameObject ownerObj, float burnDps, float r, float duration, GameObject vfxPrefab)
|
||
{
|
||
owner = ownerObj;
|
||
dps = burnDps;
|
||
radius = r;
|
||
lifetime = duration;
|
||
|
||
// 实例化地面烈焰燃烧粒子特效(严格保持 X = -90° 旋转水平平铺地面)
|
||
if (vfxPrefab != null)
|
||
{
|
||
Quaternion rot = Quaternion.Euler(-90f, 0f, 0f);
|
||
var vfxInstance = Instantiate(vfxPrefab, transform.position, rot);
|
||
vfxInstance.transform.rotation = rot;
|
||
vfxInstance.transform.SetParent(transform, true);
|
||
vfxInstance.transform.localScale = Vector3.one * (radius / 1.5f);
|
||
}
|
||
|
||
StartCoroutine(BurnRoutine());
|
||
Destroy(gameObject, duration + 0.1f);
|
||
}
|
||
|
||
private IEnumerator BurnRoutine()
|
||
{
|
||
float timer = 0f;
|
||
float tickInterval = 0.5f;
|
||
|
||
while (timer < lifetime)
|
||
{
|
||
yield return new WaitForSeconds(tickInterval);
|
||
timer += tickInterval;
|
||
|
||
if (owner == null) break;
|
||
UnitTeam myTeam = owner.GetComponent<UnitTeam>();
|
||
if (myTeam == null) break;
|
||
|
||
float sqrRadius = radius * radius;
|
||
var allUnits = Object.FindObjectsOfType<UnitTeam>();
|
||
|
||
foreach (var t in allUnits)
|
||
{
|
||
if (t == null || !t.gameObject.activeInHierarchy || !myTeam.IsEnemy(t.gameObject)) continue;
|
||
|
||
Vector3 flatOffset = t.transform.position - transform.position;
|
||
flatOffset.y = 0f;
|
||
if (flatOffset.sqrMagnitude <= sqrRadius)
|
||
{
|
||
var unit = t.GetComponentInParent<UnitBase>();
|
||
if (unit != null)
|
||
{
|
||
unit.ReceiveDamage(dps * tickInterval, owner);
|
||
}
|
||
else
|
||
{
|
||
var bar = t.GetComponentInParent<WorldHealthBar>();
|
||
if (bar != null) bar.TakeDamage(dps * tickInterval, owner);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|