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;
}
}
}