This commit is contained in:
2026-08-21 01:52:14 +08:00
parent 532eb26d10
commit 704cf26fa8
3 changed files with 213 additions and 10 deletions
@@ -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
/// <summary>翻单张卡:隐藏宝石 → 显示卡牌背面 → 翻转显示正面(分级色)→ 特效</summary>
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);
@@ -1,28 +1,77 @@
using Assets.MyGame.Scripts.Net.APIResponses.Enum;
using Assets.MyGame.Scripts.Net.WebResponses;
using DG.Tweening;
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;
// ---- 自动退出(对齐 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 + 对应动画名)后此逻辑立即生效。
// ================================================================
/// <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();
}
/// <summary>对象回收/离场时停止攻击与退出计时,避免对已销毁对象触发(DOTween 回调安全守卫)。</summary>
public override void OnDisable()
{
StopAttackCycle();
StopExitTimer();
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
}
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;
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;
}
/// <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);
}
}
@@ -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: