using DG.Tweening; using System; using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine; using Random = UnityEngine.Random; /// /// 爆奖红包的金币表现模式 /// public enum BigPrizeFishMode { /// 死鱼:随机选场上鱼死亡,在鱼位置掉金币(原 BossGlodCoinRandrom 行为) KillFish = 0, /// 不死鱼:只分包发红包,无金币特效 NoFish = 1, /// 随机爆金币不死鱼:屏幕随机位置爆金币特效,场上鱼不死亡 CoinNoFish = 2, } /// /// 积分型爆奖事件(分红包 n 次加积分)。 /// 完整流程:死亡特效 → (多次 CallBJ 发红包 → 协程模拟金币逐个到达累分)→ EffEnd → /// DelayThenScore → StartAddScore → MoveToSit → OnScoreEnd(基类:JiaFen + BJEndUI + die) /// /// CallBJ 调用 BigPrize 发包后,不立即累加分数,而是启动实例级协程, /// 按金币飞行动画的节奏(coinFlyDuration 飞行 + coinArrivalInterval 间隔)逐个累分到 tMP_Score。 /// 每个 BJRedPacketEvent 实例独立管理自己的协程,支持多个 Boss 同时爆奖。 /// /// 子类职责:只决定"何时调 CallBJ、调几次"(动画事件/Timeline/协程触发),其余流程全部复用本类。 /// public class BJRedPacketEvent : BJEventBase { [Header("分数")] public bool useScorePanel = true; // 是否启用分数面板效果(关闭则跳过创建/滚分/移动) public TextMeshProUGUI tMP_Score; // 分数显示文本(空则自动从预制体创建) public GameObject scorePanelPrefab; // 分数版预制体(优先;空则用 Manager 自带 dieFXPrefabAwardTmp) public int currScore; // 当前显示分数 public LayeredPool layeredPool; // 红包分层对象池 [Header("红包")] public int scoreCount; // 红包分包数量(每次 CallBJ 发几个红包) [Tooltip("爆奖红包的金币表现:死鱼=随机选场上鱼死亡并在鱼位置掉金币;不死鱼=只分包无金币特效;随机爆金币不死鱼=屏幕随机位置爆金币,场上鱼不死")] public BigPrizeFishMode bigPrizeFishMode = BigPrizeFishMode.KillFish; [Header("移动")] public float showDelay = 3f; // 报完奖后停留时间(让玩家看清爆奖效果) public float moveDuration = 0.5f; // UI 移动到座位的时间 public Ease moveEase = Ease.OutQuad; // 移动缓动曲线 /// 金币飞行到达面板的模拟时间(秒),应匹配 coinEvent DOMove 时长 public float coinFlyDuration = 0.5f; /// 金币之间到达的间隔时间(秒) public float coinArrivalInterval = 0.1f; /// 是否显示加分飘字特效(dieFXPrefabAwardTmp_AddScore);false=只加分不飘字 public bool showAddScoreFX = true; [Header("手动结束")] [Tooltip("积分加满后是否自动进入结束界面;关闭后需手动调用 ManualFinishBJ 触发")] public bool autoFinishOnScoreFull = true; private bool _isWaitingForManualFinish; // 积分已满,等待手动结束 // ==================== 运行时状态 ==================== public int totalBJCount; // 计划爆奖总次数(由 Init 从 scoreCount 恢复) public int currBJCount; // 已爆奖次数 public bool isEffEnd; // 特效阶段已结束 public bool isScoreStart; // 加分阶段已开始 public bool isGoldEffectStart; // 红包特效阶段已开始 private Coroutine _scoreCoroutine; // 加分协程 protected GameObject _scoreBoard; // 自动创建的分数版(用于移动)—— protected 供子类清理 protected BJImage _bjImage; // 分数版上的 BJImage 组件(可选,用于切换金币/银币样式) protected int _activeCoinRoutines; // 正在运行的 CoinArrival 协程数量(protected 供子类兜底检测) // ==================== 初始化 ==================== /// 积分型初始化:基类参数 + 恢复红包总次数 + 创建分数面板 + 金银币样式 public override void Init(long goldCount, int posID, long paobei, long fishScore, int beishu, Action action, Action action1 = null) { base.Init(goldCount, posID, paobei, fishScore, beishu, action, action1); // ⭐ 从 Inspector 默认值恢复 totalBJCount(原 Start() 逻辑移到这里): // Start() 只在首次激活时执行一次,对象池复用时不触发,会导致 totalBJCount=0 → 自动结束检测失效 totalBJCount = scoreCount; // 自动检测:tMP_Score 为空则按优先级创建分数版 EnsureScoreBoard(); // 根据 isSelf 切换分数样式(自己=金币,他人=银币) ApplyScoreStyle(); } /// /// 重置积分型运行时状态(分数面板、红包池、轮次计数等)。 /// protected override void ResetState() { base.ResetState(); // 只清理上次"自动创建"的分数面板(_scoreBoard != null 说明是 EnsureScoreBoard 创建的)。 // ⭐ 不能无条件清空 tMP_Score:金龙这类预制体在 Inspector 上拖好了 tMP_Score // 且 useScorePanel=false(自带分数显示、不自动创建面板),清掉后 Update 访问直接 NRE。 // Inspector 自带的引用随预制体本身存活(对象池复用也不会失效),无需重置。 // (自动创建的面板销毁点只有 CleanupBeforeDie 一处,销毁时已同步置空引用,不会残留) if (_scoreBoard != null) { Destroy(_scoreBoard); _scoreBoard = null; tMP_Score = null; _bjImage = null; } // 重置状态标志 isEffEnd = false; isScoreStart = false; isGoldEffectStart = false; currScore = 0; currBJCount = 0; totalBJCount = 0; _activeCoinRoutines = 0; _isWaitingForManualFinish = false; _scoreCoroutine = null; _bjImage = null; // ⭐ 重置红包分层池:对象池复用时旧的 layeredPool 仍持有上次的 maxScore 分包, // 下次 BigPrize 会用错误的总金额分包,必须置 null 让 BigPrize 按新 maxScore 重建 layeredPool = null; } /// /// 自动补充分数版:tMP_Score 为空 → 优先用 scorePanelPrefab → 否则用 Manager 的 dieFXPrefabAwardTmp /// 父对象挂在 CreateFishManager.Instance.currGameType.subPanelParent 下(随 UI 卸载自动清理) /// protected virtual void EnsureScoreBoard() { if (tMP_Score != null) return; if (!useScorePanel) return; if (CreateFishManager.Instance == null || CreateFishManager.Instance.currGameType == null) return; var parent = CreateFishManager.Instance.currGameType.subPanelParent; if (parent == null) return; // 1. 优先用 Inspector 设置的预制体 GameObject prefab = scorePanelPrefab; // 2. 否则用 Manager 自带的 dieFXPrefabAwardTmp if (prefab == null) prefab = CreateFishManager.Instance.dieFXPrefabAwardTmp; if (prefab == null) return; var board = Instantiate(prefab, parent); _scoreBoard = board; // 优先获取 BJImage 组件(新预制体),用其 tmp_CoinNum 作为分数 TMP _bjImage = board.GetComponentInChildren(); if (_bjImage != null && _bjImage.tmp_CoinNum != null) { tMP_Score = _bjImage.tmp_CoinNum; } else { // 回退:直接获取 TextMeshProUGUI var tmp = board.GetComponentInChildren(); if (tmp != null) tMP_Score = tmp; } } /// /// 根据 isSelf 切换分数 TMP 的 spriteAsset: /// 自己击杀 → goldSprite(金币样式) /// 他人击杀 → silverSprite(银币样式) /// 需要预制体上挂有 BJImage 组件且对应 sprite 已赋值;否则跳过不影响原有显示。 /// protected virtual void ApplyScoreStyle() { if (_bjImage == null || tMP_Score == null) return; var spriteAsset = isSelf ? _bjImage.goldSprite : _bjImage.silverSprite; if (spriteAsset != null) tMP_Score.spriteAsset = spriteAsset; } // ==================== 特效相关 ==================== /// 死亡特效阶段结束回调(由动画事件/子类调用) public virtual void EffEnd() { if (isEffEnd) return; isEffEnd = true; // 停留 showDelay 秒让玩家看清爆奖效果 → 开始加分滚动 StartCoroutine(DelayThenScore()); } private IEnumerator DelayThenScore() { yield return new WaitForSeconds(showDelay); StartAddScore(); // → AddScoreRoutine → MoveToSit → OnScoreEnd } /// 触发红包特效 + 开始滚分 public virtual void GoldEffect() { isGoldEffectStart = true; BigPrize(); } // ==================== 移动到座位 ==================== /// 分数版飞回到座位上方,爆奖对象本身不动 public virtual void MoveToSit() { if (!useScorePanel) { OnScoreEnd(); return; } if (_scoreBoard == null && tMP_Score == null) { OnScoreEnd(); return; } float y = bjEndUIOffsetY; if (posID == 2 || posID == 3) y *= -1f; Vector3 seatPos = GetSeatPosition() + Vector3.up * y; Transform moveTarget = _scoreBoard != null ? _scoreBoard.transform : tMP_Score.transform.parent; moveTarget.DOMove(seatPos, moveDuration) .SetEase(moveEase) .OnComplete(() => { DOVirtual.DelayedCall(0.5f, () => OnScoreEnd()); }); } // ==================== 爆奖(发红包) ==================== /// /// 执行一次爆奖(发红包)。可在动画事件中多次调用。 /// 每次调用通过 LayeredPool 分发红包数据,然后启动协程按金币飞行节奏逐个累加分数到 tMP_Score。 /// /// 每次最小红包数 /// 每次最大红包数 public virtual void CallBJ(int minCount = 10, int maxCount = 15) { if (isScoreEnd) return; var records = BigPrize(minCount, maxCount); if (records != null && records.Count > 0) { // 启动协程:模拟金币逐个飞到面板的节奏 _activeCoinRoutines++; StartCoroutine(CoinArrivalRoutine(records)); } else { // ⭐ 诊断日志:BigPrize 返回空(maxScore=0 或 layeredPool 已分完) Debug.LogWarning($"[BJRedPacketEvent] CallBJ({currBJCount + 1}/{totalBJCount}) BigPrize 返回空!maxScore={maxScore} scoreCount={scoreCount} currScore={currScore} layeredPool={layeredPool != null}"); } currBJCount++; } /// /// 协程:模拟金币飞行到达后逐个累加分数。 /// 等待 coinFlyDuration(匹配 coinEvent DOMove 0.5s), /// 然后按 coinArrivalInterval 间隔逐个累加。 /// private IEnumerator CoinArrivalRoutine(List records) { // 等待金币飞行时间(匹配 DOMove) yield return new WaitForSeconds(coinFlyDuration); foreach (var r in records) { currScore += (int)r.Amount; if (tMP_Score != null) tMP_Score.text = currScore.ToString(); // 加分弹出特效 + 音效 Transform parent = tMP_Score != null ? tMP_Score.transform.parent : transform; GameObject addScorePrefab = CreateFishManager.Instance.dieFXPrefabAwardTmp_AddScore; if (showAddScoreFX && addScorePrefab != null) { var inst = Instantiate(addScorePrefab, parent.position + Vector3.up * 60, Quaternion.identity, parent); // 优先用 ScoreNumScrollEvent 组件,按 isSelf 切换金/银币样式 var scrollEvent = inst.GetComponent(); if (scrollEvent != null) { scrollEvent.SetScoreStyle(isSelf); scrollEvent.SetScoreText(CoinScript.NumAdd(r.Amount.ToString())); } else { // 回退:直接取第一个子物体的 TMP var tmp = inst.transform.GetChild(0).GetComponent(); if (tmp != null) tmp.text = CoinScript.NumAdd(r.Amount.ToString()); } } //AudioManager.Instance.PlayerFX(62022); // 金币之间间隔 yield return new WaitForSeconds(coinArrivalInterval); } _activeCoinRoutines--; // 所有协程完成 + 轮次满足 → 决定直接结束还是等手动触发 if (_activeCoinRoutines <= 0 && totalBJCount > 0 && currBJCount >= totalBJCount) { if (autoFinishOnScoreFull) { isGoldEffectStart = false; EffEnd(); // → DelayThenScore → StartAddScore → MoveToSit → OnScoreEnd → JiaFen → die } else { _isWaitingForManualFinish = true; // 等待离场动画结束 → ManualFinishBJ → WaitScoreThenEffEnd → EffEnd } } } /// /// 包一个红包分层。由 bigPrizeFishMode 决定金币表现: /// KillFish → BossGlodCoinRandrom:随机选场上鱼死亡,在鱼位置掉金币(默认) /// NoFish → BossGlodCoinNotFish:只分包发红包,无金币特效 /// CoinNoFish → BossGlodCoinRandromNotFish:屏幕随机位置爆金币特效,场上鱼不死亡 /// public virtual List BigPrize(int min = 10, int max = 15) { if (layeredPool == null) layeredPool = new LayeredPool(maxScore, scoreCount > 0 ? scoreCount : 1); List v; Vector3 point = tMP_Score != null ? tMP_Score.transform.position : transform.position; switch (bigPrizeFishMode) { case BigPrizeFishMode.NoFish: v = layeredPool.BossGlodCoinNotFish(Random.Range(min, max + 1)); break; case BigPrizeFishMode.CoinNoFish: v = layeredPool.BossGlodCoinRandromNotFish(point, min, max, posID); break; default: v = layeredPool.BossGlodCoinRandrom(point, min, max, posID); break; } int totalAmount = 0; if (v != null) foreach (var r in v) totalAmount += (int)r.Amount; Debug.Log($"[BJRedPacketEvent] BigPrize: mode={bigPrizeFishMode} maxScore={maxScore} scoreCount={scoreCount} records={(v != null ? v.Count : 0)} totalAmount={totalAmount}"); return v; } // ==================== 加分滚动 ==================== /// 开始加分滚动动画(子类可 override 自定义) public virtual void StartAddScore() { if (!useScorePanel) { OnScoreEnd(); return; } if (_scoreCoroutine != null) StopCoroutine(_scoreCoroutine); _scoreCoroutine = StartCoroutine(AddScoreRoutine()); } /// 默认加分滚动协程 protected virtual IEnumerator AddScoreRoutine() { if (currBJCount > 0) { // CallBJ 模式:等所有金币协程跑完 while (_activeCoinRoutines > 0) yield return null; // ⭐ 不覆盖 maxScore!保留 Init/子类设置的原始值。 // currScore 是 CoinArrivalRoutine 累加的结果,可能因 LayeredPool 分包舍入 // 或 long→int 截断而与 maxScore 不一致,用 currScore 覆盖会导致最终显示错误。 // 直接显示 maxScore(真实总分)。 if (tMP_Score != null) tMP_Score.text = maxScore.ToString(); } else { // 非 CallBJ 模式(如雷龙): 滚动加分 while (currScore < maxScore - paoBei) { int step = Random.Range(paoBei , paoBei * 2); currScore += step; if (tMP_Score != null) tMP_Score.text = currScore.ToString(); yield return null; } if (tMP_Score != null) tMP_Score.text = maxScore.ToString(); yield return new WaitForSeconds(0.5f); } // 加分完毕 → 分数版飞回座位 → 结束后 JiaFen → die MoveToSit(); } // ==================== 手动结束(非自动模式) ==================== /// /// 手动结束爆奖。由离场动画结束帧事件调用。 /// 等待所有积分协程完成后再触发 EffEnd → 后续回收/加分流程。 /// public void ManualFinishBJ() { foreach (Transform routine in transform) { routine.gameObject.SetActive(false); } if (!_isWaitingForManualFinish) return; _isWaitingForManualFinish = false; StartCoroutine(WaitScoreThenEffEnd()); } /// 等待积分全部到账后再进入结束流程(避免边加分边回收) private IEnumerator WaitScoreThenEffEnd() { while (_activeCoinRoutines > 0) yield return null; isGoldEffectStart = false; EffEnd(); } // ==================== 结算定位 ==================== /// 开启分数面板时 BJEndUI 定位在分数面板位置;否则走基类座位定位 protected override Vector3 GetBJEndUIPosition() { if (useScorePanel) { if (_scoreBoard != null) return _scoreBoard.transform.position; if (tMP_Score != null) return tMP_Score.transform.position; return transform.position; } return base.GetBJEndUIPosition(); } // ==================== 清理 ==================== /// /// 停止协程并销毁分数面板。 /// ⭐ 防止 _scoreBoard 泄漏:EnsureScoreBoard 在 subPanelParent 下 Instantiate 的面板 /// 是独立物体,不会随 Event 一起回收。 /// protected override void CleanupBeforeDie() { // 停止所有协程(防止 SetActive(false) 后协程残留) base.CleanupBeforeDie(); // 清理分数面板 if (_scoreBoard != null) { Destroy(_scoreBoard); _scoreBoard = null; // ⭐ 同步清除 tMP_Score 引用(它指向 _scoreBoard 的子级,销毁后变成假 null) tMP_Score = null; _bjImage = null; } } }