using Assets.MyGame.Scripts.Net.APIResponses.Enum;
using Assets.MyGame.Scripts.Net.WebResponses;
using DG.Tweening;
using System;
using System.Collections;
using UnityEngine;
///
/// 机械蝎 Boss。
/// 死亡流程:正常打死 → 在死亡位置爆炸 → 范围内普通鱼死亡 → 加分滚动。
/// 通用爆奖模式,Boss 本体走标准 MonsterControl_BossBase.DieFish。
///
/// lua 对应:pw_enemy/PW_Scorpion.txt(本体生命周期)+ pw_effect/PW_Effect.txt DropTenCardEffect(八连抽)。
/// 本体 Spine 状态机(PW_Scorpion.txt):in(出生) → stand(站立,2.2s后) ⇄ attack(随机10~25s,需倒计时>5)
/// → out(离场/OnMoveFinish) → 爆炸(OnExplosion)。碰撞体由 MonsterContral 基类驱动。
/// 八连抽(DropTenCardEffect)见 JiXieXieEvent.cs。
///
/// C# 侧已对齐 lua 的 Spine 动画切换(OnMoveEnter 驱动 in→stand + 启动攻击循环 StartAttackCycle;DieFish 驱动 out),
/// 全部经 PlaySpineSafe 守卫:动画名缺失或 isSpine=0 时自动跳过(回退原 stand2),绝不抛异常。
/// ⚠️ 当前预制体 PW_Enemy_3000 的 isSpine=0,且 Spine 资源需含 in/stand/attack/out 动画名;
/// 美术接入 Spine 骨骼(isSpine=1 + 对应动画名)后此逻辑立即生效。
/// 音效时序(FreeCardBomb/FreeGet 等)已在 JiXieXieEvent.cs 对齐;死亡音效 isSelf 判定见 DieFish。
///
public class JiXieXie_BOSS : MonsterControl_BossBase
{
// ---- Spine 动画名(需与美术 Spine 资源动画名一致;不一致改这里即可)----
private const string AnimIn = "in";
private const string AnimStand = "stand";
private const string AnimAttack = "attack";
private const string AnimOut = "out";
// ---- 攻击节奏(对齐 lua:随机 10~25s 间隔,attack 持续 2s,出生 2.2s 后转 stand)----
// ⚠️ 攻击循环用 DOTween 递归计时驱动,从 OnMoveEnter 启动(进入 move 状态即触发,不依赖 isMove)。
// 基类 MonsterContral.OnUpdate 对 isMove=false 的固定 Boss 会在分发处直接 return,
// 导致 OnMoveUpdata 永不被调用——故攻击计时不能放在 OnMoveUpdata 里。
private Tween _attackTween;
private bool _attackRunning;
// ================================================================
// 本体特效(预制体 PW_Enemy_3000 里已作为子物体挂好、默认隐藏,由代码按 lua 时序激活)
// lua 对应(pw_enemy/PW_Scorpion.txt,原版用 MonentaryEffect 临时创建,time 到就销毁):
// PW_Effect_Scorpion_Born → 出场那一帧(dotIndex ≥ showDotIndex)创建,time=3
// PW_Effect_Scorpion_Attack → 进入攻击动作时创建(原版还要求"出场倒计时>5"),time=3.5
// PW_Effect_Scorpion_Out → 到点离场 OnMoveFinish 时创建,time=2(与本类 AutoExit 的 2s 延迟一致)
// PW_Effect_Explosion_Scorpion → 被击杀 OnExplosion 时创建,time=1(本类 DieFish)
// ⚠️ 四个子物体在 PW_Enemy_3000.prefab 里 m_IsActive=0,不激活就看不到;这里只在对应时机 SetActive。
// ================================================================
[Header("本体特效(留空则按名字自动查找子物体)")]
[Tooltip("出场特效 PW_Effect_Scorpion_Born(激活后 3s 自动隐藏,对齐 lua time=3)")]
public GameObject bornEffect;
[Tooltip("攻击特效 PW_Effect_Scorpion_Attack(激活后 3.5s 自动隐藏,对齐 lua time=3.5)")]
public GameObject attackEffect;
[Tooltip("离场特效 PW_Effect_Scorpion_Out(激活后一直显示到本体回收,对齐 lua time=2)")]
public GameObject outEffect;
[Tooltip("被杀爆炸特效 PW_Effect_Explosion_Scorpion(激活后 1s 自动隐藏,对齐 lua time=1)")]
public GameObject explosionEffect;
private const string BornEffectName = "PW_Effect_Scorpion_Born";
private const string AttackEffectName = "PW_Effect_Scorpion_Attack";
private const string OutEffectName = "PW_Effect_Scorpion_Out";
private const string ExplosionEffectName = "PW_Effect_Explosion_Scorpion";
private const float BornEffectTime = 3f; // lua MonentaryEffect time=3
private const float AttackEffectTime = 3.5f; // lua MonentaryEffect time=3.5
private const float ExplosionEffectTime = 1f; // lua MonentaryEffect time=1
private bool _bornEffectPlayed; // 出场特效只播一次(防 OnMoveEnter 重复触发)
private Tween _bornEffectTween; // 出场特效自动隐藏计时
private Tween _attackEffectTween; // 攻击特效自动隐藏计时
// 爆炸特效脱离本体时的原父节点 + 原始局部变换(1s 后挂回时还原,保证对象池复用结构完整)
private Tween _explosionEffectTween;
private Transform _explosionEffectParent;
private Vector3 _explosionEffectLocalPos;
private Quaternion _explosionEffectLocalRot;
private Vector3 _explosionEffectLocalScale;
// ================================================================
// 退场倒计时 UI(对齐 lua pw_view/PW_ScorpionTimerView.txt)
// lua:OnCreate → ScorpionCountDown(ratio) 打开视图 + SetTimerText(秒数) + EnterAction()
// 每秒 TimerUpdate() 递减;attack 需 scorepionTimerNum > 5 才播表现
// OnMoveFinish / OnExplosion → ExitAction()(从右侧滑出)
// ⚠️ 原预制体的文字/背景组件是缺失脚本(原版自定义组件未导入本项目),
// 所以这里运行时给 mTextName / mTextTimer 补上 TMP 组件再填字。
// ================================================================
[Header("退场倒计时 UI(留空则不显示倒计时,攻击表现也不受 5 秒门限限制)")]
[Tooltip("退场倒计时 UI 预制体(Art/XJMH/pw/pw_prefab/view/PW_ScorpionTimerView)")]
public GameObject timerViewPrefab;
[Tooltip("倒计时文字字体(留空用 TMP_Settings.defaultFontAsset)")]
public TMPro.TMP_FontAsset timerFont;
[Tooltip("“退场倒计时”标题字号")]
public float timerLabelFontSize = 22f;
[Tooltip("倒计时数字字号(MM : SS)")]
public float timerValueFontSize = 26f;
private const string TimerViewNodeName = "mGONode";
private const string TimerViewLabelName = "mTextName";
private const string TimerViewTextName = "mTextTimer";
private const string TimerViewLabel = "退场倒计时"; // 原版取 GC.Config.DialWeapon.Text12
private const float TimerViewStartX = 0f; // lua EnterAction: localPosition = (0, 50)
private const float TimerViewStartY = 50f;
private const float TimerViewEnterDeltaX = -250f; // lua EnterAction: localMoveBy(-250, 50, 0.5, EOutBack)
private const float TimerViewEnterDeltaY = 50f;
private const float TimerViewEnterTime = 0.5f;
private const float TimerViewExitDeltaX = 250f; // lua ExitAction: localMoveBy(250, 50, 0.3, EInBack)
private const float TimerViewExitDeltaY = 50f;
private const float TimerViewExitTime = 0.3f;
private const int AttackVisualMinSeconds = 5; // lua:倒计时 > 5 才播攻击动画/特效/音效
private GameObject _timerView; // 倒计时 UI 实例(关闭时立刻置空,销毁交给滑出 tween + DOVirtual 兜底)
private RectTransform _timerGONode; // 参与滑入/滑出的节点(mGONode)
private TMPro.TextMeshProUGUI _timerText;
private int _timerSeconds; // 当前倒计时秒数(= 距离自动离场的剩余秒数)
private bool _timerActive; // 倒计时是否生效(决定攻击表现的 5 秒门限)
private Tween _timerTickTween; // 每秒递减计时
private Tween _timerMoveTween; // 滑入/滑出动画
private Tween _attackSoundTween; // 攻击音效(动作结束前 1 秒播)
// ================================================================
// 受击抖动(对齐 lua Scorpion:OnHit + HitUpdate)
// OnHit:置 beHit 标记(防重入);HitUpdate:0.2s 内每帧把 body 的 localPosition
// 随机偏移 x=rand(-100,100)*0.0003、y=rand(-50,50)*0.0006,结束后还原。
// C# 侧由 MonsterContral.OnHurt()(子弹命中时调用,virtual 可 override)驱动,不用改公共代码。
// ================================================================
private const string BodyNodeName = "Body";
private const float HitJitterDuration = 0.2f; // lua beHitT >= 0.2
private const float HitJitterScaleX = 0.0003f; // lua math.random(-100,100) * 0.0003
private const float HitJitterScaleY = 0.0006f; // lua math.random(-50,50) * 0.0006
private Transform _bodyTf; // Body 节点(抖动对象,缓存原始 localPosition)
private Vector3 _bodyOriginLocalPos;
private bool _hitJittering; // 正在抖动(= lua beHit)
private Coroutine _hitJitterRoutine;
// ---- 自动退出(对齐 lua OnMoveFinish:未被击杀也到点离场)----
// ⚠️ 机械蝎 isMove=false 固定停在 (0,0,0),不走路线、OnMoveUpdata 永不触发路线终点检测,
// 且服务端 SyncGameData.remainingSeconds 未接上销毁逻辑 → 不被打死就永远不消失。
// 故用可配置存活计时器兜底:到点播 out(若 Spine 含)后回收。<=0 表示永不离场(纯靠击杀)。
[Header("自动退出:未被击杀时的存活时长(秒),<=0 永不自动离场")]
public float bossLifeTime = 30f;
private Tween _exitTween;
private bool _isExiting;
private const float AttackIntervalMin = 10f;
private const float AttackIntervalMax = 25f;
private const float AttackDuration = 2f;
private const float BornToStandDelay = 2.2f;
public override void OnEnable()
{
base.OnEnable();
_attackRunning = false;
_bornEffectPlayed = false;
_timerSeconds = 0;
_timerActive = false;
// ⭐ 对象池复用:先解析三个本体特效子物体,再统一复位为隐藏(避免上次残留一直亮着)
ResolveBodyEffects();
ResetBodyEffects();
// ⭐ 受击抖动:先还原上一轮残留,再缓存 Body 原始 localPosition
StopHitJitter();
ResolveBodyTransform();
// 音效:蝎子语音(PW_TextScorpion)
AudioManager.Instance.PlayerFXByName("PW_TextScorpion");
}
public override void DieFish(RoomKillNotice obj = null)
{
StopAttackCycle(); // 死亡即停止攻击循环,避免对已销毁对象触发动画
StopExitTimer(); // 取消自动退出计时,避免击杀回收后定时器再触发一次回收
base.DieFish(obj);
// ⭐ 被杀爆炸特效(lua OnExplosion 第一步:爆点创建 PW_Effect_Explosion_Scorpion,time=1)
PlayExplosionEffect();
// ⭐ 倒计时 UI 滑出(lua OnExplosion → ScorpionTimer:ExitAction)
CloseTimerView(false);
// 离场/死亡动画(对齐 lua out;非 Spine 或无 out 动画时自动跳过)
PlaySpineSafe(AnimOut, false);
// 音效:蝎子死亡 + 死亡语音(PW_ScorpionDie / PW_ScorpionDieVoice)
// lua OnExplosion:仅当击杀者为自己(isSelf)才播;本地用户不可用时回退为必播(保留原行为)
bool isSelfKill = GameManager.Instance != null && obj != null && obj.userId == GameManager.Instance.user.id;
if (isSelfKill || GameManager.Instance == null)
{
AudioManager.Instance.PlayerFXByName("PW_ScorpionDie");
AudioManager.Instance.PlayerFXByName("PW_ScorpionDieVoice");
}
// 标准爆奖事件初始化
action_effect_DieAward = (o) =>
{
// 爆奖预制体已由 CreateFishManager 在 monster.transform.position(死亡位置)创建
Vector3 diePos = transform.position;
Debug.Log($"[JiXieXie] action_effect_DieAward: 鱼位置={diePos} 爆奖预制体位置={o.transform.position}");
// 现在回收 Boss 本体
CreateFishManager.Instance.Id_DieFish_NotFX(guid);
if (obj == null) return;
foreach (var item in CreateFishManager.Instance.currGameType.list_Sits)
{
if (item.user == obj.userId)
{
var v = o.GetComponent();
// 先设置死亡位置,再 Init(Init 里会用 explosionCenter 恢复位置)
var evt = v as JiXieXieEvent;
if (evt != null)
{
evt.explosionCenter = diePos;
Debug.Log($"[JiXieXie] 设置爆炸中心(死亡位置)={diePos}");
}
v.Init(
obj.gold, // goldCount
item.posID, // posID(击杀者座位)
obj.skillsGold, // paobei(炮倍)
obj.fishScore, // fishScore(底分)
obj.beishu, // beishu(总倍数)
JiaFen, // action(加分回调)
() => { v.obj = obj.param; } // 把服务端爆奖参数交给事件类
);
}
}
};
}
// ================================================================
// 受击抖动(对齐 lua Scorpion:OnHit / HitUpdate)
// ================================================================
/// 子弹命中回调(MonsterContral.OnHurt,由 BullitMove 命中鱼时调用,virtual 可直接 override)。
public override void OnHurt()
{
base.OnHurt(); // 保留基类受击表现(hurt 子物体闪一下)
// lua HitUpdate:isOut(离场中)不抖 → 这里死亡/离场中同样跳过
if (isDieFish || _isExiting) return;
if (_hitJittering) return; // lua: if self.beHit then return(0.2s 内不重复触发)
if (_bodyTf == null) ResolveBodyTransform();
if (_bodyTf == null) return;
_hitJitterRoutine = StartCoroutine(HitJitterRoutine());
}
/// 0.2s 内每帧随机偏移 Body 的 localPosition,结束后还原(对齐 lua Scorpion:HitUpdate)
private IEnumerator HitJitterRoutine()
{
_hitJittering = true;
float t = 0f;
while (t < HitJitterDuration)
{
if (_bodyTf == null) break;
t += Time.deltaTime;
_bodyTf.localPosition = _bodyOriginLocalPos + new Vector3(
UnityEngine.Random.Range(-100f, 100f) * HitJitterScaleX,
UnityEngine.Random.Range(-50f, 50f) * HitJitterScaleY,
0f);
yield return null;
}
if (_bodyTf != null) _bodyTf.localPosition = _bodyOriginLocalPos; // 还原(预制体 Body.y=0.74,不能写死 0)
_hitJittering = false;
_hitJitterRoutine = null;
}
/// 解析 Body 节点并缓存其原始 localPosition(预制体里 Body 带偏移,还原必须用原值)
private void ResolveBodyTransform()
{
Transform t = transform.Find(BodyNodeName);
if (t == null) t = FindChildRecursive(transform, BodyNodeName);
_bodyTf = t;
if (_bodyTf != null)
_bodyOriginLocalPos = _bodyTf.localPosition;
else
Debug.LogWarning($"[JiXieXie] 找不到 {BodyNodeName} 节点,受击抖动跳过");
}
/// 停止受击抖动并把 Body 位置还原(回池/销毁时调用,防抖动残留)
private void StopHitJitter()
{
if (_hitJitterRoutine != null)
{
StopCoroutine(_hitJitterRoutine);
_hitJitterRoutine = null;
}
if (_hitJittering && _bodyTf != null)
_bodyTf.localPosition = _bodyOriginLocalPos;
_hitJittering = false;
}
// ================================================================
// Spine 动画切换(对齐 lua PW_Scorpion.txt 的 SkeletonAnimation 状态机)
// 全部经 PlaySpineSafe / HasSpineAnim 守卫:动画名缺失或 isSpine=0 时自动跳过
// (回退原 stand2 或保持现状),绝不抛 Spine ArgumentException。
// ⚠️ 当前预制体 PW_Enemy_3000 的 isSpine=0 且 Spine 资源需含 in/stand/attack/out 动画名;
// 美术接入 Spine 骨骼(isSpine=1 + 对应动画名)后此逻辑立即生效。
// ================================================================
/// 出生动画序列:in(一次) → 2.2s 后 stand(循环)。无 in 动画时回退基类 stand2。
/// 同时启动攻击循环(OnMoveEnter 在状态切到 move 时必被调用,不受 isMove 闸门影响)。
public override void OnMoveEnter()
{
if (HasSpineAnim(AnimIn))
{
PlaySpineSafe(AnimIn, false);
DOVirtual.DelayedCall(BornToStandDelay, () => PlaySpineSafe(AnimStand, true));
}
else
{
base.OnMoveEnter();
}
StartAttackCycle();
StartExitTimer();
// ⭐ 出场特效(lua:出现在路线点那一帧创建 PW_Effect_Scorpion_Born,time=3)
if (!_bornEffectPlayed)
{
_bornEffectPlayed = true;
PlayBornEffect();
}
// ⭐ 退场倒计时 UI(lua OnCreate → ScorpionCountDown:开视图 + EnterAction + 每秒递减)
OpenTimerView();
}
/// 对象回收/离场时停止攻击与退出计时,避免对已销毁对象触发(DOTween 回调安全守卫)。
public override void OnDisable()
{
StopAttackCycle();
StopExitTimer();
ResetBodyEffects(); // ⭐ 回池时把三个本体特效连同计时一起复位,防复用残留
StopHitJitter(); // ⭐ 受击抖动协程 + Body 位置还原
// ⭐ 倒计时 UI:没走 DieFish/AutoExit 的滑出就直接销毁(房间关闭/被动回收也保证不残留)
if (_timerView != null) CloseTimerView(true);
KillTween(ref _attackSoundTween);
base.OnDisable();
}
// ================================================================
// 自动退出(对齐 lua OnMoveFinish:未被击杀也到点离场)
// ================================================================
/// 启动存活计时:到点触发 AutoExit。bossLifeTime<=0 不自动离场(纯靠击杀)。
private void StartExitTimer()
{
if (bossLifeTime <= 0f) return;
_isExiting = false;
if (_exitTween != null && _exitTween.IsActive()) _exitTween.Kill();
_exitTween = DOVirtual.DelayedCall(bossLifeTime, AutoExit);
}
/// 到点离场:停攻击循环 → 播 out(若 Spine 含)→ 延迟回收本体(同 lua OnMoveFinish→DestroyEnemy)。
private void AutoExit()
{
if (_isExiting) return;
_isExiting = true;
StopAttackCycle();
float delay = 0f;
if (HasSpineAnim(AnimOut))
{
PlaySpineSafe(AnimOut, false);
delay = 2f; // 对齐 lua PW_Effect_Scorpion_Out 的 time=2
}
// ⭐ 离场特效(lua OnMoveFinish:创建 PW_Effect_Scorpion_Out,time=2,播完 DestroyEnemy)
PlayOutEffect();
// ⭐ 倒计时 UI 滑出(lua OnMoveFinish → ScorpionTimer:ExitAction)
CloseTimerView(false);
DOVirtual.DelayedCall(delay, () =>
{
if (this == null) return;
if (CreateFishManager.Instance != null)
CreateFishManager.Instance.Id_DieFish_NotFX(guid);
});
}
private void StopExitTimer()
{
_isExiting = false;
if (_exitTween != null && _exitTween.IsActive())
_exitTween.Kill();
_exitTween = null;
}
/// 攻击循环:随机 10~25s 间隔播放 attack(一次,2s) 后回 stand(对齐 lua stand⇄attack)。
/// 用 DOTween 递归计时,不依赖 OnMoveUpdata / isMove。
/// lua 原逻辑 attack 还需“出场倒计时>5”才触发,C# 侧无该倒计时阶段,故简化为纯随机间隔。
private void StartAttackCycle()
{
if (!HasSpineAnim(AnimAttack)) return;
if (_attackRunning) return; // 防止 OnMoveEnter 重复触发导致多套循环
_attackRunning = true;
ScheduleNextAttack();
}
private void ScheduleNextAttack()
{
if (!_attackRunning) return;
float delay = UnityEngine.Random.Range(AttackIntervalMin, AttackIntervalMax);
_attackTween = DOVirtual.DelayedCall(delay, PlayAttackOnce);
}
private void PlayAttackOnce()
{
if (!_attackRunning) return;
// ⭐ 原版(PW_Scorpion.txt):只有"出场倒计时 > 5"时才播 attack 动画 + 攻击特效 + 攻击音效;
// 倒计时 ≤5 时只走状态记账(isAttack / actionChangeTime),不做任何表现
if (CanAttackVisual)
{
PlaySpineSafe(AnimAttack, false);
// ⭐ 攻击特效(lua:进入攻击动作时创建 PW_Effect_Scorpion_Attack,time=3.5)
PlayAttackEffect();
// 原版:isAttackSound && actionChangeTime <= 1 时补播攻击音效(= 动作结束前 1 秒)
KillTween(ref _attackSoundTween);
_attackSoundTween = DOVirtual.DelayedCall(Mathf.Max(0f, AttackDuration - 1f), PlayAttackSound);
}
else
{
Debug.Log($"[JiXieXie] 退场倒计时 {_timerSeconds}s ≤ {AttackVisualMinSeconds}s:本次攻击不播表现(对齐 lua)");
}
_attackTween = DOVirtual.DelayedCall(AttackDuration, () =>
{
if (!_attackRunning) return;
PlaySpineSafe(AnimStand, true);
ScheduleNextAttack();
});
}
/// 是否允许攻击表现:有倒计时时需 > 5 秒(对齐 lua scorepionTimerNum > 5);无倒计时时始终允许
private bool CanAttackVisual => !_timerActive || _timerSeconds > AttackVisualMinSeconds;
/// 攻击音效(对齐 lua ScorpionAttack + ScorpionAttackVoice,动作结束前 1 秒播)
private void PlayAttackSound()
{
_attackSoundTween = null;
AudioManager.Instance.PlayerFXByName("PW_ScorpionAttack");
AudioManager.Instance.PlayerFXByName("PW_ScorpionAttackVoice");
}
private void StopAttackCycle()
{
_attackRunning = false;
if (_attackTween != null && _attackTween.IsActive())
_attackTween.Kill();
_attackTween = null;
}
// ================================================================
// 本体特效(Born / Attack / Out)
// 预制体 PW_Enemy_3000 里三个特效已作为根节点子物体挂好(默认 m_IsActive=0),
// 代码只负责在 lua 对应的时机 SetActive + 到点隐藏。
// ================================================================
///
/// 解析三个本体特效子物体:Inspector 没拖时按名字查找(先根节点直接找,再递归忽略大小写兜底)。
///
private void ResolveBodyEffects()
{
if (bornEffect == null) bornEffect = FindEffectByName(BornEffectName);
if (attackEffect == null) attackEffect = FindEffectByName(AttackEffectName);
if (outEffect == null) outEffect = FindEffectByName(OutEffectName);
if (explosionEffect == null) explosionEffect = FindEffectByName(ExplosionEffectName);
if (bornEffect == null && attackEffect == null && outEffect == null && explosionEffect == null)
Debug.LogWarning("[JiXieXie] 四个本体特效都没找到" +
"(预制体应含 PW_Effect_Scorpion_Born/Attack/Out 与 PW_Effect_Explosion_Scorpion 子物体)");
}
/// 按名字查找特效子物体(找不到返回 null)
private GameObject FindEffectByName(string effectName)
{
if (string.IsNullOrEmpty(effectName)) return null;
Transform t = transform.Find(effectName);
if (t == null) t = FindChildRecursive(transform, effectName);
return t != null ? t.gameObject : null;
}
/// 递归查找子物体(忽略大小写,防改名/挪层级后找不到)
private static Transform FindChildRecursive(Transform root, string childName)
{
if (root == null) return null;
for (int i = 0; i < root.childCount; i++)
{
Transform c = root.GetChild(i);
if (string.Equals(c.name, childName, StringComparison.OrdinalIgnoreCase)) return c;
Transform r = FindChildRecursive(c, childName);
if (r != null) return r;
}
return null;
}
/// 出场特效:激活 → BornEffectTime(3s) 后自动隐藏(对齐 lua MonentaryEffect time=3)
private void PlayBornEffect()
{
if (bornEffect == null) return;
KillTween(ref _bornEffectTween);
bornEffect.SetActive(true);
_bornEffectTween = DOVirtual.DelayedCall(BornEffectTime, () =>
{
if (bornEffect != null) bornEffect.SetActive(false);
_bornEffectTween = null;
});
Debug.Log($"[JiXieXie] 出场特效 {BornEffectName} 播放({BornEffectTime}s 后隐藏)");
}
/// 攻击特效:激活 → AttackEffectTime(3.5s) 后自动隐藏(对齐 lua MonentaryEffect time=3.5)
private void PlayAttackEffect()
{
if (attackEffect == null) return;
KillTween(ref _attackEffectTween);
attackEffect.SetActive(true);
_attackEffectTween = DOVirtual.DelayedCall(AttackEffectTime, () =>
{
if (attackEffect != null) attackEffect.SetActive(false);
_attackEffectTween = null;
});
Debug.Log($"[JiXieXie] 攻击特效 {AttackEffectName} 播放({AttackEffectTime}s 后隐藏)");
}
/// 离场特效:激活后一直显示到本体回收(lua Out time=2,与 AutoExit 的 2s 延迟一致)
private void PlayOutEffect()
{
if (outEffect == null) return;
outEffect.SetActive(true);
Debug.Log($"[JiXieXie] 离场特效 {OutEffectName} 播放");
}
///
/// 被杀爆炸特效(lua OnExplosion:在死亡位置创建 PW_Effect_Explosion_Scorpion,time=1)。
///
/// ⚠️ 它是本体的子物体,而本类 DieFish 里 Boss 当帧就会被回收(回池会连带停掉子物体),
/// 所以先把特效脱离父节点(保留世界变换)让它能完整播完 1 秒,
/// 1 秒后再挂回本体 + 还原预制体原始局部变换 + 隐藏,保证对象池复用时结构完整。
/// 爆炸特效不放进 ResetBodyEffects(),避免回池时把它一起关掉。
///
private void PlayExplosionEffect()
{
if (explosionEffect == null) return;
// 上一次爆炸还没收尾(1s 内又死一次)→ 先收尾,避免层级被越挂越上
KillTween(ref _explosionEffectTween);
RestoreExplosionEffectParent();
Transform t = explosionEffect.transform;
_explosionEffectParent = t.parent; // 原父节点 = 本体根节点
_explosionEffectLocalPos = t.localPosition; // 预制体原始局部变换,挂回时还原
_explosionEffectLocalRot = t.localRotation;
_explosionEffectLocalScale = t.localScale;
Transform host = _explosionEffectParent != null ? _explosionEffectParent.parent : null;
t.SetParent(host, true); // ⭐ 脱离本体:世界变换不变,回池时不被一起停掉
explosionEffect.SetActive(true);
Debug.Log($"[JiXieXie] 被杀爆炸特效 {ExplosionEffectName} 播放({ExplosionEffectTime}s 后回收)");
_explosionEffectTween = DOVirtual.DelayedCall(ExplosionEffectTime, () =>
{
_explosionEffectTween = null;
RestoreExplosionEffectParent();
});
}
/// 爆炸特效收尾:隐藏 + 挂回本体并还原原始局部变换(计时回调/重复触发前共用)
private void RestoreExplosionEffectParent()
{
if (explosionEffect == null) return;
explosionEffect.SetActive(false);
Transform t = explosionEffect.transform;
if (_explosionEffectParent == null)
{
// 本体已销毁:脱离出来的实例没人回收了,直接删掉防残留
Destroy(explosionEffect);
return;
}
if (t.parent == _explosionEffectParent) return;
t.SetParent(_explosionEffectParent, false);
t.localPosition = _explosionEffectLocalPos;
t.localRotation = _explosionEffectLocalRot;
t.localScale = _explosionEffectLocalScale;
}
// ================================================================
// 退场倒计时 UI(对齐 lua pw_view/PW_ScorpionTimerView.txt)
// ================================================================
///
/// 打开/重建退场倒计时 UI(lua OnCreate → ScorpionCountDown + EnterAction)。
/// 倒计时秒数 = 距离自动离场(bossLifeTime)的剩余秒数;bossLifeTime<=0 时不显示。
///
private void OpenTimerView()
{
if (timerViewPrefab == null)
{
Debug.LogWarning("[JiXieXie] timerViewPrefab 未赋值(PW_ScorpionTimerView)," +
"退场倒计时不显示(攻击表现也不受 5 秒门限限制)");
return;
}
if (bossLifeTime <= 0f)
{
Debug.LogWarning("[JiXieXie] bossLifeTime<=0(不自动离场),退场倒计时不显示");
return;
}
if (_timerView != null) CloseTimerView(true); // 复用残留先清掉
Transform parent = ResolveTimerUiParent();
if (parent == null)
{
Debug.LogWarning("[JiXieXie] 找不到 UI 层(subPanelParent / uiRoot),退场倒计时不显示");
return;
}
_timerView = Instantiate(timerViewPrefab, parent);
_timerView.name = "PW_ScorpionTimerView";
Transform node = FindChildRecursive(_timerView.transform, TimerViewNodeName);
_timerGONode = node as RectTransform;
if (_timerGONode == null)
Debug.LogWarning($"[JiXieXie] 倒计时 UI 里找不到 {TimerViewNodeName},滑入/滑出动画跳过");
// ⭐ 原预制体的文字组件是缺失脚本(原版自定义组件未导入本项目),这里运行时补 TMP 再填字
Transform labelTf = FindChildRecursive(_timerView.transform, TimerViewLabelName);
Transform textTf = FindChildRecursive(_timerView.transform, TimerViewTextName);
EnsureTmpText(labelTf, timerLabelFontSize, TimerViewLabel);
_timerText = EnsureTmpText(textTf, timerValueFontSize, null);
_timerSeconds = Mathf.CeilToInt(bossLifeTime);
_timerActive = true;
SetTimerText(_timerSeconds);
ScheduleTimerTick();
PlayTimerEnter(); // lua EnterAction
Debug.Log($"[JiXieXie] 退场倒计时开始:{_timerSeconds}s(剩余 ≤{AttackVisualMinSeconds}s 时不再攻击)");
}
///
/// 关闭倒计时 UI(lua ExitAction:向右滑出)。
///
/// ⭐ 这里是"倒计时整块一定会消失"的唯一保证点,改成了"必定销毁":
/// 旧写法把销毁挂在滑出 tween 的 OnComplete 上,而且"已在滑出中就直接 return",
/// 只要那根 tween 在 0.3s 内被中途 Kill(滑出期间 Boss 被复用、再次 OpenTimerView、
/// 或任何 KillTween(ref _timerMoveTween)),OnComplete 就永不执行 →
/// 倒计时整块永久残留在屏幕上(鱼死了也不消失)。
/// 现在:立刻摘掉字段引用 + 用不挂在任何对象上的 DOVirtual 兜底计时,到点无条件销毁。
/// 顺带修掉"旧 tween 的 OnComplete 误销毁新视图"的隐患(引用改成本地捕获,不再读字段)。
///
private void CloseTimerView(bool instant)
{
KillTween(ref _timerTickTween);
KillTween(ref _attackSoundTween);
_timerActive = false;
var view = _timerView;
if (view == null)
{
_timerGONode = null;
_timerText = null;
return;
}
var node = _timerGONode;
// ⭐ 先摘掉所有字段引用:之后不管是复用、回池还是再次开倒计时,都不会再操作这块 UI
_timerView = null;
_timerGONode = null;
_timerText = null;
KillTween(ref _timerMoveTween);
if (instant || node == null)
{
Destroy(view);
return;
}
// 滑出动画(lua ExitAction:localMoveBy(250, 50, 0.3, EInBack))
_timerMoveTween = node
.DOAnchorPos(node.anchoredPosition + new Vector2(TimerViewExitDeltaX, TimerViewExitDeltaY), TimerViewExitTime)
.SetEase(Ease.InBack);
// ⭐ 兜底销毁:DOVirtual 不挂在任何对象上,Boss 被禁用/回收/销毁都不影响它,到点无条件销毁
var viewToDestroy = view;
DOVirtual.DelayedCall(TimerViewExitTime + 0.1f, () =>
{
if (viewToDestroy != null) Destroy(viewToDestroy);
});
}
/// 滑入动画(lua EnterAction:先归位 (0,50),再 localMoveBy(-250, 50, 0.5, EOutBack))
private void PlayTimerEnter()
{
if (_timerGONode == null) return;
Vector2 start = new Vector2(TimerViewStartX, TimerViewStartY);
_timerGONode.anchoredPosition = start;
KillTween(ref _timerMoveTween);
_timerMoveTween = _timerGONode
.DOAnchorPos(start + new Vector2(TimerViewEnterDeltaX, TimerViewEnterDeltaY), TimerViewEnterTime)
.SetEase(Ease.OutBack);
}
/// 每秒递减(lua Update:deltaTimeNum 累积 ≥1 → TimerUpdate)
private void ScheduleTimerTick()
{
KillTween(ref _timerTickTween);
_timerTickTween = DOVirtual.DelayedCall(1f, TimerTick);
}
private void TimerTick()
{
_timerTickTween = null;
if (!_timerActive || _timerView == null) return;
_timerSeconds--;
if (_timerSeconds >= 0) SetTimerText(_timerSeconds);
if (_timerSeconds > 0) ScheduleTimerTick();
}
/// 刷新倒计时文本(lua SetTimerText:≤0 → "00 : 00",否则 "MM : SS" 补零)
private void SetTimerText(int seconds)
{
if (_timerText == null) return;
if (seconds <= 0)
{
_timerText.text = "00 : 00";
return;
}
int m = seconds / 60;
int s = seconds % 60;
_timerText.text = (m < 10 ? "0" + m : m.ToString()) + " : " + (s < 10 ? "0" + s : s.ToString());
}
/// 给原预制体的文本节点补 TMP 组件(已有 TMP 直接复用),返回该组件
private TMPro.TextMeshProUGUI EnsureTmpText(Transform tf, float fontSize, string text)
{
if (tf == null) return null;
var tmp = tf.GetComponent();
if (tmp == null)
{
var legacy = tf.GetComponent(); // 兜底:万一节点上是 UGUI Text,先关掉避免叠字
if (legacy != null) legacy.enabled = false;
tmp = tf.gameObject.AddComponent();
}
// ⭐ 铁律:运行时 AddComponent 的文本必须显式设字体,否则不显示
tmp.font = timerFont != null ? timerFont : TMPro.TMP_Settings.defaultFontAsset;
tmp.fontSize = fontSize;
tmp.alignment = TMPro.TextAlignmentOptions.Center;
tmp.color = Color.white;
tmp.raycastTarget = false;
if (!string.IsNullOrEmpty(text)) tmp.text = text;
return tmp;
}
/// 倒计时 UI 的父节点:游戏主 UI 面板层 → UIManager.uiRoot 兜底
private Transform ResolveTimerUiParent()
{
if (CreateFishManager.Instance != null && CreateFishManager.Instance.currGameType != null
&& CreateFishManager.Instance.currGameType.subPanelParent != null)
return CreateFishManager.Instance.currGameType.subPanelParent.parent;
if (UIManager.Instance != null) return UIManager.Instance.uiRoot;
return null;
}
/// 复位本体特效:停掉隐藏计时 + 全部隐藏(回池/复用/离场时调用;爆炸特效单独收尾)
private void ResetBodyEffects()
{
KillTween(ref _bornEffectTween);
KillTween(ref _attackEffectTween);
if (bornEffect != null) bornEffect.SetActive(false);
if (attackEffect != null) attackEffect.SetActive(false);
if (outEffect != null) outEffect.SetActive(false);
}
/// 安全停掉一个 DOTween 计时(防止对已销毁对象触发回调)
private static void KillTween(ref Tween tween)
{
if (tween != null && tween.IsActive()) tween.Kill();
tween = null;
}
/// Spine 动画是否存在(动画名或 Skeleton 缺失时返回 false)。
private bool HasSpineAnim(string name)
{
if (skeleton == null) return false;
var data = skeleton.SkeletonDataAsset?.GetSkeletonData(false);
return data != null && data.FindAnimation(name) != null;
}
/// 安全播放 Spine 动画:动画名不存在 / 非 Spine 时静默跳过,避免 Spine 抛 ArgumentException。
private void PlaySpineSafe(string name, bool loop)
{
if (skeleton == null) return;
var data = skeleton.SkeletonDataAsset?.GetSkeletonData(false);
if (data == null || data.FindAnimation(name) == null) return;
skeleton.AnimationState.SetAnimation(0, name, loop);
}
/// 本体被销毁时清理:倒计时 UI 是独立挂 UI 层的对象,必须一起销毁防残留。
private void OnDestroy()
{
StopAllCoroutines();
StopHitJitter();
KillTween(ref _timerTickTween);
KillTween(ref _timerMoveTween);
KillTween(ref _attackSoundTween);
KillTween(ref _bornEffectTween);
KillTween(ref _attackEffectTween);
KillTween(ref _explosionEffectTween);
if (_timerView != null)
{
Destroy(_timerView);
_timerView = null;
_timerGONode = null;
_timerText = null;
}
_timerActive = false;
}
}