Files
5.24BuYu/Assets/MyGame/aa/Game/XJMH/Boss/XingYunLun/XingYunLunEvent.cs
T
2026-09-24 08:49:24 +08:00

588 lines
23 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 Assets.MyGame.Scripts.Net.WebResponses;
using DG.Tweening;
using Newtonsoft.Json.Linq;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using TMPro;
using UnityEngine;
/// <summary>
/// 幸运轮 Boss 爆奖事件(Boss 控制流程模式,参照破坏球 PoHuaiQiuEvent)。
///
/// 新流程(按用户要求的顺序创建规则):
/// 1. Init: 解析服务端参数,填充 _targetSlots,不预创建 item
/// 2. FlyToSeat: 计算 3D item 目标位置,启动顺序协程
/// 3. 顺序协程 SequentialItemFlow:
/// - 创建第1个 item → 移动到目标位置(flyDuration) → 旋转(rotateDuration)
/// - 创建第2个 item → 直接设位置(无移动效果) → 旋转
/// - 创建第3个 item → 直接设位置 → 旋转
/// - ... 依次类推
/// - 最后一个 item 转完后等2秒 → EndExplosion()
///
/// 服务端 param 格式:{ "res": [{optionId, triggerRate, score}], "count": N }
/// optionId = 分值(如 130/300/280),映射到 slotScores 中匹配的格子索引
/// </summary>
public class XingYunLunEvent : BJRedPacketEvent
{
public GameObject BJFXEffectModel; // 爆奖特效模型(转轮预制体)
[Header("爆奖 item")]
[Tooltip("item 之间的 x 轴间隔(世界坐标单位)")]
public float itemGapX = 2f;
[Header("爆奖特效模型旋转")]
[Tooltip("额外旋转圈数(让转盘有转动效果)")]
public int extraSpins = 2;
[Tooltip("旋转动画时长(秒)")]
public float rotateDuration = 1.5f;
[Tooltip("每个格子的爆奖倍数(按格子顺序,共8个)")]
public int[] slotScores = { 50, 100, 230, 80, 280, 300, 130, 250 };
[Tooltip("每个格子的固定角度(按格子顺序,共8个)")]
public float[] slotAngles = { 0f, 45f, 90f, 135f, 180f, 225f, 270f, 315f };
/// <summary>转动位置(世界坐标,由 Boss 传入=死亡位置)</summary>
[HideInInspector]
public Vector3 explosionCenter = Vector3.zero;
/// <summary>已创建的 item(对象池复用需清理)</summary>
private List<GameObject> _items = new List<GameObject>();
/// <summary>服务端下发的选项列表(用于映射格子)</summary>
private List<BossKillOption> _cardParams = new List<BossKillOption>();
/// <summary>每个 item 的目标格子索引</summary>
private List<int> _targetSlots = new List<int>();
[Header("飞向座位上方")]
[Tooltip("座位上方偏移(Canvas 单位)")]
public float seatUpOffset = 150f;
[Tooltip("3D item 在世界坐标中向上偏移,微调位置用")]
public float worldUpOffset = 1f;
[Tooltip("分数面板相对最后一个item的偏移(Canvas坐标,x=水平偏移 y=垂直偏移)")]
public Vector2 scorePanelOffset = new Vector2(0f, 200f);
[Tooltip("第一个 item 飞到座位上方时长(秒)")]
public float flyDuration = 0.8f;
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);
// 定位到 Boss 传入的转动位置
transform.position = explosionCenter;
// Step1: 解析服务端参数
ParseCardParams();
// Step2: 根据 _cardParams 填充 _targetSlots(不再预创建 item)
FillTargetSlots();
// 直接显示最终分数(不滚动,Boss 控制时序)
if (tMP_Score != null)
tMP_Score.text = maxScore.ToString();
}
/// <summary>重写:不移动到屏幕中心,只缓存座位引用(供 isSelf 判断使用)</summary>
protected override void ApplySeatOffset()
{
GetSeatPosition();
}
// ============================================================
#region 解析服务端参数
// ============================================================
private void ParseCardParams()
{
_cardParams.Clear();
if (obj == null)
{
Debug.LogWarning("[XingYunLun] param 为 null");
return;
}
try
{
JToken rootToken = obj is JToken t ? t : JToken.Parse(obj.ToString());
JToken resToken = rootToken["res"] ?? rootToken;
if (resToken is JArray jarr)
_cardParams = jarr.ToObject<List<BossKillOption>>() ?? new List<BossKillOption>();
else if (resToken is JObject jobj)
_cardParams = new List<BossKillOption> { jobj.ToObject<BossKillOption>() };
else
_cardParams = new List<BossKillOption>();
}
catch (Exception e)
{
Debug.LogError($"[XingYunLun] 解析参数失败:{e.Message};原始={obj}");
_cardParams = new List<BossKillOption>();
}
Debug.Log($"[XingYunLun] 解析到 {_cardParams.Count} 个选项: " +
$"[{string.Join(", ", _cardParams.Select(p => p.score))}]");
}
/// <summary>
/// 根据 _cardParams 填充 _targetSlots。
/// </summary>
private void FillTargetSlots()
{
_targetSlots.Clear();
int count = _cardParams.Count;
for (int i = 0; i < count; i++)
{
if (_cardParams[i] != null)
{
int score = (int)_cardParams[i].score;
int slotIdx = FindSlotByScore(score);
_targetSlots.Add(slotIdx);
Debug.Log($"[XingYunLun] item[{i}] score={score} → slot={slotIdx}");
}
else
{
int lastSlot = _targetSlots.Count > 0 ? _targetSlots[_targetSlots.Count - 1] : 0;
_targetSlots.Add(lastSlot);
}
}
}
/// <summary>根据 score 在 slotScores 中查找索引,找不到则返回 0</summary>
private int FindSlotByScore(int score)
{
for (int i = 0; i < slotScores.Length; i++)
{
if (slotScores[i] == score) return i;
}
Debug.LogWarning($"[XingYunLun] score={score} 在 slotScores 中未找到,默认0");
return 0;
}
#endregion
// ============================================================
#region item 创建与清理
// ============================================================
private void ClearAllItems()
{
if (_items == null) return;
foreach (var c in _items)
{
if (c != null) Destroy(c);
}
_items.Clear();
}
#endregion
// ============================================================
#region 飞向座位与顺序旋转
// ============================================================
/// <summary>
/// 爆奖效果飞到玩家座位上方,然后依次创建并旋转每个转轮。
/// </summary>
public void FlyToSeat()
{
float yOffset = seatUpOffset;
if (posID == 2 || posID == 3) yOffset *= -1f;
Vector3 seatWorld = GetSeatPosition() + Vector3.up * yOffset;
var canvasGo = UIManager.Instance.uiRoot;
var canvasComp = canvasGo.GetComponent<Canvas>();
// Step 1: Event(UI) 直接移动到目标 Canvas 坐标
Vector2 canvasTarget = CoordinateTransformation.WorldPos_ToCanvas(seatWorld, canvasComp);
var rt = GetComponent<RectTransform>();
if (rt != null)
{
rt.DOAnchorPos(canvasTarget, flyDuration);
}
else
{
transform.DOMove(seatWorld, flyDuration);
}
// Step 2: 计算 3D item 基础目标位置(轮子中心对准座位点)
RectTransform canvasRT = canvasGo.GetComponent<RectTransform>();
RectTransform seatRT = GetSeatRectTransform();
Vector3 modelBaseTarget;
if (seatRT != null)
{
Vector3 r1 = CoordinateTransformation.UIToWorld(seatRT, canvasRT);
Camera cam = canvasComp.renderMode == RenderMode.ScreenSpaceOverlay ? Camera.main : canvasComp.worldCamera;
float worldHeight = cam.orthographicSize * 2f;
float worldYOffset = yOffset * worldHeight / canvasRT.rect.height;
r1.y += worldYOffset + worldUpOffset;
r1.z = -100f;
int totalCount = _targetSlots.Count;
Debug.Log($"[XingYunLun][坐标转换] UIToWorld={r1} totalCount={totalCount} centerOffset={(totalCount - 1) / 2f * itemGapX}");
modelBaseTarget = r1 - new Vector3((totalCount - 1) / 2f * itemGapX, 0f, 0f);
}
else
{
Debug.LogWarning("[XingYunLun][坐标转换] seatRT 为 null,降级 CalcModelTargetWorldPos");
modelBaseTarget = CalcModelTargetWorldPos(seatWorld, canvasComp);
}
// Step 3: 启动顺序创建+旋转协程
StartCoroutine(SequentialItemFlow(modelBaseTarget));
}
/// <summary>
/// 顺序流程:创建第1个 item → 移动 → 旋转 → 创建第2个 → 旋转 → ... → 最后等2秒 → EndExplosion
/// </summary>
private IEnumerator SequentialItemFlow(Vector3 modelBaseTarget)
{
ClearAllItems();
int totalItems = _targetSlots.Count;
if (totalItems <= 0)
{
Debug.LogWarning("[XingYunLun] _targetSlots 为空,直接结束");
yield return new WaitForSeconds(2f);
EndExplosion();
yield break;
}
// 音效:幸运大转盘出场(PW_FortuneWheel_Show,转轮开始出现)
AudioManager.Instance.PlayerFXByName("PW_FortuneWheel_Show");
// 计算所有 item 的倍数总和(给中间转轮的 BJEndLabel 用)
int totalScoreSum = 0;
for (int s = 0; s < _targetSlots.Count; s++)
{
int si = _targetSlots[s];
totalScoreSum += (si < slotScores.Length) ? slotScores[si] : 0;
}
// 存储每个 item 的原始位置,供 shift 后恢复
var originalPositions = new Dictionary<GameObject, Vector3>();
float shiftAmount = itemGapX * 0.3f;
for (int i = 0; i < totalItems; i++)
{
// 创建 item
// ⭐ 轮子克隆挂 bjEffectRoot(世界空间爆奖特效统一父节点,ExitRoom 退房会清空其子物体):
// 之前不挂父对象落到场景根——爆奖没播完就退房时事件被销毁、协程中断走不到 ClearAllItems,
// 场景根下的轮子没人清 → 永远卡在屏幕上。bjEffectRoot 为原点恒等变换,不影响世界坐标摆放
Transform lunParent = CreateFishManager.Instance != null ? CreateFishManager.Instance.bjEffectRoot : null;
var clone = Instantiate(BJFXEffectModel, lunParent);
clone.transform.rotation = Quaternion.identity;
clone.name = BJFXEffectModel.name + "_item_" + i;
clone.SetActive(true);
_items.Add(clone);
LunItem lunItem = clone.GetComponent<LunItem>();
// 初始关闭所有特效
if (lunItem != null)
{
if (lunItem.obj_XuanZhuanEffect != null) lunItem.obj_XuanZhuanEffect.SetActive(false);
if (lunItem.objs_IEffect != null)
foreach (var e in lunItem.objs_IEffect) if (e != null) e.SetActive(false);
if (lunItem.objs_REffect != null)
foreach (var e in lunItem.objs_REffect) if (e != null) e.SetActive(false);
}
Vector3 targetPos = modelBaseTarget + new Vector3(itemGapX * i, 0f, 0f);
originalPositions[clone] = targetPos;
// 第一个 item:移动到目标位置(有移动效果)
if (i == 0)
{
bool moveDone = false;
clone.transform.DOKill();
clone.transform.DOMove(targetPos, flyDuration)
.OnComplete(() => moveDone = true);
yield return new WaitUntil(() => moveDone);
}
else
{
// 后续 item:直接设位置(无移动效果)
clone.transform.position = targetPos;
}
// === 当前轮子放大到当前大小的1.5倍,其他轮子往旁边移动 ===
Vector3 originalScale = clone.transform.localScale;
Vector3 targetScale = originalScale * 1.5f;
bool scaleUpDone = false;
clone.transform.DOScale(targetScale, 0.2f).SetEase(Ease.OutBack)
.OnComplete(() => scaleUpDone = true);
yield return new WaitUntil(() => scaleUpDone);
// 其他已存在的轮子往旁边移动
for (int j = 0; j < _items.Count; j++)
{
if (_items[j] == clone) continue;
int dir = (j < i) ? -1 : 1;
Vector3 shifted = originalPositions[_items[j]] + new Vector3(dir * shiftAmount, 0f, 0f);
_items[j].transform.DOMove(shifted, 0.2f).SetEase(Ease.OutCubic);
}
yield return new WaitForSeconds(0.2f);
// 激活旋转特效
if (lunItem != null && lunItem.obj_XuanZhuanEffect != null)
lunItem.obj_XuanZhuanEffect.SetActive(true);
// 音效:幸运大转盘开始转动(PW_FortuneWheel_TurnBegin)
AudioManager.Instance.PlayerFXByName("PW_FortuneWheel_TurnBegin");
// 旋转(用固定角度数组,不用公式计算)
int slotIdx = (i < _targetSlots.Count) ? _targetSlots[i] : 0;
float slotAngle = (slotIdx < slotAngles.Length) ? slotAngles[slotIdx] : 0f;
float totalAngle = extraSpins * 360f + slotAngle;
int beishu = (slotIdx < slotScores.Length) ? slotScores[slotIdx] : 0;
Debug.Log($"[XingYunLun] item[{i}] → 格子{slotIdx} ({slotAngle}°) 倍数={beishu} 旋转总角度={totalAngle}°");
bool rotateDone = false;
clone.transform.DOKill();
clone.transform.DORotate(
new Vector3(totalAngle, 0f, 0f),
rotateDuration,
RotateMode.WorldAxisAdd
).SetEase(Ease.OutCubic)
.OnComplete(() =>
{
rotateDone = true;
// 音效:幸运大转盘确认(PW_FortuneWheel_Confirm,停到格子)
AudioManager.Instance.PlayerFXByName("PW_FortuneWheel_Confirm");
// 旋转完成:关闭旋转特效,激活对应格子的 IEffect 和 REffect
if (lunItem != null)
{
if (lunItem.obj_XuanZhuanEffect != null) lunItem.obj_XuanZhuanEffect.SetActive(false);
if (lunItem.objs_IEffect != null && slotIdx < lunItem.objs_IEffect.Count && lunItem.objs_IEffect[slotIdx] != null)
lunItem.objs_IEffect[slotIdx].SetActive(true);
if (lunItem.objs_REffect != null && slotIdx < lunItem.objs_REffect.Count && lunItem.objs_REffect[slotIdx] != null)
lunItem.objs_REffect[slotIdx].SetActive(true);
}
});
yield return new WaitUntil(() => rotateDone);
// === 旋转完成后:恢复当前轮子大小,恢复其他轮子位置 ===
clone.transform.DOScale(originalScale, 0.3f).SetEase(Ease.OutCubic);
for (int j = 0; j < _items.Count; j++)
{
if (_items[j] == clone) continue;
if (originalPositions.ContainsKey(_items[j]))
_items[j].transform.DOMove(originalPositions[_items[j]], 0.3f).SetEase(Ease.OutCubic);
}
yield return new WaitForSeconds(0.3f);
}
// 全部转轮转完后:显示总分面板
CreateScorePanel(totalScoreSum);
// 分数面板显示1秒后销毁
if (_scoreBoard != null)
{
StartCoroutine(DelayDestroyScorePanel(1f));
}
// 所有 item 转完后等2秒结束爆奖
Debug.Log("[XingYunLun] 所有转轮旋转完成,等待2秒后结束");
yield return new WaitForSeconds(2f);
EndExplosion();
}
/// <summary>
/// 获取座位上的 RectTransform(供 UIToWorld 使用)。
/// </summary>
private RectTransform GetSeatRectTransform()
{
if (CreateFishManager.Instance == null || CreateFishManager.Instance.currGameType == null)
return null;
var sits = CreateFishManager.Instance.currGameType.list_Sits;
if (posID < 0 || posID >= sits.Count || sits[posID] == null)
return null;
SitPosData sit = sits[posID];
if (sit.obj_BaoJiangZhongPanelPosition != null)
{
var rt = sit.obj_BaoJiangZhongPanelPosition.GetComponent<RectTransform>();
if (rt != null) return rt;
}
if (sit.currentTurret != null)
{
var rt = sit.currentTurret.GetComponent<RectTransform>();
if (rt != null) return rt;
}
return sit.GetComponent<RectTransform>();
}
/// <summary>
/// 计算 3D 模型目标世界坐标(降级用)。
/// </summary>
private Vector3 CalcModelTargetWorldPos(Vector3 seatWorld, Canvas canvas)
{
RectTransform canvasRT = canvas.GetComponent<RectTransform>();
Camera cam = canvas.renderMode == RenderMode.ScreenSpaceOverlay ? Camera.main : canvas.worldCamera;
Vector2 canvasPos = CoordinateTransformation.WorldPos_ToCanvas(seatWorld, canvas);
Vector3 worldTarget = CoordinateTransformation.CanvasToWorld(canvasPos, canvasRT, cam);
worldTarget.z = -100f;
return worldTarget;
}
#endregion
// ============================================================
#region 分数面板
// ============================================================
/// <summary>
/// 创建分数面板:用 scorePanelPrefab 显示总分,定位在幸运轮上方。
/// </summary>
private void CreateScorePanel(int totalScore)
{
if (scorePanelPrefab == null)
{
Debug.LogWarning("[XingYunLun] scorePanelPrefab 为 null,跳过分数面板创建");
return;
}
if (CreateFishManager.Instance == null || CreateFishManager.Instance.currGameType == null) return;
// ⭐ 与其他 Boss(小丑扑克/黑洞/普通鱼爆奖)保持一致:
// 分数面板挂在 subPanelParent.parent 下,坐标以 uiRoot Canvas 为基准,
// 否则 subPanelParent 与 Canvas 坐标系不一致会导致 scorePanelOffset 偏移不生效。
var parent = CreateFishManager.Instance.currGameType.subPanelParent?.parent;
if (parent == null) return;
var board = Instantiate(scorePanelPrefab, parent);
_scoreBoard = board;
_bjImage = board.GetComponentInChildren<BJImage>();
if (_bjImage != null && _bjImage.tmp_CoinNum != null)
{
tMP_Score = _bjImage.tmp_CoinNum;
}
else
{
var tmp = board.GetComponentInChildren<TextMeshProUGUI>();
if (tmp != null) tMP_Score = tmp;
}
if (tMP_Score != null)
tMP_Score.text = totalScore.ToString();
// 面板定位:最后一个 item 的位置 + offset(Canvas坐标偏移)
Vector3 lastItemPos = _items.Count > 0 ? _items[_items.Count - 1].transform.position : transform.position;
Vector3 worldTarget = lastItemPos + Vector3.up * worldUpOffset;
// ⭐ 与 GetUiCanvas() 同源:以 UIManager.uiRoot 的 Canvas 为转换基准
Canvas canvas = UIManager.Instance != null && UIManager.Instance.uiRoot != null
? UIManager.Instance.uiRoot.GetComponent<Canvas>()
: parent.GetComponentInParent<Canvas>();
if (canvas == null) return;
Vector2 canvasPos = CoordinateTransformation.WorldPos_ToCanvas(worldTarget, canvas);
Vector2 canvasPosBeforeOffset = canvasPos; // 记录加偏移前的坐标
canvasPos += scorePanelOffset;
RectTransform rt = board.GetComponent<RectTransform>();
rt.anchoredPosition = canvasPos;
Debug.Log($"[XingYunLun] 分数面板创建 总分={totalScore} worldPos={worldTarget} " +
$"加偏移前={canvasPosBeforeOffset} scorePanelOffset={scorePanelOffset} 加偏移后={canvasPos} " +
$"父节点={board.transform.parent.name}");
}
/// <summary>
/// 延迟销毁分数面板
/// </summary>
private IEnumerator DelayDestroyScorePanel(float delay)
{
yield return new WaitForSeconds(delay);
if (_scoreBoard != null)
{
Destroy(_scoreBoard);
_scoreBoard = null;
tMP_Score = null;
_bjImage = null;
Debug.Log("[XingYunLun] 分数面板已销毁");
}
}
#endregion
// ============================================================
#region 爆奖结束与回池
// ============================================================
/// <summary>
/// Boss 流程结束后调用:创建 bjEndUI 并结束爆奖。
/// </summary>
public void EndExplosion()
{
if (isScoreEnd) return;
isScoreEnd = true;
// 音效:幸运大转盘结算(PW_FortuneWheel_Settle)
AudioManager.Instance.PlayerFXByName("PW_FortuneWheel_Settle");
// 回加金币到玩家
JiaFen?.Invoke();
// bjEndUI 定位在炮台上方
if (bjEndUI != null)
{
float y = bjEndUIOffsetY;
if (posID == 2 || posID == 3) y *= -1f;
Vector3 uiPos = GetSeatPosition() + Vector3.up * y;
var endUI = Instantiate(bjEndUI, uiPos, Quaternion.identity, GetBJEndUIParent());
endUI.Init(bJbeishu, maxScore, isSelf);
}
// 回收所有 item
ClearAllItems();
// 分数面板由 DelayDestroyScorePanel 协程在1秒后自动销毁,
// 如果协程因 StopAllCoroutines 被中断,这里兜底清理
CleanupScoreBoard();
die();
}
/// <summary>回收爆奖预制体到对象池</summary>
public override void die()
{
CleanupBeforeDie();
EffectPool.Instance.Return(gameObject);
}
private void OnDestroy()
{
StopAllCoroutines();
// ⭐ 兜底清理轮子:协程被中断(退房销毁事件)时走不到 ClearAllItems,
// 虽然轮子已挂 bjEffectRoot 会被 ExitRoom 清空,这里再兜一层防残留
ClearAllItems();
CleanupScoreBoard();
}
/// <summary>清理分数面板(兜底)</summary>
private void CleanupScoreBoard()
{
if (_scoreBoard != null)
{
Destroy(_scoreBoard);
_scoreBoard = null;
tMP_Score = null;
_bjImage = null;
}
}
#endregion
}