using System.Collections; using UnityEngine; using BaseGames.Combat; using BaseGames.Core; using BaseGames.Core.Pool; namespace BaseGames.Enemies.Abilities { /// /// 远程射弹能力(扇形 / 弹幕)。 /// 每段攻击按 projectileFireT 触发,从池中生成 projectileCount 个抛射物, /// 按 spreadAngleDeg 均布角度。 /// public sealed class ProjectileAttackAbility : EnemyAbilityBase { [Header("发射点(为空则使用本物体位置)")] [SerializeField] private Transform _muzzle; [Header("行为")] [SerializeField] private bool _faceTargetOnStart = true; private IObjectPoolService _pool; protected override void Awake() { base.Awake(); if (_muzzle == null) _muzzle = _transform; } protected override IEnumerator ExecuteCoroutine() { _pool ??= ServiceLocator.GetOrDefault(); var seq = _config != null ? _config.attackSequence : null; if (seq == null || seq.Length == 0) yield break; if (_faceTargetOnStart && _enemy != null && _enemy.PlayerTransform != null) FaceTarget(_enemy.PlayerTransform); for (int i = 0; i < seq.Length; i++) { var atk = seq[i]; if (atk == null) continue; yield return PlayShot(atk); if (atk.postDelay > 0f) yield return EnemyAbilityWaits.Get(atk.postDelay); } } private IEnumerator PlayShot(EnemyAttackSO atk) { Phase = AbilityRunState.Active; float duration = atk.fallbackDuration; if (atk.clip != null && _animancer != null) { var state = _animancer.Play(atk.clip); if (state != null && state.Length > 0f) duration = state.Length; } float fireAbs = atk.projectileFireT * duration; float t = 0f; bool fired = false; while (t < duration) { t += Time.deltaTime; if (!fired && t >= fireAbs) { SpawnVolley(atk); fired = true; } yield return null; } if (!fired) SpawnVolley(atk); } private void SpawnVolley(EnemyAttackSO atk) { if (_pool == null || atk.projectileConfig == null) return; var cfg = atk.projectileConfig; if (string.IsNullOrEmpty(cfg.PoolKey)) return; Vector2 baseDir = GetAimDirection(); int count = Mathf.Max(1, atk.projectileCount); float spread = atk.spreadAngleDeg; float startAngle = -spread * 0.5f; float step = count > 1 ? spread / (count - 1) : 0f; var src = atk.damageSource != null ? atk.damageSource : cfg.DamageSource; int layer = gameObject.layer; for (int i = 0; i < count; i++) { float angle = startAngle + step * i; Vector2 dir = Rotate(baseDir, angle); var go = _pool.Spawn(cfg.PoolKey, _muzzle.position, Quaternion.identity); if (go == null) { Debug.LogWarning($"[ProjectileAttackAbility] 对象池 '{cfg.PoolKey}' 无法获取弹体,请检查池配置或容量。", this); continue; } var proj = go.GetComponent(); if (proj == null) { Debug.LogWarning($"[ProjectileAttackAbility] 弹体 '{go.name}' 缺少 Projectile 组件,请检查 Prefab 配置。", this); continue; } var info = DamageInfo.From(src, dir, _muzzle.position, layer, proj); proj.Initialize(cfg, info, dir, layer); } } private Vector2 GetAimDirection() { if (_enemy != null && _enemy.PlayerTransform != null) return ((Vector2)_enemy.PlayerTransform.position - (Vector2)_muzzle.position).normalized; return _transform.localScale.x >= 0f ? Vector2.right : Vector2.left; } private static Vector2 Rotate(Vector2 v, float degrees) { float rad = degrees * Mathf.Deg2Rad; float c = Mathf.Cos(rad), s = Mathf.Sin(rad); return new Vector2(v.x * c - v.y * s, v.x * s + v.y * c); } } }