using Assets.MyGame.Scripts.Net.APIResponses.Enum; using Assets.MyGame.Scripts.Net.WebResponses; using DG.Tweening; using System; using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; using static MonsterMoveLine; using Random = UnityEngine.Random; public class MonsterControl_BossBase : MonsterContral { // ==================== 基础 ==================== public string bossName; // ==================== 爆奖 ==================== [Header("爆奖")] public bool isToUI = true; // 爆奖特效是 UI(RectTransform)还是 3D 场景物体 public LayeredPool layeredPool; public TextMeshProUGUI tMP_CurrBossScore; // Boss 分数显示文本 public long currBossScoreValue; // 当前滚动分数 public long bossScoreValue; // 累计总分 public int poolNum; // 奖池金币 public int prizeCount = 8; // 爆奖红包数量 public Action JiaFen; // 加分回调 public List> redPacketRecords = new List>(); public Camera ZhuCam; // ==================== 死亡 ==================== public bool isDieFish; public float dieTime = 3f; public Vector3 v3; // 分数飞出目标位置(Inspector 设置) private long _turretNum; private bool _isJia; /// 是否本地玩家击杀(金/银币样式切换用,由 Ws_DieFish 设置) public bool isLocalCoin = false; /// 击杀者座位 ID(人机击杀时由房主代发,但 sitPos 应为人机座位,而非房主自己) public int killerSitPos = -1; /// /// ⭐ Init 异步防污染令牌:async void Init 里 await Addressables 加载期间, /// Boss 可能被回收并重新生成(对象池复用)。若旧 Init 的 await 完成后继续执行, /// 会用旧的 pathId 覆盖新 Boss 的 moveLine/位置/isInitialized,导致第二次 Boss 打不死。 /// 每次 Init 递增此令牌,await 后若令牌已变,说明已被新 Init 覆盖,直接 return。 /// private int _initToken = 0; // ==================== Unity 生命周期 ==================== public override void Awake() { base.Awake(); //isRotate = false; ZhuCam = GameManager.Instance.ZhuCam; action_effect_DieAward = obj => { tMP_CurrBossScore = obj.transform.GetChild(0).GetComponent(); // ⭐ 按 isLocalCoin 切金/银:自己击杀=金币,他人=银币 ApplyBossScoreStyle(obj.transform); }; } /// /// 按 isLocalCoin 切换 Boss 分数 TMP 的 spriteAsset: /// 自己击杀 → goldSprite /// 他人击杀 → silverSprite /// 优先用 BJImage(新预制体),回退 ScoreNumScrollEvent,再回退直接取 TMP。 /// 需对应 sprite 已在 Inspector 赋值,否则跳过不影响原显示。 /// private void ApplyBossScoreStyle(Transform root) { if (tMP_CurrBossScore == null) return; var bjImage = root.GetComponentInChildren(); if (bjImage != null) { var spriteAsset = isLocalCoin ? bjImage.goldSprite : bjImage.silverSprite; if (spriteAsset != null) tMP_CurrBossScore.spriteAsset = spriteAsset; return; } var scrollEvent = root.GetComponentInChildren(); if (scrollEvent != null) { scrollEvent.SetScoreStyle(isLocalCoin); } } public override void OnEnable() { base.OnEnable(); layeredPool = null; currBossScoreValue = 0; bossScoreValue = 0; redPacketRecords.Clear(); _isJia = false; isDieFish = false; isLocalCoin = false; killerSitPos = -1; var res = FishResourcesData.fishResourcesDatasDic[fishTypeID]; if (res.showAudio != null && res.showAudio.Count > 0) AudioManager.Instance.PlayerFX(res.showAudio[Random.Range(0, res.showAudio.Count)]); } public override void OnDisable() { base.OnDisable(); if (isDieFish) GameManager.Instance.StartCoroutine(YanChiDie()); } // ==================== 初始化 ==================== public async override void Init(string pathId, float time, bool isyuzhen = false, string yzID = "") { // ⭐ 递增令牌:await 后检查此令牌,若已变说明被新 Init 覆盖(对象池复用),直接 return int token = ++_initToken; isyz = isyuzhen; this.yzID = yzID; // ⭐ 缓存 yzID 前缀(Boss 未调 base.Init,需单独缓存) // 避免 UpdateYZMovement 里 _yzPrefix 为空导致鱼阵 Boss 不移动 SetYzPrefix(yzID); // ⭐ 缓存路线编号(Boss 未调 base.Init,lastMoveLineId 需单独保存, // ApplyFacingDirection 的双数路线镜像 routeFlipMirror 按它判定奇偶) lastMoveLineId = pathId; // ⭐ Boss朝向:不随路线切线旋转,按路线编号奇偶翻转X轴 // (单数路线=左→右保持预制体正向,双数路线=右→左翻转X轴为负) if (isMove && IsRouteFlipBoss) ApplyBossRouteFlip(pathId); // ⭐ 创建视频处理:配置了 createVideoClip 时通过 GameMainPanel 的播放器播放 // 播放期间 OnMoveUpdata 会跳过移动,视频结束后才开始走路线 _createVideoDuration = 0f; _isPlayingCreateVideo = false; var videoPlayer = CreateFishManager.Instance?.currGameType?.createVideoPlayer; if (createVideoClip != null && videoPlayer != null) { // 显示视频控件 videoPlayer.gameObject.SetActive(true); videoPlayer.Stop(); videoPlayer.clip = createVideoClip; // ⭐ 注册播放结束回调:视频播完自动隐藏控件 + 重置状态 videoPlayer.loopPointReached -= OnCreateVideoFinished; videoPlayer.loopPointReached += OnCreateVideoFinished; videoPlayer.Play(); _isPlayingCreateVideo = true; } // Boss 进场播报 if (GameManager.Instance.gameData.currGameType == GameType.XJMH) { var handle = Addressables.LoadAssetAsync($"Assets/Art/XJMH/pw/pw_sprite/handbook/{bossName}.png"); await handle.Task; // ⭐ async void Init 防污染:await 期间 Boss 可能被回收(对象池复用)或销毁(退出房间)。 // 检查顺序很重要:先 this == null(被 Destroy),再 activeInHierarchy(被回收),最后令牌 // Unity 重写 == 运算符:被 Destroy 的 MonoBehaviour 比较 == null 返回 true if (this == null || token != _initToken || !gameObject.activeInHierarchy) { if (this != null) Debug.Log($"[MonsterControl_BossBase] Init await 完成,但令牌已变或 Boss 已回收/销毁,跳过: {bossName} token={token} curr={_initToken} active={gameObject.activeInHierarchy}"); return; } if (CreateFishManager.Instance != null && CreateFishManager.Instance.currGameType != null && CreateFishManager.Instance.currGameType.subUIManager != null) { CreateFishManager.Instance.currGameType.subUIManager.ShowBoardBoss(handle.Result, bossName); } } if (isMove) { var bezierData = MonsterMoveLine.dic_BezierCurveDatas[bossName + pathId]; monsterData.moveLine.Init(bezierData, time); // ⭐ isMove=true 的 Boss 初始位置设为曲线起点, // 避免在 moveTime 更新前卡在生成时的 (9999,9999,0) // z 保留当前值:3D Boss 在预制体上配置的深度不被擦掉 transform.position = new Vector3(bezierData.start.x, bezierData.start.y, transform.position.z); } else { // x/y 归零;z 不带——用预制体自带的深度 transform.position = new Vector3(0, 0, transform.position.z); } // ⭐ Boss 重写 Init 未调用 base.Init,需手动标记已初始化并切换到 move 状态, // 否则 OnMoveUpdata 不会被调用(OnUpdate 里 isInitialized=false 会 return),moveTime 不自增 isInitialized = true; ChangeState(CharacterStateType.move); if (transform.childCount == 0) return; Transform child = transform.GetChild(0); Vector3 localPos = child.localPosition; // ⭐ x/y 归零摆正模型;z 深度只在显式配置 depthOffset(≠0)时才覆盖, // 否则保留预制体上设置的 3D 深度(3D Boss 用,之前被强制归零导致深度无效) localPos.x = 0f; if (depthOffset != 0) localPos.z = depthOffset; child.localPosition = localPos; } /// /// 创建视频播放结束回调:隐藏视频控件 + 重置播放状态 /// (用 loopPointReached 事件,比 OnMoveUpdata 检测更可靠, /// 因为 isMove=false 的 Boss 不会进入 OnMoveUpdata 的视频检测分支) /// private void OnCreateVideoFinished(UnityEngine.Video.VideoPlayer player) { _isPlayingCreateVideo = false; if (player != null) { player.Stop(); player.gameObject.SetActive(false); } } // ==================== 爆奖(公共) ==================== /// 确保分数显示文本存在(自动创建) private void EnsureScorePanel() { if (tMP_CurrBossScore != null) return; var prefab = CreateFishManager.Instance.dieFXPrefabAwardTmp; if (prefab == null) return; var parent = CreateFishManager.Instance.currGameType.subPanelParent; if (parent == null) return; var board = Instantiate(prefab, parent); tMP_CurrBossScore = board.transform.GetChild(0).GetComponent(); // ⭐ 按 isLocalCoin 切金/银:自己击杀=金币,他人=银币 ApplyBossScoreStyle(board.transform); } /// 初始化红包分层对象池 private void EnsureLayeredPool() { if (layeredPool == null) layeredPool = new LayeredPool(poolNum, prizeCount); } /// 爆奖:随机红包(min~max 个) public void BigPrize(int min = 4, int max = 8) { EnsureScorePanel(); EnsureLayeredPool(); var records = layeredPool.BossGlodCoinRandrom(tMP_CurrBossScore.transform.position, min, max, killerSitPos); AddRedPacket(records); } /// 爆奖:固定数量红包 public void BigPrizeNotFish(int count) { EnsureScorePanel(); EnsureLayeredPool(); var records = layeredPool.BossGlodCoinNotFish(count); AddRedPacket(records); } /// 爆奖:全屏小鱼数量红包 public void BigPrizeAll() { int smallFishCount = CreateFishManager.Instance.fishDatas_hashCodekey_Small.Count; prizeCount = 1; EnsureScorePanel(); EnsureLayeredPool(); var records = layeredPool.BossGlodCoin(smallFishCount, tMP_CurrBossScore.transform.position, killerSitPos); AddRedPacket(records); } /// 记录红包并开始分数滚动 + 加分 private void AddRedPacket(List records) { redPacketRecords.Add(records); foreach (var r in records) bossScoreValue += r.Amount; _turretNum = CreateFishManager.Instance.currGameType.currSitData.consumeGoldNum; GameManager.Instance.StartCoroutine(ShowBigPrize()); GameManager.Instance.StartCoroutine(JiaFenRoutine()); } // ==================== 分数滚动 ==================== private IEnumerator ShowBigPrize() { if (_isJia) yield break; _isJia = true; yield return new WaitForSeconds(1f); _turretNum = Random.Range((int)(_turretNum / 2), (int)_turretNum + 1); while (currBossScoreValue < bossScoreValue) { currBossScoreValue += _turretNum; if (currBossScoreValue > bossScoreValue) currBossScoreValue = bossScoreValue; tMP_CurrBossScore.text = currBossScoreValue.ToString(); yield return null; } _isJia = false; } private IEnumerator JiaFenRoutine() { yield return new WaitForSeconds(1.5f); Transform parent = tMP_CurrBossScore.transform.parent; GameObject addScorePrefab = CreateFishManager.Instance.dieFXPrefabAwardTmp_AddScore; var records = redPacketRecords[0]; redPacketRecords.RemoveAt(0); foreach (var item in records) { yield return new WaitForSeconds(0.15f); var inst = Instantiate(addScorePrefab, parent.position + Vector3.up * 60, Quaternion.identity, parent); // ⭐ 按 isLocalCoin 切金/银:优先用 ScoreNumScrollEvent,回退直接赋文本 var scrollEvent = inst.GetComponent(); if (scrollEvent != null) { scrollEvent.SetScoreStyle(isLocalCoin); scrollEvent.SetScoreText(CoinScript.NumAdd(item.Amount.ToString())); } else { inst.transform.GetChild(0).GetComponent().text = CoinScript.NumAdd(item.Amount.ToString()); } AudioManager.Instance.PlayerFX(61009); } } // ==================== 死亡 ==================== public override void DieFish(RoomKillNotice obj = null) { isDieFish = true; GameManager.Instance.cameraShake.ShakePresets.Explosion2D(3f, 4f, 1f); if (obj != null) poolNum = (int)obj.gold; var res = FishResourcesData.fishResourcesDatasDic[fishTypeID]; if (res.dieAudio != null && res.dieAudio.Count > 0) AudioManager.Instance.PlayerFX(res.dieAudio[Random.Range(0, res.dieAudio.Count)]); } /// 死亡后延迟飞分到座位 private IEnumerator YanChiDie() { yield return new WaitForSeconds(dieTime); // not-self 爆奖模式下未实例化分数面板和 effect_DieAward,跳过飞分逻辑 // (加分由 BaoJiangNotSelfHandler.OnBaoJiangEnd 处理) if (tMP_CurrBossScore == null || tMP_CurrBossScore.transform.parent == null) yield break; tMP_CurrBossScore.transform.parent .DOMove(v3, 0.5f) .OnComplete(() => { JiaFen?.Invoke(0); Destroy(tMP_CurrBossScore.transform.parent.gameObject); }); } }