832 lines
38 KiB
C#
832 lines
38 KiB
C#
using Assets.MyGame.Scripts.Net.APIResponses.Enum;
|
||
using Assets.MyGame.Scripts.Net.WebResponses;
|
||
using DG.Tweening;
|
||
using System;
|
||
using System.Collections;
|
||
using UnityEngine;
|
||
|
||
/// <summary>
|
||
/// 机械蝎 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。
|
||
/// </summary>
|
||
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<BJEventBase>();
|
||
|
||
// 先设置死亡位置,再 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<int>(加分回调)
|
||
() => { v.obj = obj.param; } // 把服务端爆奖参数交给事件类
|
||
);
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
// ================================================================
|
||
// 受击抖动(对齐 lua Scorpion:OnHit / HitUpdate)
|
||
// ================================================================
|
||
|
||
/// <summary>子弹命中回调(MonsterContral.OnHurt,由 BullitMove 命中鱼时调用,virtual 可直接 override)。</summary>
|
||
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());
|
||
}
|
||
|
||
/// <summary>0.2s 内每帧随机偏移 Body 的 localPosition,结束后还原(对齐 lua Scorpion:HitUpdate)</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>解析 Body 节点并缓存其原始 localPosition(预制体里 Body 带偏移,还原必须用原值)</summary>
|
||
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} 节点,受击抖动跳过");
|
||
}
|
||
|
||
/// <summary>停止受击抖动并把 Body 位置还原(回池/销毁时调用,防抖动残留)</summary>
|
||
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 + 对应动画名)后此逻辑立即生效。
|
||
// ================================================================
|
||
|
||
/// <summary>出生动画序列:in(一次) → 2.2s 后 stand(循环)。无 in 动画时回退基类 stand2。
|
||
/// 同时启动攻击循环(OnMoveEnter 在状态切到 move 时必被调用,不受 isMove 闸门影响)。</summary>
|
||
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();
|
||
}
|
||
|
||
/// <summary>对象回收/离场时停止攻击与退出计时,避免对已销毁对象触发(DOTween 回调安全守卫)。</summary>
|
||
public override void OnDisable()
|
||
{
|
||
StopAttackCycle();
|
||
StopExitTimer();
|
||
ResetBodyEffects(); // ⭐ 回池时把三个本体特效连同计时一起复位,防复用残留
|
||
StopHitJitter(); // ⭐ 受击抖动协程 + Body 位置还原
|
||
|
||
// ⭐ 倒计时 UI:没走 DieFish/AutoExit 的滑出就直接销毁(房间关闭/被动回收也保证不残留)
|
||
if (_timerView != null) CloseTimerView(true);
|
||
KillTween(ref _attackSoundTween);
|
||
|
||
base.OnDisable();
|
||
}
|
||
|
||
// ================================================================
|
||
// 自动退出(对齐 lua OnMoveFinish:未被击杀也到点离场)
|
||
// ================================================================
|
||
|
||
/// <summary>启动存活计时:到点触发 AutoExit。bossLifeTime<=0 不自动离场(纯靠击杀)。</summary>
|
||
private void StartExitTimer()
|
||
{
|
||
if (bossLifeTime <= 0f) return;
|
||
_isExiting = false;
|
||
if (_exitTween != null && _exitTween.IsActive()) _exitTween.Kill();
|
||
_exitTween = DOVirtual.DelayedCall(bossLifeTime, AutoExit);
|
||
}
|
||
|
||
/// <summary>到点离场:停攻击循环 → 播 out(若 Spine 含)→ 延迟回收本体(同 lua OnMoveFinish→DestroyEnemy)。</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>攻击循环:随机 10~25s 间隔播放 attack(一次,2s) 后回 stand(对齐 lua stand⇄attack)。
|
||
/// 用 DOTween 递归计时,不依赖 OnMoveUpdata / isMove。
|
||
/// lua 原逻辑 attack 还需“出场倒计时>5”才触发,C# 侧无该倒计时阶段,故简化为纯随机间隔。</summary>
|
||
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();
|
||
});
|
||
}
|
||
|
||
/// <summary>是否允许攻击表现:有倒计时时需 > 5 秒(对齐 lua scorepionTimerNum > 5);无倒计时时始终允许</summary>
|
||
private bool CanAttackVisual => !_timerActive || _timerSeconds > AttackVisualMinSeconds;
|
||
|
||
/// <summary>攻击音效(对齐 lua ScorpionAttack + ScorpionAttackVoice,动作结束前 1 秒播)</summary>
|
||
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 + 到点隐藏。
|
||
// ================================================================
|
||
|
||
/// <summary>
|
||
/// 解析三个本体特效子物体:Inspector 没拖时按名字查找(先根节点直接找,再递归忽略大小写兜底)。
|
||
/// </summary>
|
||
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 子物体)");
|
||
}
|
||
|
||
/// <summary>按名字查找特效子物体(找不到返回 null)</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>递归查找子物体(忽略大小写,防改名/挪层级后找不到)</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>出场特效:激活 → BornEffectTime(3s) 后自动隐藏(对齐 lua MonentaryEffect time=3)</summary>
|
||
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 后隐藏)");
|
||
}
|
||
|
||
/// <summary>攻击特效:激活 → AttackEffectTime(3.5s) 后自动隐藏(对齐 lua MonentaryEffect time=3.5)</summary>
|
||
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 后隐藏)");
|
||
}
|
||
|
||
/// <summary>离场特效:激活后一直显示到本体回收(lua Out time=2,与 AutoExit 的 2s 延迟一致)</summary>
|
||
private void PlayOutEffect()
|
||
{
|
||
if (outEffect == null) return;
|
||
outEffect.SetActive(true);
|
||
Debug.Log($"[JiXieXie] 离场特效 {OutEffectName} 播放");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 被杀爆炸特效(lua OnExplosion:在死亡位置创建 PW_Effect_Explosion_Scorpion,time=1)。
|
||
///
|
||
/// ⚠️ 它是本体的子物体,而本类 DieFish 里 Boss 当帧就会被回收(回池会连带停掉子物体),
|
||
/// 所以先把特效脱离父节点(保留世界变换)让它能完整播完 1 秒,
|
||
/// 1 秒后再挂回本体 + 还原预制体原始局部变换 + 隐藏,保证对象池复用时结构完整。
|
||
/// 爆炸特效不放进 ResetBodyEffects(),避免回池时把它一起关掉。
|
||
/// </summary>
|
||
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();
|
||
});
|
||
}
|
||
|
||
/// <summary>爆炸特效收尾:隐藏 + 挂回本体并还原原始局部变换(计时回调/重复触发前共用)</summary>
|
||
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)
|
||
// ================================================================
|
||
|
||
/// <summary>
|
||
/// 打开/重建退场倒计时 UI(lua OnCreate → ScorpionCountDown + EnterAction)。
|
||
/// 倒计时秒数 = 距离自动离场(bossLifeTime)的剩余秒数;bossLifeTime<=0 时不显示。
|
||
/// </summary>
|
||
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 时不再攻击)");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 关闭倒计时 UI(lua ExitAction:向右滑出)。
|
||
///
|
||
/// ⭐ 这里是"倒计时整块一定会消失"的唯一保证点,改成了"必定销毁":
|
||
/// 旧写法把销毁挂在滑出 tween 的 OnComplete 上,而且"已在滑出中就直接 return",
|
||
/// 只要那根 tween 在 0.3s 内被中途 Kill(滑出期间 Boss 被复用、再次 OpenTimerView、
|
||
/// 或任何 KillTween(ref _timerMoveTween)),OnComplete 就永不执行 →
|
||
/// 倒计时整块永久残留在屏幕上(鱼死了也不消失)。
|
||
/// 现在:立刻摘掉字段引用 + 用不挂在任何对象上的 DOVirtual 兜底计时,到点无条件销毁。
|
||
/// 顺带修掉"旧 tween 的 OnComplete 误销毁新视图"的隐患(引用改成本地捕获,不再读字段)。
|
||
/// </summary>
|
||
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);
|
||
});
|
||
}
|
||
|
||
/// <summary>滑入动画(lua EnterAction:先归位 (0,50),再 localMoveBy(-250, 50, 0.5, EOutBack))</summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>每秒递减(lua Update:deltaTimeNum 累积 ≥1 → TimerUpdate)</summary>
|
||
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();
|
||
}
|
||
|
||
/// <summary>刷新倒计时文本(lua SetTimerText:≤0 → "00 : 00",否则 "MM : SS" 补零)</summary>
|
||
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());
|
||
}
|
||
|
||
/// <summary>给原预制体的文本节点补 TMP 组件(已有 TMP 直接复用),返回该组件</summary>
|
||
private TMPro.TextMeshProUGUI EnsureTmpText(Transform tf, float fontSize, string text)
|
||
{
|
||
if (tf == null) return null;
|
||
|
||
var tmp = tf.GetComponent<TMPro.TextMeshProUGUI>();
|
||
if (tmp == null)
|
||
{
|
||
var legacy = tf.GetComponent<UnityEngine.UI.Text>(); // 兜底:万一节点上是 UGUI Text,先关掉避免叠字
|
||
if (legacy != null) legacy.enabled = false;
|
||
tmp = tf.gameObject.AddComponent<TMPro.TextMeshProUGUI>();
|
||
}
|
||
|
||
// ⭐ 铁律:运行时 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;
|
||
}
|
||
|
||
/// <summary>倒计时 UI 的父节点:游戏主 UI 面板层 → UIManager.uiRoot 兜底</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>复位本体特效:停掉隐藏计时 + 全部隐藏(回池/复用/离场时调用;爆炸特效单独收尾)</summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>安全停掉一个 DOTween 计时(防止对已销毁对象触发回调)</summary>
|
||
private static void KillTween(ref Tween tween)
|
||
{
|
||
if (tween != null && tween.IsActive()) tween.Kill();
|
||
tween = null;
|
||
}
|
||
|
||
/// <summary>Spine 动画是否存在(动画名或 Skeleton 缺失时返回 false)。</summary>
|
||
private bool HasSpineAnim(string name)
|
||
{
|
||
if (skeleton == null) return false;
|
||
var data = skeleton.SkeletonDataAsset?.GetSkeletonData(false);
|
||
return data != null && data.FindAnimation(name) != null;
|
||
}
|
||
|
||
/// <summary>安全播放 Spine 动画:动画名不存在 / 非 Spine 时静默跳过,避免 Spine 抛 ArgumentException。</summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>本体被销毁时清理:倒计时 UI 是独立挂 UI 层的对象,必须一起销毁防残留。</summary>
|
||
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;
|
||
}
|
||
}
|