479 lines
20 KiB
C#
479 lines
20 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|