2026-08-15 19:39:01 +08:00
|
|
|
|
using Assets.MyGame.Scripts.Net.WebResponses;
|
|
|
|
|
|
using Newtonsoft.Json.Linq;
|
2026-08-18 18:06:30 +08:00
|
|
|
|
using Spine.Unity;
|
2026-08-15 19:39:01 +08:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 星际巡航 Boss(继承 MonsterControl_BossBase)。
|
|
|
|
|
|
///
|
2026-08-16 00:38:45 +08:00
|
|
|
|
/// 支持多次死亡(1~3次,服务端决定):
|
2026-08-15 19:39:01 +08:00
|
|
|
|
/// 非最终死亡 → 显示 BJEndUI(倍数+分数),Boss 不消失,等待下次击杀
|
2026-08-16 00:38:45 +08:00
|
|
|
|
/// 最终死亡 → isKill=true 直接删除 Boss + 触发完整爆奖流程 + 道具武器掉落
|
2026-08-15 19:39:01 +08:00
|
|
|
|
///
|
|
|
|
|
|
/// 死亡判定来源:
|
2026-08-16 00:38:45 +08:00
|
|
|
|
/// 方式1(优先):BossKillParamParser.ParseMultiDeath 解析 RoomKillNotice.param
|
|
|
|
|
|
/// { "isKill": bool, "beishu": int, "fishId": "原ID-总次数-当前次数" }
|
|
|
|
|
|
/// 方式2(兜底):param 缺失时按 maxDeathCount 简单判定死亡次数
|
2026-08-15 19:39:01 +08:00
|
|
|
|
///
|
2026-08-16 00:38:45 +08:00
|
|
|
|
/// ⭐ 非最终死亡时不从鱼列表删除 Boss;最终死亡时下一帧直接 Id_DieFish_NotFX 删除。
|
2026-08-15 19:39:01 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class XingJiXunHang_BOSS : MonsterControl_BossBase
|
|
|
|
|
|
{
|
|
|
|
|
|
// ==================== Inspector 字段 ====================
|
|
|
|
|
|
|
|
|
|
|
|
[Header("星际巡航 - 多次死亡")]
|
|
|
|
|
|
[Tooltip("非最终死亡时显示的 BJEndUI 预制体(只显示倍数+分数,Boss 不消失)")]
|
|
|
|
|
|
public BJEndUI nonFinalDeathBjEndUI;
|
|
|
|
|
|
|
|
|
|
|
|
[Tooltip("非最终死亡后,Boss 恢复游动的延迟(秒)")]
|
|
|
|
|
|
public float resumeMoveDelay = 2f;
|
|
|
|
|
|
|
2026-08-18 18:06:30 +08:00
|
|
|
|
[Header("星际巡航 - 非最终死亡动画(animId=1)")]
|
|
|
|
|
|
[Tooltip("不死时播放的 Spine 动画名(stand02 = 第一种不死亡爆奖动画)")]
|
|
|
|
|
|
public string nonFinalDeathAnimationName = "stand02";
|
|
|
|
|
|
|
|
|
|
|
|
[Tooltip("正常默认动画名(stand01,不死动画播放完恢复游动时切回)")]
|
|
|
|
|
|
public string normalAnimationName = "stand01";
|
|
|
|
|
|
|
|
|
|
|
|
[Tooltip("SkeletonAnimation 所在子对象名(留空=自动 GetComponentInChildren 查找)")]
|
|
|
|
|
|
public string skeletonChildName = "";
|
|
|
|
|
|
|
|
|
|
|
|
[Header("星际巡航 - Hitting 特效")]
|
|
|
|
|
|
[Tooltip("Hitting 特效子对象路径(相对 Boss 根,如 body/bone/hitting)")]
|
|
|
|
|
|
public string hittingPath = "body/bone/hitting";
|
|
|
|
|
|
|
|
|
|
|
|
[Tooltip("Hitting 特效激活持续时间(秒,默认2)")]
|
|
|
|
|
|
public float hittingDuration = 2f;
|
|
|
|
|
|
|
2026-08-15 19:39:01 +08:00
|
|
|
|
[Header("星际巡航 - 服务端参数解析")]
|
2026-08-16 00:38:45 +08:00
|
|
|
|
[Tooltip("是否从 param 解析 isKill(关闭则用 maxDeathCount 客户端兜底判定)")]
|
2026-08-15 19:39:01 +08:00
|
|
|
|
public bool useServerParam = true;
|
|
|
|
|
|
|
|
|
|
|
|
[Header("星际巡航 - 客户端兜底配置")]
|
2026-08-16 00:38:45 +08:00
|
|
|
|
[Tooltip("最大死亡次数(param 缺失时生效,默认3次)")]
|
|
|
|
|
|
public int maxDeathCount = 3;
|
2026-08-15 19:39:01 +08:00
|
|
|
|
|
|
|
|
|
|
// ==================== 运行时状态 ====================
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>爆奖事件引用(死亡时赋值,供外部检查爆奖是否结束)</summary>
|
|
|
|
|
|
[System.NonSerialized]
|
|
|
|
|
|
public XingJiXunHangEvent currEvent;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>当前死亡次数(从 1 开始)</summary>
|
|
|
|
|
|
private int _deathCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>星际巡航专用 fishId(带次数后缀,由 GetResponse 返回)</summary>
|
|
|
|
|
|
private string _cruiseFishId = "";
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>缓存的击杀数据(最终死亡时使用)</summary>
|
|
|
|
|
|
private RoomKillNotice _cachedKillNotice;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>是否已进入最终死亡流程</summary>
|
|
|
|
|
|
private bool _isFinalDeath = false;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>Boss 是否已从鱼列表回收(防止重复回收)</summary>
|
|
|
|
|
|
private bool _removed = false;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>死亡位置(删除 Boss 前缓存,供爆奖事件使用)</summary>
|
|
|
|
|
|
private Vector3 _diePos;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>是否已缓存死亡位置</summary>
|
|
|
|
|
|
private bool _diePosCached = false;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>爆奖事件是否已 Init(决定特效实例由谁回收)</summary>
|
|
|
|
|
|
private bool _awardInitCalled = false;
|
|
|
|
|
|
|
2026-08-18 18:06:30 +08:00
|
|
|
|
/// <summary>正在运行的 Hitting 特效协程(同一时间只允许一个,防连续触发冲突)</summary>
|
|
|
|
|
|
private Coroutine _hittingRoutine = null;
|
|
|
|
|
|
|
2026-08-15 19:39:01 +08:00
|
|
|
|
// ==================== Unity 生命周期 ====================
|
|
|
|
|
|
|
|
|
|
|
|
public override void OnEnable()
|
|
|
|
|
|
{
|
|
|
|
|
|
base.OnEnable();
|
|
|
|
|
|
_deathCount = 0;
|
|
|
|
|
|
_cruiseFishId = "";
|
|
|
|
|
|
_cachedKillNotice = null;
|
|
|
|
|
|
_isFinalDeath = false;
|
|
|
|
|
|
_removed = false;
|
|
|
|
|
|
_diePosCached = false;
|
|
|
|
|
|
_awardInitCalled = false;
|
2026-08-18 18:06:30 +08:00
|
|
|
|
_hittingRoutine = null;
|
2026-08-15 19:39:01 +08:00
|
|
|
|
currEvent = null;
|
2026-08-18 18:06:30 +08:00
|
|
|
|
|
|
|
|
|
|
// ⭐ 恢复被修改过的激活状态(对象池复用:Hitting 特效强制恢复初始关闭)
|
|
|
|
|
|
ResetHittingState();
|
2026-08-20 16:27:42 +08:00
|
|
|
|
|
|
|
|
|
|
// ⭐ 回池复用/重生:把前端 spine 复位回正常待机(stand01),
|
|
|
|
|
|
// 清除上次残留的"不死爆奖循环动画",避免复用后还在循环播死亡动画
|
|
|
|
|
|
ResetSpineToIdle();
|
|
|
|
|
|
|
|
|
|
|
|
// 音效:星舰出场(PW_StarShip_Show)
|
|
|
|
|
|
AudioManager.Instance.PlayerFXByName("PW_StarShip_Show");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>复用/重生时把前端 spine 复位为正常待机(stand01,循环播放)</summary>
|
|
|
|
|
|
private void ResetSpineToIdle()
|
|
|
|
|
|
{
|
|
|
|
|
|
PlaySkeletonAnimation(normalAnimationName);
|
2026-08-18 18:06:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>恢复 Hitting 特效的初始激活状态(关闭)</summary>
|
|
|
|
|
|
private void ResetHittingState()
|
|
|
|
|
|
{
|
|
|
|
|
|
// 停掉残留的 Hitting 协程
|
|
|
|
|
|
if (_hittingRoutine != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
StopCoroutine(_hittingRoutine);
|
|
|
|
|
|
_hittingRoutine = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Transform hitting = null;
|
|
|
|
|
|
if (!string.IsNullOrEmpty(hittingPath))
|
|
|
|
|
|
{
|
|
|
|
|
|
hitting = transform.Find(hittingPath);
|
|
|
|
|
|
if (hitting == null)
|
|
|
|
|
|
hitting = FindChildPathIgnoreCase(transform, hittingPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (hitting != null)
|
|
|
|
|
|
hitting.gameObject.SetActive(false);
|
2026-08-15 19:39:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 死亡逻辑 ====================
|
|
|
|
|
|
|
|
|
|
|
|
public override void DieFish(RoomKillNotice obj = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
_deathCount++;
|
|
|
|
|
|
|
|
|
|
|
|
// 解析 isKill 和 beishu
|
|
|
|
|
|
bool isKill;
|
|
|
|
|
|
int beishu;
|
|
|
|
|
|
|
|
|
|
|
|
if (useServerParam && obj?.param != null)
|
|
|
|
|
|
{
|
2026-08-16 00:38:45 +08:00
|
|
|
|
// 方式1:BossKillParamParser 解析服务端 param
|
2026-08-15 19:39:01 +08:00
|
|
|
|
Debug.Log($"[XingJiXunHang] DieFish 第{_deathCount}次 param原文: {obj.param.ToString(Newtonsoft.Json.Formatting.None)}");
|
2026-08-16 00:38:45 +08:00
|
|
|
|
var p = BossKillParamParser.ParseMultiDeath(obj.param);
|
|
|
|
|
|
if (p != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
isKill = p.isKill;
|
|
|
|
|
|
beishu = p.beishu;
|
|
|
|
|
|
if (!string.IsNullOrEmpty(p.fishId))
|
|
|
|
|
|
_cruiseFishId = p.fishId;
|
2026-08-16 21:40:22 +08:00
|
|
|
|
|
|
|
|
|
|
// ⭐ animId 优先于 isKill(服务端实际以 animId 为准):
|
|
|
|
|
|
// animId==1 → 不死(只播 BJEndUI,Boss 保留)
|
|
|
|
|
|
// animId>=2 → 带死亡的爆奖(2/3/4 三种表现)
|
|
|
|
|
|
// animId==0 → 老消息无 animId,回退用 isKill
|
|
|
|
|
|
if (p.animId >= 1)
|
|
|
|
|
|
{
|
|
|
|
|
|
isKill = p.animId >= 2;
|
|
|
|
|
|
Debug.Log($"[XingJiXunHang] animId={p.animId} → isKill={isKill}");
|
|
|
|
|
|
}
|
2026-08-16 00:38:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning("[XingJiXunHang] param 解析失败,走客户端兜底");
|
|
|
|
|
|
(isKill, beishu) = GetClientFallback(obj);
|
|
|
|
|
|
}
|
|
|
|
|
|
Debug.Log($"[XingJiXunHang] DieFish 第{_deathCount}次(服务端) isKill={isKill} beishu={beishu} fishId={_cruiseFishId} guid={guid}");
|
2026-08-15 19:39:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-08-16 00:38:45 +08:00
|
|
|
|
// 方式2:客户端兜底(param 缺失时按 maxDeathCount 判定)
|
|
|
|
|
|
(isKill, beishu) = GetClientFallback(obj);
|
|
|
|
|
|
Debug.Log($"[XingJiXunHang] DieFish 第{_deathCount}次(客户端兜底) isKill={isKill} beishu={beishu}");
|
2026-08-15 19:39:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!isKill)
|
|
|
|
|
|
{
|
|
|
|
|
|
// ===== 非最终死亡:显示 BJEndUI,Boss 不消失 =====
|
|
|
|
|
|
NonFinalDeath(obj, beishu);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// ===== 最终死亡:完整爆奖流程 =====
|
|
|
|
|
|
FinalDeath(obj, beishu);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-11 14:05:54 +08:00
|
|
|
|
// ==================== 锁定 ====================
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// ⭐ 爆奖不死(非最终死亡)时不影响各座位对该 Boss 的锁定:不清锁,玩家不用重新点一次。
|
|
|
|
|
|
/// 判定与 DieFish 保持一致:animId 优先于 isKill(animId>=2 才算最终死亡)。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public override bool ShouldClearLockOnKill(RoomKillNotice killNotice)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!useServerParam || killNotice?.param == null || killNotice.param.Type == JTokenType.Null)
|
|
|
|
|
|
return true; // 没有 param → 按"会死"处理,走原来的清锁逻辑
|
|
|
|
|
|
|
|
|
|
|
|
var p = BossKillParamParser.ParseMultiDeath(killNotice.param);
|
|
|
|
|
|
if (p == null) return true;
|
|
|
|
|
|
|
|
|
|
|
|
return p.animId >= 1 ? p.animId >= 2 : p.isKill;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-15 19:39:01 +08:00
|
|
|
|
// ==================== 非最终死亡 ====================
|
|
|
|
|
|
|
2026-08-18 18:06:30 +08:00
|
|
|
|
/// <summary>非最终死亡:显示 BJEndUI + 播放不死动画,Boss 保留,等待下次击杀</summary>
|
2026-08-15 19:39:01 +08:00
|
|
|
|
private void NonFinalDeath(RoomKillNotice obj, int beishu)
|
|
|
|
|
|
{
|
|
|
|
|
|
_cachedKillNotice = obj;
|
|
|
|
|
|
|
|
|
|
|
|
Debug.Log($"[XingJiXunHang] 非最终死亡:显示 BJEndUI,Boss 保留");
|
|
|
|
|
|
|
2026-08-20 16:27:42 +08:00
|
|
|
|
// 音效:星舰受击(PW_StarShip_On_Hit)
|
|
|
|
|
|
AudioManager.Instance.PlayerFXByName("PW_StarShip_On_Hit");
|
|
|
|
|
|
|
2026-08-18 18:06:30 +08:00
|
|
|
|
// ⭐ 播放不死动画(SkeletonAnimation,从子对象获取)
|
|
|
|
|
|
PlayNonFinalDeathAnimation();
|
|
|
|
|
|
|
|
|
|
|
|
// ⭐ 播放 Hitting 特效:只激活一个协程(连续触发先停旧的,防止特效状态错乱)
|
|
|
|
|
|
if (_hittingRoutine != null)
|
|
|
|
|
|
StopCoroutine(_hittingRoutine);
|
|
|
|
|
|
_hittingRoutine = StartCoroutine(PlayHittingEffect());
|
|
|
|
|
|
|
2026-08-15 19:39:01 +08:00
|
|
|
|
// 显示 BJEndUI(倍数+分数)
|
|
|
|
|
|
ShowNonFinalDeathBjEndUI(obj, beishu);
|
|
|
|
|
|
|
|
|
|
|
|
// 跳过爆奖特效初始化(CreateFishManager 仍会创建 effect_DieAward)
|
|
|
|
|
|
action_effect_DieAward = (o) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.Log("[XingJiXunHang] 非最终死亡:跳过爆奖特效");
|
|
|
|
|
|
// ⭐ o 是从 EffectPool 取的实例,必须用 Return 回池,不能 Destroy(会破坏对象池)
|
|
|
|
|
|
if (o != null) EffectPool.Instance.Return(o);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ⭐ 不设置 isDieFish,不调用 base.DieFish,不从鱼列表删除
|
|
|
|
|
|
// 延迟后恢复游动状态
|
|
|
|
|
|
StartCoroutine(ResumeMoveAfterDelay());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>非最终死亡后恢复游动</summary>
|
|
|
|
|
|
private IEnumerator ResumeMoveAfterDelay()
|
|
|
|
|
|
{
|
|
|
|
|
|
yield return new WaitForSeconds(resumeMoveDelay);
|
|
|
|
|
|
|
|
|
|
|
|
// 恢复游动状态(如果 Boss 需要继续移动)
|
|
|
|
|
|
if (!_isFinalDeath && isInitialized)
|
|
|
|
|
|
{
|
|
|
|
|
|
ChangeState(CharacterStateType.move);
|
|
|
|
|
|
isMove = true;
|
2026-09-15 22:38:11 +08:00
|
|
|
|
// ⭐ 复位死亡标记:Ws_DieFish 对每次击杀统一置 isDead=true,非最终死亡 Boss 不回池,
|
|
|
|
|
|
// OnEnable 不会重跑导致 isDead 卡 true → 重新锁定后子弹被 BullitMove 判 target.isDead
|
|
|
|
|
|
// 立即 NotLock(碰撞器恢复+直线飞行,被路径中间的小鱼挡住)
|
|
|
|
|
|
isDead = false;
|
2026-08-15 19:39:01 +08:00
|
|
|
|
Debug.Log("[XingJiXunHang] 非最终死亡后恢复游动");
|
|
|
|
|
|
}
|
2026-08-18 18:06:30 +08:00
|
|
|
|
|
|
|
|
|
|
// ⭐ 不死动画(stand02)播完,切回正常默认动画(stand01)
|
|
|
|
|
|
PlaySkeletonAnimation(normalAnimationName);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>获取 SkeletonAnimation(指定子对象或递归查找)并播放指定动画;动画不存在时回退 fallbackName</summary>
|
2026-08-20 16:27:42 +08:00
|
|
|
|
private void PlaySkeletonAnimation(string animationName, string fallbackName = null, bool loop = true)
|
2026-08-18 18:06:30 +08:00
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrEmpty(animationName)) return;
|
|
|
|
|
|
|
|
|
|
|
|
SkeletonAnimation sa = null;
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 指定子对象名 → 直接找该子对象上的组件
|
|
|
|
|
|
if (!string.IsNullOrEmpty(skeletonChildName))
|
|
|
|
|
|
{
|
|
|
|
|
|
Transform child = transform.Find(skeletonChildName);
|
|
|
|
|
|
if (child != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
sa = child.GetComponent<SkeletonAnimation>();
|
|
|
|
|
|
if (sa == null)
|
|
|
|
|
|
sa = child.GetComponentInChildren<SkeletonAnimation>(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 递归查找:⭐ 优先选"包含目标动画"的 SkeletonAnimation(避免选错骨架)
|
|
|
|
|
|
if (sa == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var all = GetComponentsInChildren<SkeletonAnimation>(true);
|
|
|
|
|
|
Debug.Log($"[XingJiXunHang] 找到 {all.Length} 个 SkeletonAnimation,目标动画={animationName}");
|
|
|
|
|
|
foreach (var s in all)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Skeleton.data 是 protected,改用 SkeletonDataAsset 公共 API
|
|
|
|
|
|
var sd = s.SkeletonDataAsset != null ? s.SkeletonDataAsset.GetSkeletonData(false) : null;
|
|
|
|
|
|
if (sd != null && sd.FindAnimation(animationName) != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
sa = s;
|
|
|
|
|
|
Debug.Log($"[XingJiXunHang] 选择包含动画 {animationName} 的骨架: {s.gameObject.name}");
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (sa == null && all.Length > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
sa = all[0];
|
|
|
|
|
|
Debug.LogWarning($"[XingJiXunHang] 没有骨架包含动画 {animationName},用第一个 {sa.gameObject.name}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (sa == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning($"[XingJiXunHang] 找不到 SkeletonAnimation(子对象={skeletonChildName}),跳过动画 {animationName}");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ⭐ 目标动画在骨架里不存在 → 回退 fallbackName(防预制体存旧动画名如 "hit")
|
|
|
|
|
|
var data = sa.SkeletonDataAsset != null ? sa.SkeletonDataAsset.GetSkeletonData(false) : null;
|
|
|
|
|
|
if (data != null && data.FindAnimation(animationName) == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!string.IsNullOrEmpty(fallbackName) && data.FindAnimation(fallbackName) != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning($"[XingJiXunHang] 动画 {animationName} 不存在,回退 {fallbackName}");
|
|
|
|
|
|
animationName = fallbackName;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
var animNames = new System.Collections.Generic.List<string>();
|
|
|
|
|
|
foreach (var a in data.Animations) animNames.Add(a.Name);
|
|
|
|
|
|
Debug.LogWarning($"[XingJiXunHang] 骨架 {sa.gameObject.name} 可用动画: {string.Join(", ", animNames)}");
|
2026-08-20 16:27:42 +08:00
|
|
|
|
return; // 找不到则直接返回,避免设置不存在的动画
|
2026-08-18 18:06:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-20 16:27:42 +08:00
|
|
|
|
// ⭐ 用 SetAnimation 显式控制是否循环:不死爆奖(stand02)只播一次(loop=false),
|
|
|
|
|
|
// 待机(stand01)循环播放
|
|
|
|
|
|
sa.AnimationState.SetAnimation(0, animationName, loop);
|
2026-08-18 18:06:30 +08:00
|
|
|
|
sa.timeScale = 1f;
|
2026-08-20 16:27:42 +08:00
|
|
|
|
Debug.Log($"[XingJiXunHang] 播放 Spine 动画: {animationName} loop={loop}(SkeletonAnimation on {sa.gameObject.name})");
|
2026-08-18 18:06:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-20 16:27:42 +08:00
|
|
|
|
/// <summary>播放不死动画(stand02 = 第一种不死亡爆奖动画;只播一次,不循环)</summary>
|
2026-08-18 18:06:30 +08:00
|
|
|
|
private void PlayNonFinalDeathAnimation()
|
|
|
|
|
|
{
|
2026-08-20 16:27:42 +08:00
|
|
|
|
PlaySkeletonAnimation(nonFinalDeathAnimationName, "stand02", false);
|
2026-08-18 18:06:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 播放 Hitting 特效:激活子对象(body/bone/hitting),等待 hittingDuration 秒后关闭。
|
|
|
|
|
|
/// ⭐ transform.Find 大小写敏感,找不到时用大小写不敏感路径查找兜底(Body/bone/hitting)。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private IEnumerator PlayHittingEffect()
|
|
|
|
|
|
{
|
|
|
|
|
|
Transform hitting = null;
|
|
|
|
|
|
if (!string.IsNullOrEmpty(hittingPath))
|
|
|
|
|
|
{
|
|
|
|
|
|
hitting = transform.Find(hittingPath);
|
|
|
|
|
|
if (hitting == null)
|
|
|
|
|
|
hitting = FindChildPathIgnoreCase(transform, hittingPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (hitting == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning($"[XingJiXunHang] 找不到 Hitting 特效子对象 {hittingPath},跳过");
|
|
|
|
|
|
yield break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
hitting.gameObject.SetActive(true);
|
|
|
|
|
|
Debug.Log($"[XingJiXunHang] Hitting 特效激活({hittingPath} → {hitting.gameObject.name})");
|
|
|
|
|
|
|
|
|
|
|
|
yield return new WaitForSeconds(hittingDuration);
|
|
|
|
|
|
|
|
|
|
|
|
if (hitting != null)
|
|
|
|
|
|
hitting.gameObject.SetActive(false);
|
|
|
|
|
|
_hittingRoutine = null; // 协程结束,允许下一次触发
|
|
|
|
|
|
Debug.Log($"[XingJiXunHang] Hitting 特效关闭({hittingDuration}s 后)");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>按路径逐级查找子对象(忽略大小写)</summary>
|
|
|
|
|
|
private Transform FindChildPathIgnoreCase(Transform root, string path)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (root == null || string.IsNullOrEmpty(path)) return null;
|
|
|
|
|
|
|
|
|
|
|
|
Transform cur = root;
|
|
|
|
|
|
string[] parts = path.Split('/');
|
|
|
|
|
|
foreach (var p in parts)
|
|
|
|
|
|
{
|
|
|
|
|
|
Transform found = null;
|
|
|
|
|
|
for (int i = 0; i < cur.childCount; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.Equals(cur.GetChild(i).name, p, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
{
|
|
|
|
|
|
found = cur.GetChild(i);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (found == null) return null;
|
|
|
|
|
|
cur = found;
|
|
|
|
|
|
}
|
|
|
|
|
|
return cur;
|
2026-08-15 19:39:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 16:53:04 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 非最终死亡(不死爆奖):显示 BJEndUI(倍数+分数)。
|
|
|
|
|
|
///
|
|
|
|
|
|
/// ⭐ 爆奖面板是 UI 预制体 → 必须挂游戏主 UI 面板层(currGameType.subPanelParent.parent),
|
|
|
|
|
|
/// 并用 UI 画布坐标(RectTransform.anchoredPosition)定位;
|
|
|
|
|
|
/// 之前挂 bjEffectRoot(uiRoot 层)用 3D 世界坐标摆放,不在游戏 UI 面板里,
|
|
|
|
|
|
/// 会浮在 3D 上、渲染层级和位置都不对。
|
|
|
|
|
|
/// </summary>
|
2026-08-15 19:39:01 +08:00
|
|
|
|
private void ShowNonFinalDeathBjEndUI(RoomKillNotice obj, int beishu)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (nonFinalDeathBjEndUI == null || obj == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning("[XingJiXunHang] nonFinalDeathBjEndUI 或 obj 为 null,跳过 BJEndUI 显示");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 16:53:04 +08:00
|
|
|
|
var mgr = CreateFishManager.Instance;
|
|
|
|
|
|
if (mgr == null || mgr.currGameType == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning("[XingJiXunHang] CreateFishManager/currGameType 为空,跳过 BJEndUI 显示");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查找击杀者座位:⭐ XJMH 座位/炮台都是 UI 元素,取到的位置已是 UI 世界坐标
|
2026-08-15 19:39:01 +08:00
|
|
|
|
int posID = 0;
|
|
|
|
|
|
Vector3 seatPos = transform.position;
|
2026-09-16 16:53:04 +08:00
|
|
|
|
foreach (var item in mgr.currGameType.list_Sits)
|
2026-08-15 19:39:01 +08:00
|
|
|
|
{
|
|
|
|
|
|
if (item.user == obj.userId)
|
|
|
|
|
|
{
|
|
|
|
|
|
posID = item.posID;
|
|
|
|
|
|
seatPos = item.currentTurret != null
|
|
|
|
|
|
? item.currentTurret.transform.position
|
|
|
|
|
|
: item.transform.position;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 16:53:04 +08:00
|
|
|
|
// 座位上方偏移:0/1 号座位向上,2/3 号座位向下(沿用原逻辑)
|
2026-08-15 19:39:01 +08:00
|
|
|
|
float y = 150f; // 默认 bjEndUIOffsetY
|
|
|
|
|
|
if (posID == 2 || posID == 3) y *= -1f;
|
|
|
|
|
|
|
|
|
|
|
|
bool isMine = obj.userId == GameManager.Instance.user.id;
|
2026-09-16 16:53:04 +08:00
|
|
|
|
|
|
|
|
|
|
// ⭐ UI 父节点:游戏主 UI 面板层(与 CreateFishManager 生成 UI 爆奖特效、其它 Boss 面板一致),
|
|
|
|
|
|
// 取不到才回退 bjEffectRoot
|
|
|
|
|
|
Transform uiParent = mgr.currGameType.subPanelParent != null
|
|
|
|
|
|
? mgr.currGameType.subPanelParent.parent
|
|
|
|
|
|
: null;
|
|
|
|
|
|
if (uiParent == null)
|
|
|
|
|
|
uiParent = mgr.bjEffectRoot;
|
|
|
|
|
|
|
2026-08-20 16:27:42 +08:00
|
|
|
|
// ⭐ 字段类型就是 BJEndUI:Instantiate 返回的 endUI 本身就是 BJEndUI 脚本(预制体必带)
|
2026-09-16 16:53:04 +08:00
|
|
|
|
var endUI = uiParent != null
|
|
|
|
|
|
? Instantiate(nonFinalDeathBjEndUI, uiParent)
|
|
|
|
|
|
: Instantiate(nonFinalDeathBjEndUI);
|
|
|
|
|
|
|
|
|
|
|
|
// ⭐ 用 UI 画布坐标定位(anchoredPosition),不再用 3D 世界坐标
|
|
|
|
|
|
var rect = endUI.GetComponent<RectTransform>();
|
|
|
|
|
|
var uiCanvas = UIManager.Instance != null && UIManager.Instance.uiRoot != null
|
|
|
|
|
|
? UIManager.Instance.uiRoot.GetComponent<Canvas>()
|
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
|
|
if (rect != null && uiCanvas != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// UI 元素世界坐标 → 屏幕坐标:Overlay 画布下世界坐标即屏幕坐标,其它模式走画布摄像机
|
|
|
|
|
|
Vector2 screenPos = uiCanvas.renderMode == RenderMode.ScreenSpaceOverlay
|
|
|
|
|
|
? new Vector2(seatPos.x, seatPos.y)
|
|
|
|
|
|
: RectTransformUtility.WorldToScreenPoint(uiCanvas.worldCamera, seatPos);
|
|
|
|
|
|
|
|
|
|
|
|
Vector2 canvasPos = CoordinateTransformation.ScreenPoint_ToCanvas(screenPos, uiCanvas);
|
|
|
|
|
|
rect.anchoredPosition = new Vector2(canvasPos.x, canvasPos.y + y);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// 兜底:拿不到画布(或非 UI 预制体)时,按原世界坐标摆放
|
|
|
|
|
|
endUI.transform.position = seatPos + Vector3.up * y;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-15 19:39:01 +08:00
|
|
|
|
endUI.Init(beishu, (int)obj.gold, isMine);
|
|
|
|
|
|
|
2026-09-16 16:53:04 +08:00
|
|
|
|
Debug.Log($"[XingJiXunHang] 非最终死亡 BJEndUI(UI层): 倍数={beishu} 分数={obj.gold} " +
|
|
|
|
|
|
$"isMine={isMine} posID={posID} parent={endUI.transform.parent?.name}");
|
2026-08-15 19:39:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 最终死亡 ====================
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>最终死亡:⭐ 直接删除 Boss + 初始化爆奖事件</summary>
|
|
|
|
|
|
private void FinalDeath(RoomKillNotice obj, int beishu)
|
|
|
|
|
|
{
|
|
|
|
|
|
_isFinalDeath = true;
|
|
|
|
|
|
_cachedKillNotice = obj;
|
|
|
|
|
|
isDieFish = true;
|
|
|
|
|
|
|
|
|
|
|
|
// ⭐ 先缓存死亡位置(删除后 transform 位置就不可靠了)
|
|
|
|
|
|
_diePos = transform.position;
|
|
|
|
|
|
_diePosCached = true;
|
|
|
|
|
|
|
|
|
|
|
|
if (obj != null)
|
|
|
|
|
|
poolNum = (int)obj.gold;
|
|
|
|
|
|
|
|
|
|
|
|
// 相机震动 + 死亡音效
|
|
|
|
|
|
GameManager.Instance.cameraShake.ShakePresets.Explosion2D(3f, 4f, 1f);
|
|
|
|
|
|
|
2026-08-20 16:27:42 +08:00
|
|
|
|
// 音效:星舰爆炸(PW_StarShip_Explosion)
|
|
|
|
|
|
AudioManager.Instance.PlayerFXByName("PW_StarShip_Explosion");
|
|
|
|
|
|
|
2026-08-15 19:39:01 +08:00
|
|
|
|
var res = FishResourcesData.fishResourcesDatasDic.ContainsKey(fishTypeID)
|
|
|
|
|
|
? FishResourcesData.fishResourcesDatasDic[fishTypeID]
|
|
|
|
|
|
: null;
|
|
|
|
|
|
if (res != null && res.dieAudio != null && res.dieAudio.Count > 0)
|
|
|
|
|
|
AudioManager.Instance.PlayerFX(res.dieAudio[UnityEngine.Random.Range(0, res.dieAudio.Count)]);
|
|
|
|
|
|
|
|
|
|
|
|
Debug.Log($"[XingJiXunHang] 最终死亡 isKill=true:直接删除 Boss beishu={beishu} gold={obj?.gold} guid={guid} pos={_diePos}");
|
|
|
|
|
|
|
|
|
|
|
|
// 设置爆奖特效初始化回调(爆奖事件用缓存的 _diePos)
|
|
|
|
|
|
action_effect_DieAward = (o) => InitAwardEffect(o, beishu);
|
|
|
|
|
|
|
|
|
|
|
|
// ⭐ isKill=true → 直接从鱼列表删除 Boss(等1帧让 Ws_DieFish 当帧在正确位置创建爆奖特效)
|
|
|
|
|
|
StartCoroutine(RemoveBossNextFrame());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 下一帧删除 Boss:Ws_DieFish 在 DieFish 返回后同帧会实例化爆奖特效
|
|
|
|
|
|
/// (用 Boss 的 transform 定位)并触发 action_effect_DieAward 回调。
|
|
|
|
|
|
/// 等到下一帧:① Boss 一定被删除 ② 此时已知爆奖事件是否初始化成功,
|
|
|
|
|
|
/// 没初始化(座位没找到)则把特效实例回池,防止残留在屏幕上。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private IEnumerator RemoveBossNextFrame()
|
|
|
|
|
|
{
|
|
|
|
|
|
yield return null;
|
|
|
|
|
|
|
|
|
|
|
|
if (!_removed)
|
|
|
|
|
|
{
|
|
|
|
|
|
_removed = true;
|
|
|
|
|
|
|
|
|
|
|
|
// ⭐ 兜底:爆奖事件没初始化成功(击杀者不在座位列表)→ 特效实例回池
|
|
|
|
|
|
// (回调里已 Init 过的不动,由 BJEventBase 自己的流程收尾回收)
|
|
|
|
|
|
if (!_awardInitCalled && currEffect_DieAward != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError($"[XingJiXunHang] 爆奖事件未初始化(座位未找到 userId={_cachedKillNotice?.userId}),回收特效实例");
|
|
|
|
|
|
var effect = currEffect_DieAward;
|
|
|
|
|
|
currEffect_DieAward = null;
|
|
|
|
|
|
EffectPool.Instance.Return(effect);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
CreateFishManager.Instance.Id_DieFish_NotFX(guid);
|
|
|
|
|
|
Debug.Log($"[XingJiXunHang] Boss 已删除(isKill=true 直接回收)guid={guid}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>初始化爆奖事件(由 action_effect_DieAward 回调触发)</summary>
|
|
|
|
|
|
private void InitAwardEffect(GameObject o, int beishu)
|
|
|
|
|
|
{
|
|
|
|
|
|
// ⭐ 用缓存的死亡位置(Boss 下一帧就被删除回池,transform.position 不可靠)
|
|
|
|
|
|
Vector3 diePos = _diePosCached ? _diePos : transform.position;
|
|
|
|
|
|
Debug.Log($"[XingJiXunHang] action_effect_DieAward: 鱼位置={diePos} guid={guid}");
|
|
|
|
|
|
|
|
|
|
|
|
if (_cachedKillNotice == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning("[XingJiXunHang] _cachedKillNotice 为 null,跳过爆奖初始化");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查找击杀者座位,初始化爆奖事件
|
|
|
|
|
|
bool seatFound = false;
|
|
|
|
|
|
foreach (var item in CreateFishManager.Instance.currGameType.list_Sits)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (item.user == _cachedKillNotice.userId)
|
|
|
|
|
|
{
|
|
|
|
|
|
seatFound = true;
|
|
|
|
|
|
_awardInitCalled = true; // ⭐ 标记已 Init(特效实例交给 BJEventBase 流程自己回收)
|
|
|
|
|
|
|
|
|
|
|
|
var v = o.GetComponent<BJEventBase>();
|
|
|
|
|
|
var evt = v as XingJiXunHangEvent;
|
|
|
|
|
|
if (evt != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
evt.bossCtrl = this;
|
|
|
|
|
|
evt.diePos = diePos;
|
|
|
|
|
|
currEvent = evt;
|
|
|
|
|
|
Debug.Log($"[XingJiXunHang] 设置爆奖事件 位置={diePos}");
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError("[XingJiXunHang] 爆奖预制体上没有 XingJiXunHangEvent 组件!" +
|
|
|
|
|
|
"(effect_DieAward 拖的是默认预制体?只走基类 BJEventBase 流程)");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
v.Init(
|
|
|
|
|
|
_cachedKillNotice.gold, // goldCount(最终回加金币)
|
|
|
|
|
|
item.posID, // posID(击杀者座位)
|
|
|
|
|
|
_cachedKillNotice.skillsGold, // paobei(炮倍)
|
|
|
|
|
|
_cachedKillNotice.fishScore, // fishScore(底分)
|
|
|
|
|
|
beishu, // beishu(总倍数,BJEndUI 显示)
|
|
|
|
|
|
JiaFen, // action<int>(加分回调)
|
|
|
|
|
|
() => { v.obj = _cachedKillNotice.param; } // 服务端爆奖参数
|
|
|
|
|
|
);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!seatFound)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError($"[XingJiXunHang] 未找到击杀者座位 userId={_cachedKillNotice.userId},爆奖事件未初始化!" +
|
|
|
|
|
|
"(特效实例将由 RemoveBossNextFrame 回收)");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>爆奖流程正常结束时调用(XingJiXunHangEvent.die())</summary>
|
|
|
|
|
|
public void NotifyBossRemoved()
|
|
|
|
|
|
{
|
|
|
|
|
|
_removed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 参数解析 ====================
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-08-16 00:38:45 +08:00
|
|
|
|
/// 客户端兜底判定(param 缺失或解析失败时使用):
|
|
|
|
|
|
/// 按 maxDeathCount 简单判定死亡次数(类似 LieYanFengBao 模式)。
|
2026-08-15 19:39:01 +08:00
|
|
|
|
/// </summary>
|
2026-08-16 00:38:45 +08:00
|
|
|
|
private (bool isKill, int beishu) GetClientFallback(RoomKillNotice obj)
|
2026-08-15 19:39:01 +08:00
|
|
|
|
{
|
|
|
|
|
|
bool kill = _deathCount >= maxDeathCount;
|
|
|
|
|
|
int bs = obj?.beishu > 0 ? (int)obj.beishu : 1;
|
2026-08-16 00:38:45 +08:00
|
|
|
|
Debug.Log($"[XingJiXunHang] 客户端兜底: 第{_deathCount}次/{maxDeathCount}次 isKill={kill} beishu={bs}");
|
|
|
|
|
|
return (kill, bs);
|
2026-08-15 19:39:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 清理 ====================
|
|
|
|
|
|
|
|
|
|
|
|
private void OnDestroy()
|
|
|
|
|
|
{
|
|
|
|
|
|
StopAllCoroutines();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|