Refactor code structure for improved readability and maintainability

This commit is contained in:
2026-05-26 13:04:38 +08:00
parent f74d7f1877
commit 5a0f1548ea
53 changed files with 4853 additions and 163 deletions

View File

@@ -10,11 +10,12 @@ namespace BaseGames.Enemies.AI
/// BD Action启动 Boss 阶段过渡演出(无敌帧 + 可选定格),等待过渡完成后返回 Success。
///
/// 返回 Running过渡演出进行中BossBase.IsPhaseTransitioning = true
/// 返回 Success过渡完成,已切换到目标阶段
/// 返回 Success过渡完成或目标阶段已达到(含守护逻辑,防止重复触发)
/// 返回 FailureBossBase 组件不存在。
///
/// 典型 BT 用法:
/// Sequence [ BD_IsHPBelow(0.5) → BD_BossPhaseTransition(Phase=1, Duration=1.5) → ... ]
/// Sequence [ BD_IsHPBelow(0.5) → BD_BossPhaseTransition(Phase=1, Duration=1.5) → Phase2战斗节点 ]
/// 过渡完成后,此节点每 tick 因守护立即返回 SuccessSequence 直接进入后续战斗节点。
/// </summary>
[TaskName("Boss Phase Transition")]
[TaskCategory("BaseGames/Enemy/Boss")]
@@ -41,6 +42,9 @@ namespace BaseGames.Enemies.AI
{
if (_boss == null) return TaskStatus.Failure;
// 防止 BT 重入时重复触发阶段过渡:目标阶段已达到则直接返回 Success
if (_boss.CurrentPhase >= m_TargetPhase) return TaskStatus.Success;
if (!_started)
{
_boss.BeginPhaseTransition(m_TargetPhase, m_InvincibleDuration);

View File

@@ -0,0 +1,88 @@
using System.Collections;
using Animancer;
using UnityEngine;
namespace BaseGames.Enemies.Abilities
{
/// <summary>
/// 带动画的天花板跌落能力:播放 Fall 动画 + 切换物理体为 Dynamic 自由下落,落地后启用接触伤害并恢复巡逻。
/// 取代 sealed 的 CeilingDropAbility动画所有权完整封装在本能力中。
/// 可复用于任何需要从天花板落下并造成接触伤害的敌人。
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public class AnimatedCeilingDropAbility : EnemyAbilityBase
{
[Header("动画")]
[Tooltip("下落循环动画(旋转下落姿态,循环播放直到落地)")]
[SerializeField] private ClipTransition _fallLoopClip;
[Header("物理下落")]
[Tooltip("切换 Dynamic 后使用的重力倍率")]
[SerializeField] private float _fallGravityScale = 3.5f;
[Tooltip("下落超时保护(秒),超时后强制继续执行")]
[SerializeField] private float _maxFallTime = 3f;
[SerializeField] private LayerMask _groundMask;
[Header("落地后")]
[Tooltip("落地后恢复帧延迟(秒),之后切换为 Patrol 阶段")]
[SerializeField] private float _recoveryTime = 0.1f;
[SerializeField] private BodyContactDamage _contactDamage;
private Rigidbody2D _rb;
protected override void Awake()
{
base.Awake();
_rb = GetComponentInParent<Rigidbody2D>();
}
protected override IEnumerator ExecuteCoroutine()
{
Phase = AbilityRunState.Active;
// 播放下落动画(能力脚本负责动画所有权)
if (_fallLoopClip.Clip != null)
_animancer.Play(_fallLoopClip);
// 切换物理Kinematic → Dynamic + 重力
var origBodyType = _rb.bodyType;
var origGravScale = _rb.gravityScale;
_rb.bodyType = RigidbodyType2D.Dynamic;
_rb.gravityScale = _fallGravityScale;
_rb.velocity = Vector2.zero;
// 等待落地(超时保护)
float elapsed = 0f;
while (elapsed < _maxFallTime)
{
elapsed += Time.fixedDeltaTime;
yield return new WaitForFixedUpdate();
if (elapsed > 0.05f && IsGrounded()) break;
}
_rb.velocity = Vector2.zero;
// 落地后启用接触伤害
if (_contactDamage != null)
_contactDamage.enabled = true;
yield return EnemyAbilityWaits.Get(_recoveryTime);
// SetAiPhase(Patrol) 自动播放 AnimConfig.Walk地面移动动画
_enemy.SetAiPhase(AiPhase.Patrol);
}
private bool IsGrounded()
{
var hit = Physics2D.Raycast(_rb.position, Vector2.down, 0.6f, _groundMask);
return hit.collider != null;
}
protected override void OnInterrupted(InterruptReason reason)
{
if (_rb != null)
_rb.velocity = Vector2.zero;
if (_contactDamage != null)
_contactDamage.enabled = false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 76c9c3969851d084396edd617bac8ef0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,25 @@
using System.Collections;
using Animancer;
using UnityEngine;
namespace BaseGames.Enemies.Abilities
{
/// <summary>
/// 出场演出能力:播放出场动画后切换到 Combat 阶段适用于小Boss或需出场动画的精英怪
/// 可复用于任何"播出场动画→进入战斗"的敌人。
/// </summary>
public class AppearAbility : EnemyAbilityBase
{
[SerializeField] private ClipTransition _appearClip;
protected override IEnumerator ExecuteCoroutine()
{
Phase = AbilityRunState.Active;
if (_appearClip.Clip == null) yield break;
_animancer.Play(_appearClip);
yield return EnemyAbilityWaits.Get(_appearClip.Clip.length);
_enemy.SetAiPhase(AiPhase.Combat);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 419edb00543abc047a8e7601cf0f3ac4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,69 @@
using System.Collections;
using Animancer;
using BaseGames.Combat;
using UnityEngine;
namespace BaseGames.Enemies.Abilities
{
/// <summary>
/// 天花板三段攻击能力:出击 → 脆弱悬挂窗口 → 收回。
/// 悬挂阶段_loopClip为脆弱窗口HurtBox 激活,等待 _hangDuration 后结束。
/// 可复用于任何固定位置的天花板攻击型敌人。
/// </summary>
public class CeilingHangStrikeAbility : EnemyAbilityBase
{
[Header("动画")]
[SerializeField] private ClipTransition _strikeClip;
[SerializeField] private ClipTransition _loopClip;
[SerializeField] private ClipTransition _endClip;
[Header("碰撞")]
[SerializeField] private HitBox _attackHitBox;
[SerializeField] private HurtBox _hurtBox;
[Header("行为")]
[Tooltip("脆弱悬挂窗口持续时间(秒)")]
[SerializeField] private float _hangDuration = 2f;
protected override IEnumerator ExecuteCoroutine()
{
Phase = AbilityRunState.Active;
// 出击阶段:播放攻击动画并激活 HitBox
if (_strikeClip.Clip != null)
{
_animancer.Play(_strikeClip);
var dmgSrc = _config?.attackSequence?.Length > 0 ? _config.attackSequence[0].damageSource : null;
_attackHitBox?.Activate(dmgSrc, _transform);
yield return EnemyAbilityWaits.Get(_strikeClip.Clip.length);
}
_attackHitBox?.Deactivate();
// 脆弱悬挂阶段HurtBox 激活为玩家反击窗口
if (_loopClip.Clip != null)
_animancer.Play(_loopClip);
if (_hurtBox != null)
_hurtBox.enabled = true;
yield return EnemyAbilityWaits.Get(_hangDuration);
if (_hurtBox != null)
_hurtBox.enabled = false;
// 收回阶段
if (_endClip.Clip != null)
{
_animancer.Play(_endClip);
yield return EnemyAbilityWaits.Get(_endClip.Clip.length);
}
}
protected override void OnInterrupted(InterruptReason reason)
{
_attackHitBox?.Deactivate();
if (_hurtBox != null)
_hurtBox.enabled = false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5615d1d3eb70f414fb137428a3f7f962
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,65 @@
using System.Collections;
using Animancer;
using UnityEngine;
using BaseGames.Enemies.Perception;
namespace BaseGames.Enemies.Abilities
{
/// <summary>
/// 循环追击 + 体接触伤害能力。
/// 追击期间每帧更新 MoveTo失去感知后收招退出并恢复巡逻状态。
/// 可复用于任何需要"追击+接触伤害循环"的敌人。
/// </summary>
public class ContactChaseAbility : EnemyAbilityBase
{
[Header("动画")]
[SerializeField] private ClipTransition _loopClip;
[SerializeField] private ClipTransition _endClip;
[Header("感知与接触伤害")]
[SerializeField] private BodyContactDamage _contactDamage;
[SerializeField] private EnemySensorHub _sensorHub;
[Tooltip("用于追击感知判断的传感器槽位名,通常为 \"aggro\"")]
[SerializeField] private string _aggroSlotName = "aggro";
protected override IEnumerator ExecuteCoroutine()
{
Phase = AbilityRunState.Active;
if (_loopClip.Clip != null)
_animancer.Play(_loopClip);
if (_contactDamage != null)
_contactDamage.enabled = true;
while (true)
{
if (_enemy.PlayerTransform == null) break;
if (_sensorHub != null && !_sensorHub.IsDetecting(_aggroSlotName, _enemy.PlayerTransform.gameObject))
break;
_enemy.MoveTo(_enemy.PlayerTransform.position);
yield return null;
}
if (_contactDamage != null)
_contactDamage.enabled = false;
_enemy.StopMovement();
if (_endClip.Clip != null)
{
_animancer.Play(_endClip);
yield return EnemyAbilityWaits.Get(_endClip.Clip.length);
}
_enemy.SetAiPhase(AiPhase.Patrol);
}
protected override void OnInterrupted(InterruptReason reason)
{
if (_contactDamage != null)
_contactDamage.enabled = false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7bfd6f44ebdb5bf489ab6703b1ee429b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,42 @@
using System.Collections;
using System.Linq;
using Animancer;
using UnityEngine;
namespace BaseGames.Enemies.Abilities
{
/// <summary>
/// 面向玩家能力:朝向玩家后播放转身/翻转动画并等待完成。
/// CanUse 重写:仅在无其他技能运行且玩家在背后时可用。
/// 可复用于任何需要"检测背后玩家并翻转"的敌人。
/// </summary>
public class FacePlayerAbility : EnemyAbilityBase
{
[SerializeField] private ClipTransition _faceClip;
public override bool CanUse =>
base.CanUse
&& !_enemy.Abilities.All.Any(a => a != this && a.IsRunning)
&& IsPlayerBehind();
private bool IsPlayerBehind()
{
if (_enemy.PlayerTransform == null) return false;
float dx = _enemy.PlayerTransform.position.x - _enemy.transform.position.x;
int facing = _enemy.Movement?.FacingDirection ?? 1;
return (facing > 0 && dx < 0) || (facing < 0 && dx > 0);
}
protected override IEnumerator ExecuteCoroutine()
{
Phase = AbilityRunState.Active;
_enemy.Movement?.FaceTarget(_enemy.PlayerTransform.position);
if (_faceClip.Clip == null) yield break;
_animancer.Play(_faceClip);
yield return EnemyAbilityWaits.Get(_faceClip.Clip.length);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6c97df5c477ad0b488171b2b39530b08
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,81 @@
using System.Collections;
using Animancer;
using BaseGames.Combat;
using UnityEngine;
namespace BaseGames.Enemies.Abilities
{
/// <summary>
/// 近战攻击带后摇脆弱窗口能力:攻击动画期间按时间比例激活 HitBox
/// 攻击完成后开放 HurtBox脆弱窗口窗口结束后恢复正常。
/// 可复用于任何"攻击后有反击窗口"的敌人技能。
/// </summary>
public class MeleeVulnerabilityAbility : EnemyAbilityBase
{
[Header("动画")]
[SerializeField] private ClipTransition _attackClip;
[Header("碰撞")]
[SerializeField] private HitBox _hitBox;
[SerializeField] private HurtBox _hurtBox;
[Header("打击时间0~1 归一化,基于 Clip 时长)")]
[SerializeField, Range(0f, 1f)] private float _hitEnterT = 0.30f;
[SerializeField, Range(0f, 1f)] private float _hitExitT = 0.60f;
[Header("后摇脆弱窗口")]
[Tooltip("HurtBox 激活时间(秒)")]
[SerializeField] private float _staggerDuration = 1.0f;
protected override IEnumerator ExecuteCoroutine()
{
Phase = AbilityRunState.Active;
if (_attackClip.Clip == null) yield break;
float len = _attackClip.Clip.length;
float t = 0f;
bool active = false;
var dmgSrc = _config?.attackSequence?.Length > 0 ? _config.attackSequence[0].damageSource : null;
_animancer.Play(_attackClip);
while (t < len)
{
t += Time.deltaTime;
if (!active && t >= len * _hitEnterT)
{
_hitBox?.Activate(dmgSrc, _transform);
active = true;
}
if (active && t >= len * _hitExitT)
{
_hitBox?.Deactivate();
active = false;
}
yield return null;
}
if (active)
_hitBox?.Deactivate();
// 后摇脆弱窗口
if (_hurtBox != null)
_hurtBox.enabled = true;
yield return EnemyAbilityWaits.Get(_staggerDuration);
if (_hurtBox != null)
_hurtBox.enabled = false;
}
protected override void OnInterrupted(InterruptReason reason)
{
_hitBox?.Deactivate();
if (_hurtBox != null)
_hurtBox.enabled = false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b5fc3af69bdd62a418fe6e2ce136c9a8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,24 @@
using System.Collections;
using Animancer;
using UnityEngine;
namespace BaseGames.Enemies.Abilities
{
/// <summary>
/// 播放单个动画 Clip 并等待完成后退出(无副作用)。
/// 可复用于任何需要"播一段动画即完成"的能力(如出场激活、台词演出等)。
/// </summary>
public class PlayClipAbility : EnemyAbilityBase
{
[SerializeField] private ClipTransition _clip;
protected override IEnumerator ExecuteCoroutine()
{
Phase = AbilityRunState.Active;
if (_clip.Clip == null) yield break;
_animancer.Play(_clip);
yield return EnemyAbilityWaits.Get(_clip.Clip.length);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a26fca0fa72894a4da1a5a58ee023154
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,75 @@
using System.Collections;
using Animancer;
using BaseGames.Combat;
using UnityEngine;
namespace BaseGames.Enemies.Abilities
{
/// <summary>
/// 多段砸地能力:起手 → 循环砸地N次带 HitBox 激活)→ 收招。
/// 支持霸体ABL SO 配置 interruptOnHurt=false后摇时间可配置。
/// 可复用于任何多段地面攻击型敌人。
/// </summary>
public class RepeatSlamAbility : EnemyAbilityBase
{
[Header("动画")]
[SerializeField] private ClipTransition _startClip;
[SerializeField] private ClipTransition _loopClip;
[SerializeField] private ClipTransition _endClip;
[Header("打击")]
[SerializeField] private HitBox _hitBox;
[Header("行为配置")]
[Tooltip("每次砸地时 HitBox 激活时长(秒)")]
[SerializeField] private float _hitActiveTime = 0.15f;
[Tooltip("砸地次数")]
[SerializeField] private int _slamCount = 2;
[Tooltip("最后一次砸地收招后额外等待时间(秒)")]
[SerializeField] private float _staggerDuration = 1.2f;
protected override IEnumerator ExecuteCoroutine()
{
Phase = AbilityRunState.Active;
if (_startClip.Clip != null)
{
_animancer.Play(_startClip);
yield return EnemyAbilityWaits.Get(_startClip.Clip.length);
}
for (int i = 0; i < _slamCount; i++)
{
if (_loopClip.Clip != null)
{
_animancer.Play(_loopClip);
float preHit = Mathf.Max(0f, _loopClip.Clip.length - _hitActiveTime - 0.05f);
yield return EnemyAbilityWaits.Get(preHit);
}
var dmgSrc = _config?.attackSequence?.Length > 0 ? _config.attackSequence[0].damageSource : null;
_hitBox?.Activate(dmgSrc, _transform);
yield return EnemyAbilityWaits.Get(_hitActiveTime);
_hitBox?.Deactivate();
if (i < _slamCount - 1)
yield return EnemyAbilityWaits.Get(0.1f);
}
if (_endClip.Clip != null)
{
_animancer.Play(_endClip);
yield return EnemyAbilityWaits.Get(_endClip.Clip.length + _staggerDuration);
}
else
{
yield return EnemyAbilityWaits.Get(_staggerDuration);
}
}
protected override void OnInterrupted(InterruptReason reason)
{
_hitBox?.Deactivate();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ff71de2a91f535d488c76df6395f1d78
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -214,6 +214,7 @@ namespace BaseGames.Enemies
private IEnumerator PhaseTransitionCoroutine(int targetPhase, float duration)
{
IsPhaseTransitioning = true;
OnBeginPhaseTransition(targetPhase);
// 打断技能 + 停止移动
_skillExecutor?.InterruptCurrentSkill();
@@ -232,8 +233,7 @@ namespace BaseGames.Enemies
_phaseTransitionCoroutine = null;
}
/// <summary>
/// 立即终止阶段过渡协程并清除标志位。
/// <summary>立即终止阶段过渡协程并清除标志位。
/// 死亡时调用,防止 IsPhaseTransitioning 永久为 true 影响对象池复用。
/// </summary>
private void AbortPhaseTransition()
@@ -246,6 +246,12 @@ namespace BaseGames.Enemies
IsPhaseTransitioning = false;
}
/// <summary>
/// 阶段过渡开始时回调(子类可重写以触发演出动画或特殊逻辑)。
/// 在无敌帧等待之前调用。
/// </summary>
protected virtual void OnBeginPhaseTransition(int targetPhase) { }
/// <summary>检查当前 HP 是否低于指定百分比0~1。</summary>
public bool IsHPBelow(float ratio)
{

View File

@@ -0,0 +1,171 @@
using System.Collections;
using Animancer;
using BaseGames.Combat;
using BaseGames.Core;
using BaseGames.Core.Pool;
using UnityEngine;
using UnityEngine.Events;
namespace BaseGames.Enemies.Boss
{
/// <summary>
/// 嘲风 Boss 主脚本。
///
/// Phase 0地面4 技能加权随机(回旋扇/扇形连击/小龙卷/大龙卷)。
/// Phase 1空中风石技能 + 击落计数机制。
///
/// 阶段过渡流程:
/// 1. BossBase.BeginPhaseTransition → OnBeginPhaseTransition(1) 立即播放过渡动画 + 开始浮空
/// 2. 无敌期结束(≥ _riseDuration+buffer→ BossBase.EnterPhase(1) 广播阶段切换事件
///
/// ⚠️ 动画由本脚本通过 Animancer.Play() 完整控制,不在 BD 中调用 BD_PlayAnimation。
/// </summary>
public class ChaoFengBoss : BossBase
{
[Header("浮空 / 击落")]
[SerializeField] private ChaoFengFloatController _floatController;
[SerializeField] private ChaoFengKnockdownCounter _knockdownCounter;
[Header("阶段过渡动画")]
[SerializeField] private ClipTransition _phaseTransitionClip;
[Header("回旋扇收招动画")]
[SerializeField] private ClipTransition _boomerangEndClip;
[Header("弹体发射点")]
[SerializeField] private Transform _boomerangMuzzle;
[SerializeField] private Transform _tornadoMuzzle;
[SerializeField] private Transform _windStoneMuzzle;
[Header("击败演出动画")]
[SerializeField] private ClipTransition _defeatStruggleClip;
[Tooltip("倒地喘气(循环);与 ChaoFengKnockdownCounter._staggerClip 共用同一 Clip")]
[SerializeField] private ClipTransition _defeatPantClip;
[SerializeField] private ClipTransition _defeatStandUpClip;
[SerializeField] private float _defeatPantDuration = 3f;
[Header("白屏回调(可接 CameraManager / VFX Event")]
[SerializeField] private UnityEngine.Events.UnityEvent _onDefeatWhiteFlash;
// ── 阶段过渡钩子 ─────────────────────────────────────────────────────
/// <summary>
/// 阶段过渡开始时立即播放过渡动画并启动浮空协程。
/// invincibleDuration 需在 BD_BossPhaseTransition 中配置为 ≥ _riseDuration + 缓冲(约 2.0s)。
/// </summary>
protected override void OnBeginPhaseTransition(int targetPhase)
{
if (targetPhase == 1)
{
if (_phaseTransitionClip.Clip != null)
Animancer.Play(_phaseTransitionClip);
StartCoroutine(_floatController.FloatUp());
}
}
// ── 受击转发 ─────────────────────────────────────────────────────────
/// <summary>
/// 转发受击事件至击落计数器。
/// ⚠️ HurtBox 无公开 OnDamageTaken 事件,必须通过此虚方法转发。
/// </summary>
protected override void OnDamageTaken(DamageInfo info)
{
base.OnDamageTaken(info);
_knockdownCounter?.OnBossHit(info);
}
// ── 动画事件 ─────────────────────────────────────────────────────────
/// <summary>
/// 回旋扇返回时由 ReturnProjectile 调用,触发收扇动画。
/// </summary>
public void OnBoomerangReturned()
{
if (_boomerangEndClip.Clip != null)
Animancer.Play(_boomerangEndClip);
}
// ── 弹体生成 ─────────────────────────────────────────────────────────
/// <summary>
/// 由技能动画 AnimationEvent 触发,生成对应弹体。
/// payload: "boomerang" / "tornado_small" / "tornado_large" / "wind_stone"
/// </summary>
public override void SpawnProjectile(string payload)
{
var pool = ServiceLocator.GetOrDefault<IObjectPoolService>();
if (pool == null) return;
switch (payload)
{
case "boomerang":
{
var go = pool.Spawn("PROJ_Boomerang",
_boomerangMuzzle != null ? _boomerangMuzzle.position : transform.position,
Quaternion.identity);
go?.GetComponent<ReturnProjectile>()?.SetOwner(transform);
break;
}
case "tornado_small":
pool.Spawn("PROJ_TornadoSmall",
_tornadoMuzzle != null ? _tornadoMuzzle.position : transform.position,
Quaternion.identity);
break;
case "tornado_large":
if (PlayerTransform != null)
pool.Spawn("PROJ_TornadoLarge", PlayerTransform.position, Quaternion.identity);
break;
case "wind_stone":
pool.Spawn("PROJ_WindStone",
_windStoneMuzzle != null ? _windStoneMuzzle.position : transform.position,
Quaternion.identity);
break;
}
}
// ── 击败演出 ─────────────────────────────────────────────────────────
protected override void Die()
{
StartCoroutine(DefeatSequence());
}
private IEnumerator DefeatSequence()
{
StopBehaviorTree();
_knockdownCounter?.ForceEnd();
// Phase 2空中先落地
if (CurrentPhase >= 1 && _floatController != null)
yield return _floatController.FallDown();
// 空中挣扎Defeat_Struggle
if (_defeatStruggleClip.Clip != null)
{
Animancer.Play(_defeatStruggleClip);
yield return new WaitForSeconds(_defeatStruggleClip.Clip.length);
}
// 白屏效果
_onDefeatWhiteFlash?.Invoke();
// 倒地喘气Defeat_Pant 循环)
if (_defeatPantClip.Clip != null)
Animancer.Play(_defeatPantClip);
yield return new WaitForSeconds(_defeatPantDuration);
// 站起Defeat_StandUp 单次)
if (_defeatStandUpClip.Clip != null)
{
Animancer.Play(_defeatStandUpClip);
yield return new WaitForSeconds(_defeatStandUpClip.Clip.length);
}
// 广播战斗结束、触发结算过场
base.Die();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7eec9978ded028a409537e37d029c8cd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,46 @@
using System.Collections;
using UnityEngine;
namespace BaseGames.Enemies.Boss
{
/// <summary>
/// 嘲风漂浮控制器:负责 Phase 2 进入时上浮、击落时下落的平滑位移。
/// 使用 Rigidbody2D.MovePosition 做帧级 Lerp 插值,避免物理穿墙。
/// </summary>
public class ChaoFengFloatController : MonoBehaviour
{
[SerializeField] private float _floatHeight = 5f;
[SerializeField] private float _riseDuration = 1.5f;
[SerializeField] private float _fallDuration = 0.8f;
[SerializeField] private Rigidbody2D _rb;
private float _groundY;
private void Start() => _groundY = transform.position.y;
/// <summary>上浮至悬空高度(切换为 Kinematic 后执行)。</summary>
public IEnumerator FloatUp()
{
_rb.bodyType = RigidbodyType2D.Kinematic;
_rb.velocity = Vector2.zero;
yield return TweenY(transform.position.y, _groundY + _floatHeight, _riseDuration);
}
/// <summary>下落回地面(落地后恢复 Dynamic。</summary>
public IEnumerator FallDown()
{
yield return TweenY(transform.position.y, _groundY, _fallDuration);
_rb.bodyType = RigidbodyType2D.Dynamic;
}
private IEnumerator TweenY(float from, float to, float duration)
{
for (float t = 0f; t < duration; t += Time.deltaTime)
{
_rb.MovePosition(new Vector2(transform.position.x, Mathf.Lerp(from, to, t / duration)));
yield return null;
}
_rb.MovePosition(new Vector2(transform.position.x, to));
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 29caa23156f932e43b02015182e66819
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,78 @@
using System.Collections;
using Animancer;
using BaseGames.Boss;
using BaseGames.Combat;
using UnityEngine;
namespace BaseGames.Enemies.Boss
{
/// <summary>
/// 嘲风击落计数器Phase 2
/// 由 <see cref="ChaoFengBoss.OnDamageTaken"/> 直接调用 <see cref="OnBossHit"/>
/// 累计命中达到阈值后触发击落序列(下落 → 硬直 → 复位浮空)。
///
/// ⚠️ HurtBox 无公开 OnDamageTaken 事件,必须通过 EnemyBase.OnDamageTaken 虚方法转发。
/// </summary>
public class ChaoFengKnockdownCounter : MonoBehaviour
{
[SerializeField] private int _threshold = 8;
[Header("依赖引用")]
[SerializeField] private ChaoFengBoss _boss;
[SerializeField] private ChaoFengFloatController _floatCtrl;
[Header("击落动画")]
[SerializeField] private ClipTransition _knockdownHitClip;
[Tooltip("击落后硬直动画(复用 Defeat_Pant Clip")]
[SerializeField] private ClipTransition _staggerClip;
[SerializeField] private float _staggerDuration = 3f;
private int _count;
private bool _inKnockdown;
/// <summary>
/// 由 <see cref="ChaoFengBoss.OnDamageTaken"/> 调用,累计受击并在达到阈值时触发击落。
/// </summary>
public void OnBossHit(DamageInfo info)
{
if (_inKnockdown || _boss == null || _boss.CurrentPhase != 1) return;
_count++;
if (_count >= _threshold)
{
_count = 0;
StartCoroutine(KnockdownSequence());
}
}
/// <summary>强制结束正在进行中的击落序列(由 ChaoFengBoss.DefeatSequence 调用)。</summary>
public void ForceEnd()
{
StopAllCoroutines();
_inKnockdown = false;
_count = 0;
}
private IEnumerator KnockdownSequence()
{
_inKnockdown = true;
_boss.GetComponentInChildren<BossSkillExecutor>()?.InterruptCurrentSkill();
if (_knockdownHitClip.Clip != null)
_boss.Animancer.Play(_knockdownHitClip);
yield return _floatCtrl.FallDown();
if (_staggerClip.Clip != null)
_boss.Animancer.Play(_staggerClip);
yield return new WaitForSeconds(_staggerDuration);
yield return _floatCtrl.FloatUp();
_inKnockdown = false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4e0b39336c1ec5740abb2754b75bde04
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,61 @@
using UnityEngine;
using BaseGames.Combat;
namespace BaseGames.Enemies.Boss
{
/// <summary>
/// 嘲风回旋扇弹体:向前飞行至最大射程后自动追踪 Boss 返回。
/// 命中目标或返回到 Boss 身边时归还对象池,并通知 Boss 触发收扇动画。
/// </summary>
public class ReturnProjectile : Projectile
{
private enum Stage { Forward, Returning }
[SerializeField] private float _maxRange = 8f;
[SerializeField] private float _returnSpeed = 6f;
private Stage _stage;
private Transform _ownerTransform;
private Vector2 _startPos;
/// <summary>设置持有者 Transform发射时由 ChaoFengBoss.SpawnProjectile 调用)。</summary>
public void SetOwner(Transform owner) => _ownerTransform = owner;
protected override void OnInitialized()
{
_stage = Stage.Forward;
_startPos = transform.position;
_rb.velocity = Direction * _config.Speed;
}
protected override void Update()
{
// 不使用基类的 Lifetime 自毁;由射程 / 返回逻辑控制生命周期
if (_stage == Stage.Forward)
{
if (Vector2.Distance(transform.position, _startPos) >= _maxRange)
{
_stage = Stage.Returning;
_rb.velocity = Vector2.zero;
}
}
else // Returning
{
if (_ownerTransform == null)
{
base.ReturnToPool();
return;
}
Vector2 dir = ((Vector2)_ownerTransform.position - (Vector2)transform.position).normalized;
_rb.velocity = dir * _returnSpeed;
if (Vector2.Distance(transform.position, _ownerTransform.position) < 0.5f)
{
_ownerTransform.GetComponentInParent<ChaoFengBoss>()?.OnBoomerangReturned();
base.ReturnToPool();
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4b3290cb8ac8a2f42a10c7c0c845c8ef
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,34 @@
using System.Collections;
using Animancer;
using UnityEngine;
namespace BaseGames.Enemies
{
/// <summary>
/// E003 幼蛭。HP=1一击即死。
/// 支持双路初始化:
/// - 预置路径:场景战斗触发器调用 <see cref="ActivateFromCeiling"/>
/// - 对象池路径E005 死亡时通过 <see cref="OnSpawn"/> 自动触发
/// 能力脚本 <c>AnimatedCeilingDropAbility</c> 负责 Fall 动画 + 物理下落 + SetAiPhase(Patrol)。
/// </summary>
public class E003_YouZhi : EnemyBase
{
[Tooltip("对象池生成时是否立即执行下落能力E005 触发生成路径)")]
[SerializeField] private bool _activateOnSpawn = true;
public override void OnSpawn()
{
base.OnSpawn();
if (_activateOnSpawn)
Abilities.Get("e003_fall")?.Execute();
}
/// <summary>
/// 场景预置路径由场景触发器EventTrigger / Animator Event调用触发天花板跌落。
/// </summary>
public void ActivateFromCeiling()
{
Abilities.Get("e003_fall")?.Execute();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d86a36c2999f88842a212d095749c349
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,45 @@
using System.Collections;
using Animancer;
using BaseGames.Combat;
using UnityEngine;
namespace BaseGames.Enemies
{
/// <summary>
/// E004 蛭母小Boss
/// 特性:
/// - 出场演出AppearAbility
/// - 三技能(撕咬/头槌/酸液)+ 翻身FacePlayerAbility
/// - 死亡两阶段Death_Pre 无敌演出 → base.Die()
/// </summary>
public class E004_ZhiMu : EnemyBase
{
[Header("死亡演出")]
[SerializeField] private ClipTransition _deathPreClip;
[SerializeField] private HurtBox _hurtBox;
[SerializeField] private float _deathPreDuration = 3f;
protected override void Die()
{
StartCoroutine(DeathSequence());
}
private IEnumerator DeathSequence()
{
// 停止行为树防止覆盖演出动画
StopBehaviorTree();
StopMovement();
if (_hurtBox != null)
_hurtBox.enabled = false;
if (_deathPreClip.Clip != null)
{
Animancer.Play(_deathPreClip);
yield return new WaitForSeconds(_deathPreDuration);
}
base.Die();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cf8f8c7225dca9c42b5a451b177319b9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,66 @@
using System.Collections;
using Animancer;
using BaseGames.Combat;
using BaseGames.Core;
using BaseGames.Core.Pool;
using UnityEngine;
namespace BaseGames.Enemies
{
/// <summary>
/// E005 肥蛭(精英怪)。
/// 特性:
/// - 撕咬MeleeVulnerabilityAbility含后摇脆弱窗口
/// - 死亡两阶段Death_Pre 无敌演出(含 AnimationEvent spawn_e003→ base.Die()
/// </summary>
public class E005_FeiZhi : EnemyBase
{
[Header("死亡演出")]
[SerializeField] private ClipTransition _deathPreClip;
[SerializeField] private HurtBox _hurtBox;
[SerializeField] private float _deathPreDuration = 3f;
[Header("生成 E003Death_Pre AnimationEvent 触发)")]
[SerializeField] private int _spawnCount = 3;
[SerializeField] private float _spawnRadius = 1.5f;
/// <summary>
/// Death_Pre 动画适当帧的 AnimationEvent 调用 SpawnProjectile("spawn_e003") 生成幼蛭。
/// </summary>
public override void SpawnProjectile(string payload)
{
if (payload != "spawn_e003") return;
var pool = BaseGames.Core.ServiceLocator.GetOrDefault<IObjectPoolService>();
if (pool == null) return;
for (int i = 0; i < _spawnCount; i++)
{
Vector2 offset = Random.insideUnitCircle * _spawnRadius;
pool.Spawn("ENM_YouZhi", (Vector2)transform.position + offset, Quaternion.identity);
}
}
protected override void Die()
{
StartCoroutine(DeathSequence());
}
private IEnumerator DeathSequence()
{
StopBehaviorTree();
StopMovement();
if (_hurtBox != null)
_hurtBox.enabled = false;
if (_deathPreClip.Clip != null)
{
Animancer.Play(_deathPreClip);
yield return new WaitForSeconds(_deathPreDuration);
}
base.Die();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f2460e8735a4dc5409fe6b0949bd65c0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -13,6 +13,8 @@ namespace BaseGames.Enemies
public AnimationClip Idle;
public AnimationClip Walk;
public AnimationClip Run;
[Tooltip("转身动画(可选);配合 EnemyMovement._enableTurnAnimation 使用,留空则瞬时翻转")]
public AnimationClip Turn;
[Header("战斗")]
public AnimationClip Attack;
@@ -41,6 +43,7 @@ namespace BaseGames.Enemies
"Idle" => Idle,
"Walk" => Walk,
"Run" or "Chase" => Run,
"Turn" => Turn,
"Attack" or "Attack_Melee" => Attack,
"Hurt" => Hurt,
"Stagger" => Stagger,

View File

@@ -616,6 +616,17 @@ namespace BaseGames.Enemies
private float _btCurrentInterval;
#endif
/// <summary>
/// 停止行为树(子类 Die() 预演出阶段可调用,防止 BT 继续 Tick 覆盖演出逻辑)。
/// 内部使用 #if GRAPH_DESIGNER 保护,子类无需处理条件编译。
/// </summary>
protected void StopBehaviorTree()
{
#if GRAPH_DESIGNER
_behaviorTree?.StopBehavior();
#endif
}
protected virtual void Die()
{
if (_currentState == EnemyStateType.Dead) return;
@@ -702,7 +713,7 @@ namespace BaseGames.Enemies
protected virtual void OnValidate()
{
if (_statsSO == null)
Debug.LogError($"[EnemyBase] {gameObject.name} 缺少 EnemyStatsSO 配置运行时会 NullRef。", this);
Debug.LogWarning($"[EnemyBase] {gameObject.name} 缺少 EnemyStatsSO 配置运行时会 NullRef。", this);
if (_stats == null)
Debug.LogWarning($"[EnemyBase] {gameObject.name} 未绑定 EnemyStats 组件引用。", this);
if (_animancer == null)

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections;
using Animancer;
using UnityEngine;
namespace BaseGames.Enemies
@@ -22,6 +23,14 @@ namespace BaseGames.Enemies
[SerializeField] private EnemyStatsSO _config;
[SerializeField] private SpriteRenderer _spriteRenderer;
[Header("转身动画")]
[Tooltip("开启后,敌人翻转方向时播放转身动画并暂停水平移动,动画结束后完成翻转")]
[SerializeField] private bool _enableTurnAnimation = false;
[Tooltip("Animancer 组件引用;留空则在 Awake 时自动从父级查找")]
[SerializeField] private AnimancerComponent _animancer;
[Tooltip("动画配置 SO留空则在 Awake 时自动从 EnemyBase 读取")]
[SerializeField] private EnemyAnimationConfigSO _animConfig;
[Header("导航跳跃能力INavLinkHandler")]
[Tooltip("可处理的最大跳跃垂直高度(超出则让 TBM 兜底)")]
[SerializeField] private float _navJumpMaxHeight = 6f;
@@ -36,9 +45,16 @@ namespace BaseGames.Enemies
private int _facingDir = 1;
private Coroutine _linkCoroutine;
// ── 转身状态 ────────────────────────────────────────────────────────
private bool _isTurning;
private int _pendingFacingDir; // 转身目标方向,转身完成后 ApplyFacingFlip 使用
private Coroutine _turnCoroutine;
public bool IsGrounded { get; private set; }
/// <summary>当前朝向1 = 右,-1 = 左。</summary>
public int FacingDirection => _facingDir;
/// <summary>当前是否正在播放转身动画(移动输入在此期间被屏蔽)。</summary>
public bool IsTurning => _isTurning;
// ── INavLinkHandler ────────────────────────────────────────────
private static readonly NavLinkType[] _handledTypes =
@@ -59,6 +75,7 @@ namespace BaseGames.Enemies
public void BeginLinkTraversal(NavLinkType type, Vector2 linkStart, Vector2 linkEnd, Action onComplete)
{
CancelTurn(); // 进入连接段前中止任何进行中的转身
if (_linkCoroutine != null) StopCoroutine(_linkCoroutine);
_linkCoroutine = type == NavLinkType.Jump
? StartCoroutine(JumpLinkCoroutine(linkStart, linkEnd, onComplete))
@@ -68,6 +85,7 @@ namespace BaseGames.Enemies
public void AbortLinkTraversal()
{
if (_linkCoroutine != null) { StopCoroutine(_linkCoroutine); _linkCoroutine = null; }
CancelTurn();
StopHorizontal();
}
@@ -117,6 +135,16 @@ namespace BaseGames.Enemies
{
Debug.Assert(_config != null, "[EnemyMovement] _config 未赋值,请在 Prefab Inspector 中指定 EnemyStatsSO。", this);
_rb = GetComponent<Rigidbody2D>();
if (_enableTurnAnimation)
{
if (_animancer == null) _animancer = GetComponentInParent<AnimancerComponent>(true);
if (_animConfig == null)
{
var enemyBase = GetComponentInParent<EnemyBase>(true);
if (enemyBase != null) _animConfig = enemyBase.AnimConfig;
}
}
}
private void FixedUpdate()
@@ -124,30 +152,49 @@ namespace BaseGames.Enemies
IsGrounded = IsGroundedCheck();
}
/// <summary>按 SO 配置速度水平移动。dir: +1 右 / -1 左 / 0 停止。</summary>
/// <summary>按 SO 配置速度水平移动。dir: +1 右 / -1 左 / 0 停止。转身动画期间调用无效。</summary>
public void MoveHorizontal(float dir)
{
if (_isTurning) return;
var vel = _rb.velocity;
vel.x = dir * _config.WalkSpeed;
_rb.velocity = vel;
UpdateFacing(dir);
}
/// <summary>显式指定速度BD 追击任务调用)。</summary>
/// <summary>显式指定速度BD 追击任务调用)。转身动画期间调用无效。</summary>
public void MoveWithSpeed(float dir, float speed)
{
if (_isTurning) return;
var vel = _rb.velocity;
vel.x = dir * speed;
_rb.velocity = vel;
UpdateFacing(dir);
}
/// <summary>朝向指定世界坐标(通常传入玩家位置)。</summary>
public void FaceTarget(Vector2 targetPos)
{
float dir = targetPos.x < transform.position.x ? -1f : 1f;
UpdateFacing(dir);
}
/// <summary>
/// 直接指定朝向方向。dir: +1 = 右,-1 = 左。
/// 若启用转身动画且方向确实改变,会触发转身流程。
/// </summary>
public void FaceDirection(int dir)
{
if (dir == 0) return;
UpdateFacing(dir > 0 ? 1f : -1f);
}
/// <summary>朝向右方(+X。</summary>
public void FaceRight() => FaceDirection(1);
/// <summary>朝向左方(-X。</summary>
public void FaceLeft() => FaceDirection(-1);
public void ApplyKnockback(Vector2 dir, float force)
{
_rb.velocity = dir.normalized * force;
@@ -197,16 +244,68 @@ namespace BaseGames.Enemies
private void UpdateFacing(float dir)
{
if (Mathf.Approximately(dir, 0f)) return;
if (_isTurning) return; // 转身进行中,忽略新的朝向请求
int newDir = dir > 0f ? 1 : -1;
if (newDir == _facingDir) return;
_facingDir = newDir;
if (_spriteRenderer != null)
if (_enableTurnAnimation && _animancer != null && _animConfig?.Turn != null)
{
_spriteRenderer.flipX = newDir < 0;
// 启动转身协程:动画播完后再实际翻转
_pendingFacingDir = newDir;
if (_turnCoroutine != null) StopCoroutine(_turnCoroutine);
_turnCoroutine = StartCoroutine(TurnCoroutine(newDir));
}
else
{
// SpriteRenderer 未绑定时通过 localScale 翻转朝向
ApplyFacingFlip(newDir);
}
}
/// <summary>转身动画协程:停止水平移动 → 播放 Turn 动画 → 翻转朝向 → 恢复。</summary>
private IEnumerator TurnCoroutine(int newDir)
{
_isTurning = true;
StopHorizontal();
_animancer.Play(_animConfig.Turn);
float elapsed = 0f;
float duration = _animConfig.Turn.length;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
yield return null;
}
ApplyFacingFlip(newDir);
_isTurning = false;
_turnCoroutine = null;
}
/// <summary>
/// 立即中止进行中的转身协程,并将朝向应用到待转方向。
/// 受击、死亡、NavLink 穿越等外部中断时调用。
/// </summary>
public void CancelTurn()
{
if (_turnCoroutine == null) return;
StopCoroutine(_turnCoroutine);
_turnCoroutine = null;
if (_isTurning)
{
ApplyFacingFlip(_pendingFacingDir);
_isTurning = false;
}
}
/// <summary>真正执行朝向翻转(修改 SpriteRenderer.flipX 或 localScale。</summary>
private void ApplyFacingFlip(int newDir)
{
_facingDir = newDir;
if (_spriteRenderer != null)
_spriteRenderer.flipX = newDir < 0;
else
{
Vector3 s = transform.localScale;
transform.localScale = new Vector3(Mathf.Abs(s.x) * newDir, s.y, s.z);
}