Files
5.24BuYu/Assets/MyGame/aa/Game/FLZY/BOSS/BJEventBase.cs
T
2026-08-16 14:58:50 +08:00

600 lines
24 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>
/// Boss 爆奖事件基类
/// 完整流程:死亡特效 → (多次 CallBJ 发红包 → 协程模拟金币逐个到达累分)→ EffEnd →
/// DelayThenScore → StartAddScore → MoveToSit → OnScoreEnd → JiaFen → die
///
/// CallBJ 调用 BigPrize 发包后,不立即累加分数,而是启动实例级协程,
/// 按金币飞行动画的节奏(0.5s 飞行 + 0.1s 间隔)逐个累分到 tMP_Score。
/// 每个 BJEventBase 实例独立管理自己的协程,支持多个 Boss 同时爆奖。
/// </summary>
public class BJEventBase : MonoBehaviour
{
public BJEndUI bjEndUI;
// ==================== Inspector 字段 ====================
[Header("分数")]
public bool useScorePanel = true; // 是否启用分数面板效果(关闭则跳过创建/滚分/移动)
public TextMeshProUGUI tMP_Score; // 分数显示文本(空则自动从预制体创建)
public GameObject scorePanelPrefab; // 分数版预制体(优先;空则用 Manager 自带 dieFXPrefabAwardTmp)
public int currScore; // 当前显示分数
public int maxScore; // 最终总分
public LayeredPool layeredPool; // 红包分层对象池
[Header("红包")]
public int scoreCount; // 红包分包数量(每次 CallBJ 发几个红包)
[Header("玩家")]
public int posID; // 击杀者座位ID
public int paoBei; // 炮倍
public int fishScore; // 底分
public int bJbeishu;
[Header("移动")]
public float showDelay = 3f; // 报完奖后停留时间(让玩家看清爆奖效果)
public float moveDuration = 0.5f; // UI 移动到座位的时间
public Ease moveEase = Ease.OutQuad; // 移动缓动曲线
public Vector3 startOffset = new Vector3(0, 0, -100);
public float bjEndUIOffsetY = 150f; // BJEndUI 在座位上方偏移(useScorePanel=false 时生效)
/// <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;
// ==================== 回调 ====================
public Action JiaFen; // 加分完成回调(回加到玩家金币)
// ==================== 归属 ====================
public bool isSelf; // 是否是自己打出的爆奖(非自己只播特效不加分)
[Header("手动结束")]
[Tooltip("积分加满后是否自动进入结束界面;关闭后需手动调用 ManualFinishBJ 触发")]
public bool autoFinishOnScoreFull = true;
private bool _isWaitingForManualFinish; // 积分已满,等待手动结束
// ==================== 运行时状态 ====================
public int totalBJCount; // 计划爆奖总次数(由 boss 设置)
public int currBJCount; // 已爆奖次数
public bool isEffEnd; // 特效阶段已结束
public bool isScoreStart; // 加分阶段已开始
public bool isScoreEnd; // 加分阶段已结束
public bool isGoldEffectStart; // 红包特效阶段已开始
private SitPosData _targetSit; // 目标座位引用
private Coroutine _scoreCoroutine; // 加分协程
protected GameObject _scoreBoard; // 自动创建的分数版(用于移动)—— protected 供子类清理
protected BJImage _bjImage; // 分数版上的 BJImage 组件(可选,用于切换金币/银币样式)
protected int _activeCoinRoutines; // 正在运行的 CoinArrival 协程数量(protected 供子类兜底检测)
public object obj;
// ==================== 初始化 ====================
/// <summary>
/// 初始化爆奖参数。由 Boss 的 DieFish → action_effect_DieAward 回调中调用。
/// ⭐ 对象池复用安全:每次 Init 重置所有运行时状态,避免上次爆奖残留导致下次流程跳过
/// </summary>
/// <param name="goldCount">服务器返回的总金币</param>
/// <param name="posID">击杀者座位 ID</param>
/// <param name="paobei">炮倍</param>
/// <param name="action">最终加分回调(回加到玩家金币)</param>
/// <param name="action1">初始化完成后的附加回调(如设置龙珠值)</param>
public virtual void Init(long goldCount, int posID, long paobei,long fishScore, int beishu,Action<int> action, Action action1 = null)
{
// ⭐ 重置运行时状态(对象池复用的爆奖对象会残留上次状态,导致 OnScoreEnd 等直接 return)
ResetState();
// ⭐ 从 Inspector 默认值恢复 totalBJCount(原 Start() 逻辑移到这里)
// Start() 只在首次激活时执行一次,对象池复用时不触发,会导致 totalBJCount=0 → 自动结束检测失效
totalBJCount = scoreCount;
JiaFen = () => { action?.Invoke(0); };
maxScore = (int)goldCount;
this.posID = posID;
this.fishScore = (int)fishScore;
paoBei = (int)paobei;
bJbeishu = beishu;
// 自动检测:tMP_Score 为空则按优先级创建分数版
EnsureScoreBoard();
// 初始位置:座位坐标 + 手调偏移
ApplySeatOffset();
// 是否是本地玩家自己打的
isSelf = _targetSit != null && _targetSit.user == GameManager.Instance.user.id;
// 根据 isSelf 切换分数样式(自己=金币,他人=银币)
ApplyScoreStyle();
action1?.Invoke();
}
/// <summary>
/// 重置所有运行时状态(对象池复用时调用,避免上次爆奖残留导致下次流程异常)。
/// 同时清理上次可能残留的分数面板。
/// </summary>
protected virtual void 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 引用残留
// (CleanupBeforeDie 只 Destroy _scoreBoard,不重置 tMP_Score)
tMP_Score = null;
}
// 重置状态标志
isEffEnd = false;
isScoreStart = false;
isScoreEnd = false;
isGoldEffectStart = false;
currScore = 0;
maxScore = 0;
currBJCount = 0;
totalBJCount = 0;
_activeCoinRoutines = 0;
_isWaitingForManualFinish = false;
_scoreCoroutine = null;
_bjImage = null;
_targetSit = null;
obj = null;
// ⭐ 重置红包分层池:对象池复用时旧的 layeredPool 仍持有上次的 maxScore 分包,
// 下次 BigPrize 会用错误的总金额分包,必须置 null 让 BigPrize 按新 maxScore 重建
layeredPool = null;
// 停止所有残留协程
StopAllCoroutines();
}
/// <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>
/// 设置爆奖初始位置:RectTransform → UI 画布中间;普通 Transform → 场景原点 (0,0,0)
/// </summary>
protected virtual void ApplySeatOffset()
{
var rect = GetComponent<RectTransform>();
if (rect != null)
{
var canvas = GetComponentInParent<Canvas>();
if (canvas != null && canvas.renderMode != RenderMode.WorldSpace)
{
Vector2 screenCenter = new Vector2(Screen.width / 2f, Screen.height / 2f);
RectTransformUtility.ScreenPointToWorldPointInRectangle(
canvas.GetComponent<RectTransform>(), screenCenter, canvas.worldCamera, out Vector3 worldPos);
transform.position = worldPos + startOffset;
}
else
{
transform.position = Vector3.zero + startOffset;
}
}
else
{
transform.SetParent(null);
transform.position = Vector3.zero + startOffset;
}
// 缓存座位引用
if (CreateFishManager.Instance == null || CreateFishManager.Instance.currGameType == null) return;
var sits = CreateFishManager.Instance.currGameType.list_Sits;
if (posID >= 0 && posID < sits.Count && sits[posID] != null)
_targetSit = sits[posID];
}
/// <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($"[BJEventBase] 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>
/// 包一个红包分层
/// </summary>
public virtual List<RedPacketRecord> BigPrize(int min = 10, int max = 15)
{
if (layeredPool == null)
layeredPool = new LayeredPool(maxScore, scoreCount > 0 ? scoreCount : 1);
var v = layeredPool.BossGlodCoinRandrom(
tMP_Score != null ? tMP_Score.transform.position : transform.position,
min, max, posID);
int totalAmount = 0;
if (v != null) foreach (var r in v) totalAmount += (int)r.Amount;
Debug.Log($"[BJEventBase] BigPrize: 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>加分完成——加金币、显示结束UI、删面板、销毁自身</summary>
protected virtual void OnScoreEnd()
{
if (isScoreEnd) return;
isScoreEnd = true;
// 回加金币到玩家
JiaFen?.Invoke();
// BJEndUI:无论是否开启分数面板都显示
if (bjEndUI != null)
{
Vector3 uiPos;
if (useScorePanel)
{
// 开启分数面板:定位在分数面板位置
if (_scoreBoard != null)
uiPos = _scoreBoard.transform.position;
else if (tMP_Score != null)
uiPos = tMP_Score.transform.position;
else
uiPos = transform.position;
}
else
{
float y = bjEndUIOffsetY;
if (posID == 2 || posID == 3) y *= -1f;
uiPos = GetSeatPosition() + Vector3.up * y;
}
var endUI = Instantiate(bjEndUI, uiPos, Quaternion.identity, CreateFishManager.Instance.bjEffectRoot);
endUI.Init(bJbeishu, maxScore, isSelf);
}
// 删除分数面板(同步重置引用,避免 Destroy 延迟导致 CleanupBeforeDie 跳过清理)
if (_scoreBoard != null)
{
Destroy(_scoreBoard);
_scoreBoard = null;
tMP_Score = null;
_bjImage = null;
}
// 销毁爆奖对象自身
die();
}
/// <summary>获取座位坐标</summary>
public Vector3 GetSeatPosition()
{
if (_targetSit != null)
{
if (_targetSit.currentTurret != null)
return _targetSit.currentTurret.transform.position;
return _targetSit.transform.position;
}
if (CreateFishManager.Instance != null && CreateFishManager.Instance.currGameType != null)
{
var sits = CreateFishManager.Instance.currGameType.list_Sits;
if (posID >= 0 && posID < sits.Count && sits[posID] != null)
{
_targetSit = sits[posID];
if (_targetSit.currentTurret != null)
return _targetSit.currentTurret.transform.position;
return _targetSit.transform.position;
}
}
return transform.position;
}
// ==================== 手动结束(非自动模式) ====================
/// <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>缩小隐藏爆奖本体(不影响分数面板)</summary>
public virtual void ShrinkEffect()
{
transform.DOScale(Vector3.zero, 0.5f);
}
/// <summary>
/// 清理分数面板并停止协程(子类 die() 回收/销毁前必须调用)。
/// ⭐ 防止 _scoreBoard 泄漏:EnsureScoreBoard 在 subPanelParent 下 Instantiate 的面板
/// 是独立物体,不会随 Event 一起回收。
/// </summary>
protected virtual void CleanupBeforeDie()
{
// 停止所有协程(防止 SetActive(false) 后协程残留)
StopAllCoroutines();
// 清理分数面板
if (_scoreBoard != null)
{
Destroy(_scoreBoard);
_scoreBoard = null;
// ⭐ 同步清除 tMP_Score 引用(它指向 _scoreBoard 的子级,销毁后变成假 null)
tMP_Score = null;
_bjImage = null;
}
}
/// <summary>销毁自身 GameObject(先清理分数面板)</summary>
public virtual void die()
{
CleanupBeforeDie();
Destroy(gameObject);
}
}