Files
5.24BuYu/Assets/MyGame/aa/Game/FLZY/BOSS/BJRedPacketEvent.cs
T
2026-08-22 17:23:07 +08:00

479 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using DG.Tweening;
using System;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using Random = UnityEngine.Random;
/// <summary>
/// 爆奖红包的金币表现模式
/// </summary>
public enum BigPrizeFishMode
{
/// <summary>死鱼:随机选场上鱼死亡,在鱼位置掉金币(原 BossGlodCoinRandrom 行为)</summary>
KillFish = 0,
/// <summary>不死鱼:只分包发红包,无金币特效</summary>
NoFish = 1,
/// <summary>随机爆金币不死鱼:屏幕随机位置爆金币特效,场上鱼不死亡</summary>
CoinNoFish = 2,
}
/// <summary>
/// 积分型爆奖事件(分红包 n 次加积分)。
/// 完整流程:死亡特效 → (多次 CallBJ 发红包 → 协程模拟金币逐个到达累分)→ EffEnd →
/// DelayThenScore → StartAddScore → MoveToSit → OnScoreEnd(基类:JiaFen + BJEndUI + die)
///
/// CallBJ 调用 BigPrize 发包后,不立即累加分数,而是启动实例级协程,
/// 按金币飞行动画的节奏(coinFlyDuration 飞行 + coinArrivalInterval 间隔)逐个累分到 tMP_Score。
/// 每个 BJRedPacketEvent 实例独立管理自己的协程,支持多个 Boss 同时爆奖。
///
/// 子类职责:只决定"何时调 CallBJ、调几次"(动画事件/Timeline/协程触发),其余流程全部复用本类。
/// </summary>
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; // 移动缓动曲线
/// <summary>金币飞行到达面板的模拟时间(秒),应匹配 coinEvent DOMove 时长</summary>
public float coinFlyDuration = 0.5f;
/// <summary>金币之间到达的间隔时间(秒)</summary>
public float coinArrivalInterval = 0.1f;
/// <summary>是否显示加分飘字特效(dieFXPrefabAwardTmp_AddScore);false=只加分不飘字</summary>
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 供子类兜底检测)
// ==================== 初始化 ====================
/// <summary>积分型初始化:基类参数 + 恢复红包总次数 + 创建分数面板 + 金银币样式</summary>
public override void Init(long goldCount, int posID, long paobei, long fishScore, int beishu, Action<int> 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();
}
/// <summary>
/// 重置积分型运行时状态(分数面板、红包池、轮次计数等)。
/// </summary>
protected override void ResetState()
{
base.ResetState();
// 清理上次残留的分数面板(如果上次流程未正常跑完 OnScoreEnd)
// ⭐ 同时重置 tMP_Score:所有预制体 tMP_Score 都是 {fileID:0}(未在 Inspector 赋值),
// 由 EnsureScoreBoard 从 _scoreBoard 子级动态获取。_scoreBoard 被销毁后
// tMP_Score 变成"假 null"(Unity 已销毁但 C# 引用仍在),EnsureScoreBoard 的
// if (tMP_Score != null) 判断在 Destroy 延迟销毁的同一帧内仍为 true,导致跳过创建新面板。
// 必须显式置 null 让 EnsureScoreBoard 下次重建。
if (_scoreBoard != null)
{
Destroy(_scoreBoard);
_scoreBoard = null;
tMP_Score = null;
}
else if (tMP_Score != null)
{
// 上次 die() → CleanupBeforeDie 已销毁 _scoreBoard,但 tMP_Score 引用残留
tMP_Score = 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;
}
/// <summary>
/// 自动补充分数版:tMP_Score 为空 → 优先用 scorePanelPrefab → 否则用 Manager 的 dieFXPrefabAwardTmp
/// 父对象挂在 CreateFishManager.Instance.currGameType.subPanelParent 下(随 UI 卸载自动清理)
/// </summary>
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<BJImage>();
if (_bjImage != null && _bjImage.tmp_CoinNum != null)
{
tMP_Score = _bjImage.tmp_CoinNum;
}
else
{
// 回退:直接获取 TextMeshProUGUI
var tmp = board.GetComponentInChildren<TextMeshProUGUI>();
if (tmp != null) tMP_Score = tmp;
}
}
/// <summary>
/// 根据 isSelf 切换分数 TMP 的 spriteAsset:
/// 自己击杀 → goldSprite(金币样式)
/// 他人击杀 → silverSprite(银币样式)
/// 需要预制体上挂有 BJImage 组件且对应 sprite 已赋值;否则跳过不影响原有显示。
/// </summary>
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;
}
// ==================== 特效相关 ====================
/// <summary>死亡特效阶段结束回调(由动画事件/子类调用)</summary>
public virtual void EffEnd()
{
if (isEffEnd) return;
isEffEnd = true;
// 停留 showDelay 秒让玩家看清爆奖效果 → 开始加分滚动
StartCoroutine(DelayThenScore());
}
private IEnumerator DelayThenScore()
{
yield return new WaitForSeconds(showDelay);
StartAddScore(); // → AddScoreRoutine → MoveToSit → OnScoreEnd
}
/// <summary>触发红包特效 + 开始滚分</summary>
public virtual void GoldEffect()
{
isGoldEffectStart = true;
BigPrize();
}
// ==================== 移动到座位 ====================
/// <summary>分数版飞回到座位上方,爆奖对象本身不动</summary>
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());
});
}
// ==================== 爆奖(发红包) ====================
/// <summary>
/// 执行一次爆奖(发红包)。可在动画事件中多次调用。
/// 每次调用通过 LayeredPool 分发红包数据,然后启动协程按金币飞行节奏逐个累加分数到 tMP_Score。
/// </summary>
/// <param name="minCount">每次最小红包数</param>
/// <param name="maxCount">每次最大红包数</param>
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++;
}
/// <summary>
/// 协程:模拟金币飞行到达后逐个累加分数。
/// 等待 coinFlyDuration(匹配 coinEvent DOMove 0.5s),
/// 然后按 coinArrivalInterval 间隔逐个累加。
/// </summary>
private IEnumerator CoinArrivalRoutine(List<RedPacketRecord> 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<ScoreNumScrollEvent>();
if (scrollEvent != null)
{
scrollEvent.SetScoreStyle(isSelf);
scrollEvent.SetScoreText(CoinScript.NumAdd(r.Amount.ToString()));
}
else
{
// 回退:直接取第一个子物体的 TMP
var tmp = inst.transform.GetChild(0).GetComponent<TextMeshProUGUI>();
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
}
}
}
/// <summary>
/// 包一个红包分层。由 bigPrizeFishMode 决定金币表现:
/// KillFish → BossGlodCoinRandrom:随机选场上鱼死亡,在鱼位置掉金币(默认)
/// NoFish → BossGlodCoinNotFish:只分包发红包,无金币特效
/// CoinNoFish → BossGlodCoinRandromNotFish:屏幕随机位置爆金币特效,场上鱼不死亡
/// </summary>
public virtual List<RedPacketRecord> BigPrize(int min = 10, int max = 15)
{
if (layeredPool == null)
layeredPool = new LayeredPool(maxScore, scoreCount > 0 ? scoreCount : 1);
List<RedPacketRecord> 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;
}
// ==================== 加分滚动 ====================
/// <summary>开始加分滚动动画(子类可 override 自定义)</summary>
public virtual void StartAddScore()
{
if (!useScorePanel)
{
OnScoreEnd();
return;
}
if (_scoreCoroutine != null)
StopCoroutine(_scoreCoroutine);
_scoreCoroutine = StartCoroutine(AddScoreRoutine());
}
/// <summary>默认加分滚动协程</summary>
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();
}
// ==================== 手动结束(非自动模式) ====================
/// <summary>
/// 手动结束爆奖。由离场动画结束帧事件调用。
/// 等待所有积分协程完成后再触发 EffEnd → 后续回收/加分流程。
/// </summary>
public void ManualFinishBJ()
{
foreach (Transform routine in transform)
{
routine.gameObject.SetActive(false);
}
if (!_isWaitingForManualFinish) return;
_isWaitingForManualFinish = false;
StartCoroutine(WaitScoreThenEffEnd());
}
/// <summary>等待积分全部到账后再进入结束流程(避免边加分边回收)</summary>
private IEnumerator WaitScoreThenEffEnd()
{
while (_activeCoinRoutines > 0)
yield return null;
isGoldEffectStart = false;
EffEnd();
}
// ==================== 结算定位 ====================
/// <summary>开启分数面板时 BJEndUI 定位在分数面板位置;否则走基类座位定位</summary>
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();
}
// ==================== 清理 ====================
/// <summary>
/// 停止协程并销毁分数面板。
/// ⭐ 防止 _scoreBoard 泄漏:EnsureScoreBoard 在 subPanelParent 下 Instantiate 的面板
/// 是独立物体,不会随 Event 一起回收。
/// </summary>
protected override void CleanupBeforeDie()
{
// 停止所有协程(防止 SetActive(false) 后协程残留)
base.CleanupBeforeDie();
// 清理分数面板
if (_scoreBoard != null)
{
Destroy(_scoreBoard);
_scoreBoard = null;
// ⭐ 同步清除 tMP_Score 引用(它指向 _scoreBoard 的子级,销毁后变成假 null)
tMP_Score = null;
_bjImage = null;
}
}
}