From 704cf26fa8167c39b13c2a01dff2de8196a21da1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=A3=AE2003?= <2384017988@qq.com>
Date: Fri, 21 Aug 2026 01:52:14 +0800
Subject: [PATCH] 1
---
.../Game/XJMH/Boss/JiXieXie/JiXieXieEvent.cs | 24 ++-
.../Game/XJMH/Boss/JiXieXie/JiXieXie_BOSS.cs | 183 +++++++++++++++++-
.../XJMH/Boss/JiXieXie/PW_Enemy_3000.prefab | 16 +-
3 files changed, 213 insertions(+), 10 deletions(-)
diff --git a/Assets/MyGame/aa/Game/XJMH/Boss/JiXieXie/JiXieXieEvent.cs b/Assets/MyGame/aa/Game/XJMH/Boss/JiXieXie/JiXieXieEvent.cs
index 1301fbbac..8be388eb9 100644
--- a/Assets/MyGame/aa/Game/XJMH/Boss/JiXieXie/JiXieXieEvent.cs
+++ b/Assets/MyGame/aa/Game/XJMH/Boss/JiXieXie/JiXieXieEvent.cs
@@ -23,9 +23,10 @@ using Random = UnityEngine.Random;
/// num < 炮倍×100 绿,< 炮倍×200 蓝,< 炮倍×500 紫,≥ 炮倍×500 红(金色不用),
/// 每张卡下方显示本份金币数量。
///
-/// 流程(用户确认版,全程 UI 层):
-/// 死亡位置一次出现 8 颗宝石 → 停留1秒 → 飞到屏幕中心两排(每排4个)排好
+/// 流程(用户确认版,全程 UI 层;lua 对应 PW_Effect.txt DropTenCardEffect):
+/// 屏幕靠上位置一次出现 N 颗宝石 → 飞到屏幕中心两排排好
/// → 一张一张翻牌(全是金币)→ 卡牌飞到座位上方 → 显示 BJEndUI + 加分 + 回收
+/// 音效对齐 lua:宝石掉落 FreeCardDrop / 翻牌 FreeCardBomb / 分数滚动 FreeCardEarnRoll(+Enlarge/Reduce) / 领取 FreeGet
///
/// ⭐ 资源配置:所有预制体/图片/名称集中在 JiXieXieConfig 资产(ScriptableObject),
/// 只需拖一个 config 字段。资产用菜单 Tools → 机械蝎 → 生成八连抽配置资产 一键生成。
@@ -386,8 +387,9 @@ public class JiXieXieEvent : BJEventBase
Vector2 baseUi = CoordinateTransformation.ScreenPoint_ToCanvas(
new Vector2(Screen.width / 2f, Screen.height * spawnYRatio), _uiCanvas);
- // 音效:宝石掉落(PW_FreeCardDrop,只播一次,宝石间隔太密不逐颗播)
- AudioManager.Instance.PlayerFXByName("PW_FreeCardDrop");
+ // 音效:宝石掉落(lua OnCreate 仅 isSelf 播 FreeCardDrop,只播一次,宝石间隔太密不逐颗播)
+ if (isSelf)
+ AudioManager.Instance.PlayerFXByName("PW_FreeCardDrop");
_chipInstances.Clear();
@@ -523,12 +525,19 @@ public class JiXieXieEvent : BJEventBase
{
Debug.Log($"[JiXieXie] 八连抽完成,显示 BJEndUI 倍数={bJbeishu} 总分={maxScore} isSelf={isSelf}");
+ // 音效:分数滚动开始(lua RollScoreAim 用 FreeCardEarnRoll)
+ AudioManager.Instance.PlayerFXByName("PW_FreeCardEarnRoll");
+
Vector3 uiPos = GetSeatPosition() + Vector3.up * bjEndUIOffsetY;
if (bjEndUI != null)
{
var endUI = Instantiate(bjEndUI, uiPos, Quaternion.identity,
CreateFishManager.Instance != null ? CreateFishManager.Instance.bjEffectRoot : null);
endUI.Init(bJbeishu, maxScore, isSelf);
+
+ // 音效:分数数字放大→还原(lua RollScoreAim 用 FreeCardEarnEnlarge / FreeCardEarnReduce)
+ AudioManager.Instance.PlayerFXByName("PW_FreeCardEarnEnlarge");
+ DOVirtual.DelayedCall(0.4f, () => AudioManager.Instance.PlayerFXByName("PW_FreeCardEarnReduce"));
}
else
{
@@ -538,6 +547,9 @@ public class JiXieXieEvent : BJEventBase
// 回加金币到玩家
JiaFen?.Invoke();
+ // 音效:领取成功(lua MoveEndAim 最后一张卡 i==10 用 FreeGet)
+ AudioManager.Instance.PlayerFXByName("PW_FreeGet");
+
// 回收爆奖预制体 + 销毁 chip
die();
}
@@ -561,8 +573,8 @@ public class JiXieXieEvent : BJEventBase
/// 翻单张卡:隐藏宝石 → 显示卡牌背面 → 翻转显示正面(分级色)→ 特效
private void FlipOneCard(GameObject chip, DropItemInfo info, int index)
{
- // 音效:打开卡片(PW_OpenCard)
- AudioManager.Instance.PlayerFXByName("PW_OpenCard");
+ // 音效:翻牌/爆开(lua BombToAmi 用 FreeCardBomb;原 PW_OpenCard 为旧口径)
+ AudioManager.Instance.PlayerFXByName("PW_FreeCardBomb");
// 隐藏宝石相关子物体
Transform gemstone = chip.transform.Find(gemstoneName);
diff --git a/Assets/MyGame/aa/Game/XJMH/Boss/JiXieXie/JiXieXie_BOSS.cs b/Assets/MyGame/aa/Game/XJMH/Boss/JiXieXie/JiXieXie_BOSS.cs
index 66ce55339..51a9085e3 100644
--- a/Assets/MyGame/aa/Game/XJMH/Boss/JiXieXie/JiXieXie_BOSS.cs
+++ b/Assets/MyGame/aa/Game/XJMH/Boss/JiXieXie/JiXieXie_BOSS.cs
@@ -1,28 +1,77 @@
using Assets.MyGame.Scripts.Net.APIResponses.Enum;
using Assets.MyGame.Scripts.Net.WebResponses;
+using DG.Tweening;
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;
+
+ // ---- 自动退出(对齐 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;
// 音效:蝎子语音(PW_TextScorpion)
AudioManager.Instance.PlayerFXByName("PW_TextScorpion");
}
public override void DieFish(RoomKillNotice obj = null)
{
+ StopAttackCycle(); // 死亡即停止攻击循环,避免对已销毁对象触发动画
+ StopExitTimer(); // 取消自动退出计时,避免击杀回收后定时器再触发一次回收
base.DieFish(obj);
+ // 离场/死亡动画(对齐 lua out;非 Spine 或无 out 动画时自动跳过)
+ PlaySpineSafe(AnimOut, false);
+
// 音效:蝎子死亡 + 死亡语音(PW_ScorpionDie / PW_ScorpionDieVoice)
- AudioManager.Instance.PlayerFXByName("PW_ScorpionDie");
- AudioManager.Instance.PlayerFXByName("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) =>
@@ -62,4 +111,134 @@ public class JiXieXie_BOSS : MonsterControl_BossBase
}
};
}
+
+ // ================================================================
+ // 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();
+ }
+
+ /// 对象回收/离场时停止攻击与退出计时,避免对已销毁对象触发(DOTween 回调安全守卫)。
+ public override void OnDisable()
+ {
+ StopAttackCycle();
+ StopExitTimer();
+ 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
+ }
+ 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;
+ PlaySpineSafe(AnimAttack, false);
+ _attackTween = DOVirtual.DelayedCall(AttackDuration, () =>
+ {
+ if (!_attackRunning) return;
+ PlaySpineSafe(AnimStand, true);
+ ScheduleNextAttack();
+ });
+ }
+
+ private void StopAttackCycle()
+ {
+ _attackRunning = false;
+ if (_attackTween != null && _attackTween.IsActive())
+ _attackTween.Kill();
+ _attackTween = 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);
+ }
}
diff --git a/Assets/MyGame/aa/Game/XJMH/Boss/JiXieXie/PW_Enemy_3000.prefab b/Assets/MyGame/aa/Game/XJMH/Boss/JiXieXie/PW_Enemy_3000.prefab
index bed90506c..dc3161f06 100644
--- a/Assets/MyGame/aa/Game/XJMH/Boss/JiXieXie/PW_Enemy_3000.prefab
+++ b/Assets/MyGame/aa/Game/XJMH/Boss/JiXieXie/PW_Enemy_3000.prefab
@@ -125,7 +125,7 @@ MonoBehaviour:
moveSpeed: 0
cachedCamera: {fileID: 0}
cachedUirootRect: {fileID: 0}
- skeleton: {fileID: 0}
+ skeleton: {fileID: 8745352577998363756}
isSpine: 0
fishTypeID: 12028
currState: 0
@@ -137,7 +137,7 @@ MonoBehaviour:
isMove: 0
is3D: 0
forwordXYZ:
- fishenum: 0
+ fishenum: 2
moveTime: 0
isyz: 0
yzID:
@@ -167,3 +167,15 @@ MonoBehaviour:
v3: {x: 0, y: 0, z: 0}
isLocalCoin: 0
killerSitPos: -1
+--- !u!114 &8745352577998363756 stripped
+MonoBehaviour:
+ m_CorrespondingSourceObject: {fileID: 114517790449158321, guid: 75d387735e7c6cb4d806a8603178a855,
+ type: 3}
+ m_PrefabInstance: {fileID: 8704159038579725533}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: d247ba06193faa74d9335f5481b2b56c, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: