Files
5.24BuYu/Assets/MyGame/aa/Game/FLZY/BOSS/SLZP/SLZPDieEvent.cs
T
2026-09-15 16:30:20 +08:00

594 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 TMPro;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 风雷之渊·神龙转盘 Boss 爆奖事件(三层转盘,参照 RotatePan_XJMH)。
///
/// 升级型三层转盘(停靠路径全部由客户端自算,不依赖服务端 option)。
///
/// 最终倍数:服务端 res.beizhu 优先,否则取顶层 RoomKillNotice.beishu(Init 传入)。
///
/// 升级规则:数值表里的 -1 是“升级格”——
/// 外圈停在 -1 → 升级转中圈;中圈停在 -1 → 升级转内圈;内圈直接出最终倍数。
///
/// 路径自算(从内圈往外圈判断):
/// 1. 内圈表里有该倍数(如 10000/20000)→ 外圈、中圈都停 -1,内圈停该倍数;
/// 2. 中圈、外圈表里都有(如 300/500/600)→ 一半概率外圈直接停,一半概率外圈-1升级到中圈停;
/// 3. 仅中圈有(如 800/1000)→ 外圈停 -1,中圈停该倍数;
/// 4. 仅外圈有(如 350/550/650/700)→ 外圈直接停该倍数。
/// 不参与升级链的圈持续慢转(停靠值=0),停 -1 的圈精确停在升级格。
///
/// 显示:txt_BeiShu / tMP_BeiShu 直接输出最终倍数数字,无任何加减计算。
///
/// 流程:
/// Boss 死亡 → Init(定位到击杀者座位上方偏移处 + 读最终倍数)→ 三层转盘按 外→中→内 依次旋转
/// → 参与的圈精确停在对应格子正点(含 -1 升级格,无任何随机偏移)
/// → 不参与的圈持续慢转 → 全部停稳后显示最终倍数
/// → StartAddScore(RollNumber 快速滚动总分,基类逐帧加底分模式滚不完巨量分数)
/// → MoveToSit → OnScoreEnd → JiaFen → die
///
/// ⚠️ waiValues / zhongValues / neiValues 三张数值表必须与转盘美术各圈格子数值
/// 完全一致(-1 为升级格),否则停靠格子会找错。
/// </summary>
public class SLZPDieEvent : BJRedPacketEvent
{
[Header("转盘")]
public Transform wai; // 外圈转盘
public Transform zhong; // 中圈转盘
public Transform nei; // 内圈转盘
[Header("旋转参数")]
public int spinRounds = 5; // 旋转圈数(到达目标前转的圈数)
public float rotateDuration = 2f; // 单圈旋转总时长
public float waiDir = -1f; // 外圈方向(-1逆时针,1顺时针)
public float zhongDir = 1f; // 中圈方向(-1逆时针,1顺时针)
public float neiDir = -1f; // 内圈方向(-1逆时针,1顺时针)
[Tooltip("不参与升级链的圈(停靠值=0)持续慢转一圈的秒数")]
public float idleSpinDuration = 3f; // 不参与升级链的圈的慢转速度
[Header("三圈数值表(-1=升级格,必须与转盘美术格子数值一致)")]
[Tooltip("外圈数值(wai):抽到-1升级到中圈")]
private int[] waiValues = new int[] { -1, 50, 100, 200, 300, 400, 500, 600 };
[Tooltip("中圈数值(zhong):抽到-1升级到内圈")]
private int[] zhongValues = new int[] { -1, 100, 200, 300, 400, 500 };
[Tooltip("内圈数值(nei):直接出最终倍数")]
private int[] neiValues = new int[] { -1, 10000, 20000 };
[Header("抽中高亮(转盘背后,索引与各数值表一一对应,停哪格亮哪格)")]
[Tooltip("外圈各格抽中效果,索引与 waiValues 一一对应")]
public GameObject[] waiFx;
[Tooltip("中圈各格抽中效果,索引与 zhongValues 一一对应")]
public GameObject[] zhongFx;
[Tooltip("内圈各格抽中效果,索引与 neiValues 一一对应")]
public GameObject[] neiFx;
[Header("座位定位")]
[Tooltip("转盘在座位上方的偏移(UI 大件需要足够偏移避开炮台);2/3号座位在下方,自动翻转向下")]
public float seatOffsetY = 400f;
[Header("转完后加分延迟")]
public float scoreStartDelay = 0.5f; // 转盘停稳后到加分的延迟
[Tooltip("升级链相邻两圈之间的停顿秒数(如外圈停-1后停顿一下再转中圈)")]
public float nextRingDelay = 0.3f; // 升级链圈间停顿
[Header("倍数显示(可选)")]
public TextMeshProUGUI tMP_BeiShu; // 倍数文本(转完后显示最终倍数)
public Text txt_BeiShu; // 倍数文本(转完后显示最终倍数)
private Tween _waiTween;
private Tween _zhongTween;
private Tween _neiTween;
/// <summary>三圈停靠值(0=不参与升级链持续慢转,-1=停在升级格,正值=停在该数值格):0外 1中 2内</summary>
private int[] _stopValues = new int[3];
/// <summary>最终倍数(服务端下发,直接显示,无加减计算)</summary>
private int _totalBeishu;
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);
// 座位上方定位(与 BJEndUI / MoveToSit 同一套座位坐标系):
// 转盘是 UI 大件,需要足够偏移避开炮台;2/3号座位在下方,偏移翻转向下
float y = seatOffsetY;
if (posID == 2 || posID == 3) y *= -1f;
transform.position = GetSeatPosition() + Vector3.up * y;
ParseDragonParam();
LogRings("Init");
// 音效:转盘出现(PW_Dial_Appear)
AudioManager.Instance.PlayerFXByName("PW_Dial_Appear");
StartCoroutine(RotateThenScore());
}
// ============================================================
#region 解析服务端参数
// ============================================================
/// <summary>
/// 解析最终倍数并自算三圈停靠路径(升级型,不依赖服务端 option)。
/// 最终倍数优先级:服务端 res.beizhu → 顶层 RoomKillNotice.beishu(Init 传入)→ 外圈随机正值兜底。
/// 停靠路径按“从内圈往外圈”规则自算,详见 ComputeStopPath。
/// </summary>
private void ParseDragonParam()
{
int target = 0;
if (obj != null)
{
try
{
JToken rootToken = obj is JToken t ? t : JToken.Parse(obj.ToString());
var dragon = BossKillParamParser.ParseDragon(rootToken);
if (dragon != null && dragon.res != null && dragon.res.beizhu > 0)
target = dragon.res.beizhu;
}
catch (Exception e)
{
Debug.LogError($"[SLZP] 解析神龙转盘参数失败:{e.Message};原始={obj}");
}
}
if (target <= 0)
{
// res 为空或未下发 beizhu:用 Init 传入的顶层 beishu(RoomKillNotice.beishu)
target = bJbeishu;
}
if (target <= 0)
{
// 仍无数据:兜底随机一个外圈正值格
target = RandomPositiveValue(waiValues);
Debug.LogWarning($"[SLZP] 服务端未下发最终倍数,兜底随机 target={target}");
}
ComputeStopPath(target);
_totalBeishu = target;
Debug.Log($"[SLZP] 最终倍数={_totalBeishu} 停靠:外={_stopValues[0]} 中={_stopValues[1]} 内={_stopValues[2]}");
}
/// <summary>
/// 从内圈往外圈自算停靠路径(升级型):
/// 1. 内圈表里有该倍数(如 10000/20000)→ 外圈、中圈都停 -1,内圈停该倍数;
/// 2. 中圈、外圈表里都有(如 300/500/600)→ 一半概率外圈直接停,一半概率外圈-1升级中圈停;
/// 3. 仅中圈有(如 800/1000)→ 外圈停 -1,中圈停该倍数;
/// 4. 仅外圈有(如 350/550/650/700)→ 外圈直接停该倍数。
/// 倍数不在任何表中时兜底随机外圈正值格。
/// </summary>
private void ComputeStopPath(int target)
{
bool inWai = IndexOfValue(waiValues, target) >= 0;
bool inZhong = IndexOfValue(zhongValues, target) >= 0;
bool inNei = IndexOfValue(neiValues, target) >= 0;
if (inNei)
{
_stopValues[0] = -1;
_stopValues[1] = -1;
_stopValues[2] = target;
}
else if (inZhong && inWai)
{
if (UnityEngine.Random.Range(0, 2) == 0)
{
_stopValues[0] = target; _stopValues[1] = 0; _stopValues[2] = 0;
}
else
{
_stopValues[0] = -1; _stopValues[1] = target; _stopValues[2] = 0;
}
}
else if (inZhong)
{
_stopValues[0] = -1; _stopValues[1] = target; _stopValues[2] = 0;
}
else if (inWai)
{
_stopValues[0] = target; _stopValues[1] = 0; _stopValues[2] = 0;
}
else
{
_stopValues[0] = RandomPositiveValue(waiValues);
_stopValues[1] = 0;
_stopValues[2] = 0;
Debug.LogWarning($"[SLZP] 倍数 {target} 不在三圈数值表中,兜底随机外圈停靠 {_stopValues[0]}");
}
}
/// <summary>数值在表中的索引;表为空或不在表中返回 -1</summary>
private int IndexOfValue(int[] table, int value)
{
return table == null || table.Length == 0 ? -1 : Array.IndexOf(table, value);
}
/// <summary>随机取表中一个非 -1(非升级格)的值;先随机试 8 次再线性兜底</summary>
private int RandomPositiveValue(int[] table)
{
if (table == null || table.Length == 0) return 0;
for (int i = 0; i < 8; i++)
{
int v = table[UnityEngine.Random.Range(0, table.Length)];
if (v >= 0) return v;
}
for (int i = 0; i < table.Length; i++)
if (table[i] >= 0) return table[i];
return 0;
}
#endregion
// ============================================================
#region 旋转流程
// ============================================================
/// <summary>
/// 打印三圈关键事实:停靠值、表长、每格角度、初始角度、父级旋转。
/// 用于排查"该停 0° 却停到别的格子"类错位。
/// </summary>
private void LogRings(string tag)
{
Transform[] rings = { wai, zhong, nei };
int[][] tables = { waiValues, zhongValues, neiValues };
string[] names = { "外圈", "中圈", "内圈" };
for (int i = 0; i < rings.Length; i++)
{
var r = rings[i];
if (r == null)
{
Debug.Log($"[SLZP][{tag}] {names[i]}: 未赋值");
continue;
}
int len = tables[i] != null ? tables[i].Length : 0;
float parentZ = r.parent != null ? r.parent.eulerAngles.z : 0f;
Debug.Log($"[SLZP][{tag}] {names[i]}({r.name}): 停靠值={_stopValues[i]} 表长={len} 每格={(len > 0 ? 360f / len : 0f):F1}° 初始localZ={r.localEulerAngles.z:F1}° 世界Z={r.eulerAngles.z:F1}° 父级Z={parentZ:F1}°");
}
}
/// <summary>转盘旋转完成后进入加分滚动</summary>
private IEnumerator RotateThenScore()
{
Transform[] rings = { wai, zhong, nei };
int[][] tables = { waiValues, zhongValues, neiValues };
if (rings[0] == null && rings[1] == null && rings[2] == null)
{
Debug.LogWarning("[SLZP] 三圈转盘均未赋值,跳过旋转直接加分");
yield return new WaitForSeconds(scoreStartDelay);
ShowBeishuUI();
yield return new WaitForSeconds(1f);
StartAddScore();
yield break;
}
// 1. 不停靠的圈(0=未停靠)先开始持续慢转(不停,直到爆奖结束回池)
for (int i = 0; i < rings.Length; i++)
{
if (rings[i] == null) continue;
if (_stopValues[i] == 0)
StartIdleSpin(i);
}
// 2. 参与的圈按 外→中→内 依次精确停靠(正点,无随机;0=不参与慢转,-1=停升级格)
for (int i = 0; i < rings.Length; i++)
{
if (rings[i] == null) continue;
if (_stopValues[i] == 0) continue;
bool done = false;
RotateToValue(i, tables[i], _stopValues[i], () => done = true);
yield return new WaitUntil(() => done);
// 停稳 → 激活该圈对应格的抽中效果(该圈其余格全部关闭)
ShowRingFx(i, tables[i], _stopValues[i]);
// 升级链:该圈停稳后、下一参与圈开转前的停顿(如外圈停-1后顿一下再转中圈)
if (nextRingDelay > 0 && HasLaterStop(rings, i))
yield return new WaitForSeconds(nextRingDelay);
}
// 3. 显示最终倍数
ShowBeishuUI();
// 4. 转完后延迟再加分 → MoveToSit → OnScoreEnd → JiaFen → die
if (scoreStartDelay > 0)
yield return new WaitForSeconds(scoreStartDelay);
StartAddScore();
}
/// <summary>
/// 神龙转盘:总分巨大(如 2000 万),基类 AddScoreRoutine 是逐帧加底分模式
/// (Random.Range(paoBei, paoBei*2)),paoBei 为 0 时直接死循环、分数也永远滚不完,
/// 导致 MoveToSit → OnScoreEnd → ShowBJEndUI 全被卡住(EndUI 出不来)。
/// 改用基类 RollNumber 一秒内快速滚动到总分。
/// </summary>
protected override IEnumerator AddScoreRoutine()
{
if (tMP_Score != null)
{
bool done = false;
RollNumber(v => tMP_Score.text = v.ToString(), 0, maxScore, 1f, () => done = true);
yield return new WaitUntil(() => done);
yield return new WaitForSeconds(0.5f);
}
// 加分完毕 → 分数版飞回座位 → 结束后 JiaFen → ShowBJEndUI → die
MoveToSit();
}
/// <summary>显示最终倍数:直接输出服务端下发的数字,不做任何计算;兼容 TMP 与旧版 UGUI Text</summary>
private void ShowBeishuUI()
{
// 音效:转盘展示(PW_Dial_Show)
AudioManager.Instance.PlayerFXByName("PW_Dial_Show");
if (tMP_BeiShu != null)
{
tMP_BeiShu.text = _totalBeishu.ToString();
tMP_BeiShu.gameObject.SetActive(true);
}
if (txt_BeiShu != null)
{
txt_BeiShu.text = _totalBeishu.ToString();
txt_BeiShu.gameObject.SetActive(true);
}
Debug.Log($"[SLZP] 显示最终倍数:{_totalBeishu}");
}
/// <summary>
/// 按数值停靠指定圈:数值 → 格子索引 → 精确角度(索引 × 360/格子数),无随机。
/// </summary>
private void RotateToValue(int ringIndex, int[] table, int value, Action onComplete)
{
Transform circle = GetRing(ringIndex);
if (circle == null)
{
onComplete?.Invoke();
return;
}
float dir = GetDir(ringIndex);
int slot = FindSlotIndex(table, value);
// 正点停靠:格子中心角度 = 索引 × 每格角度,精确无随机
float targetAngle = slot * (360f / (table.Length > 0 ? table.Length : 1));
string ringName = ringIndex == 0 ? "外圈" : ringIndex == 1 ? "中圈" : "内圈";
float curr = circle.localRotation.eulerAngles.z;
Debug.Log($"[SLZP] {ringName} 开始旋转: value={value} slot={slot} 当前={curr:F1}° 目标={targetAngle}° dir={dir} 表长={table.Length} 每格={(360f / (table.Length > 0 ? table.Length : 1)):F1}°");
// 音效:转盘旋转(PW_Dial_Rotate)
AudioManager.Instance.PlayerFXByName("PW_Dial_Rotate");
// 同向到目标的角度差(已带方向符号)
float deltaToTarget = AngleDiff(curr, targetAngle, dir);
// 总增量 = N圈 + 到目标的差值,保证转 N 圈再停在目标
float totalDelta = dir * spinRounds * 360f + deltaToTarget;
// LocalAxisAdd 增量旋转,不取最短路径,保证圈数和方向
Tween tween = circle.DOLocalRotate(new Vector3(0, 0, totalDelta), rotateDuration, RotateMode.LocalAxisAdd);
tween.SetEase(Ease.OutCubic);
tween.OnComplete(() =>
{
// 强制校正到精确目标角度,消除浮点误差
circle.localRotation = Quaternion.Euler(0, 0, targetAngle);
float actual = circle.localRotation.eulerAngles.z;
Debug.Log($"[SLZP] {ringName} 停靠完成: 实际={actual}° 目标={targetAngle}°");
SetTween(ringIndex, null);
onComplete?.Invoke();
});
SetTween(ringIndex, tween);
}
/// <summary>判断 ringIndex 之后是否还有参与停靠的圈(用于升级链圈间停顿)</summary>
private bool HasLaterStop(Transform[] rings, int ringIndex)
{
for (int i = ringIndex + 1; i < rings.Length; i++)
{
if (rings[i] != null && _stopValues[i] != 0) return true;
}
return false;
}
/// <summary>未停靠的圈持续慢转(线性循环,直到销毁清理)</summary>
private void StartIdleSpin(int ringIndex)
{
Transform circle = GetRing(ringIndex);
if (circle == null) return;
KillTween(ringIndex);
Debug.Log($"[SLZP] 圈{ringIndex} 慢转启动: localZ={circle.localEulerAngles.z:F1}° 世界Z={circle.eulerAngles.z:F1}°");
Tween tween = circle.DOLocalRotate(new Vector3(0, 0, GetDir(ringIndex) * 360f), idleSpinDuration, RotateMode.LocalAxisAdd);
tween.SetEase(Ease.Linear);
tween.SetLoops(-1, LoopType.Restart);
SetTween(ringIndex, tween);
}
/// <summary>
/// 停稳后激活该圈对应格的抽中效果(纯 SetActive 切换);
/// 该圈其余格全部关闭。路径上的升级格(-1)同样会亮,展示完整中奖路径。
/// </summary>
private void ShowRingFx(int ringIndex, int[] table, int value)
{
var fx = GetRingFx(ringIndex);
if (fx == null || fx.Length == 0) return;
int slot = FindSlotIndex(table, value);
for (int i = 0; i < fx.Length; i++)
{
if (fx[i] == null) continue;
fx[i].SetActive(i == slot);
}
string ringName = ringIndex == 0 ? "外圈" : ringIndex == 1 ? "中圈" : "内圈";
Debug.Log($"[SLZP] {ringName} 亮格: slot={slot} value={value}");
}
/// <summary>圈索引 → 抽中效果数组</summary>
private GameObject[] GetRingFx(int ringIndex)
{
switch (ringIndex)
{
case 0: return waiFx;
case 1: return zhongFx;
default: return neiFx;
}
}
/// <summary>关闭三圈全部抽中效果</summary>
private void HideAllRingFx()
{
HideRingFx(waiFx);
HideRingFx(zhongFx);
HideRingFx(neiFx);
}
private void HideRingFx(GameObject[] fx)
{
if (fx == null) return;
for (int i = 0; i < fx.Length; i++)
if (fx[i] != null) fx[i].SetActive(false);
}
/// <summary>数值 → 格子索引;找不到返回 0 并警告(兜底停第一格)</summary>
private int FindSlotIndex(int[] table, int value)
{
if (table == null || table.Length == 0) return 0;
int idx = Array.IndexOf(table, value);
if (idx < 0)
{
Debug.LogWarning($"[SLZP] 数值 {value} 不在数值表中,兜底停索引0");
idx = 0;
}
return idx;
}
/// <summary>圈索引 → Transform</summary>
private Transform GetRing(int ringIndex)
{
switch (ringIndex)
{
case 0: return wai;
case 1: return zhong;
default: return nei;
}
}
/// <summary>圈索引 → 旋转方向</summary>
private float GetDir(int ringIndex)
{
switch (ringIndex)
{
case 0: return waiDir;
case 1: return zhongDir;
default: return neiDir;
}
}
/// <summary>圈索引 → Tween</summary>
private void SetTween(int ringIndex, Tween tween)
{
switch (ringIndex)
{
case 0: _waiTween = tween; break;
case 1: _zhongTween = tween; break;
default: _neiTween = tween; break;
}
}
/// <summary>圈索引 → 杀掉残留 Tween</summary>
private void KillTween(int ringIndex)
{
switch (ringIndex)
{
case 0: _waiTween?.Kill(); _waiTween = null; break;
case 1: _zhongTween?.Kill(); _zhongTween = null; break;
default: _neiTween?.Kill(); _neiTween = null; break;
}
}
/// <summary>
/// 计算从 curr 到 target 的同向最短角度差。
/// dir>0(顺时针):diff ∈ [0, 360)
/// dir<0(逆时针):diff ∈ (-360, 0]
/// </summary>
private float AngleDiff(float curr, float target, float dir)
{
float diff = (target - curr) % 360f;
if (diff < 0) diff += 360f; // 归一化到 [0, 360)
if (dir < 0 && diff > 0) diff -= 360f; // 逆时针取负值
return diff;
}
#endregion
// ============================================================
#region 定位(重写基类,禁止脱父级)
// ============================================================
/// <summary>
/// 重写基类 ApplySeatOffset:基类在预制体根节点无 RectTransform 时会
/// transform.SetParent(null) 把特效从 UI 下拽到场景根节点,
/// 导致“勾了 isToUI 也没挂在 UI 上”。这里只缓存座位引用,
/// 位置统一由 Init 中座位上方偏移逻辑处理,绝不脱父级。
/// </summary>
protected override void ApplySeatOffset()
{
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>
/// 对象池复用前重置:杀掉三圈残留 Tween(防复用时还在转)。
/// </summary>
protected override void ResetState()
{
base.ResetState();
KillTween(0);
KillTween(1);
KillTween(2);
HideAllRingFx();
_stopValues = new int[3];
_totalBeishu = 0;
}
/// <summary>
/// 结束时播放结算语音(时来运转,PW_Settle_Dial),
/// 其余走基类(回加金币 + bjEndUI + 清面板 + die)。
/// </summary>
protected override void OnScoreEnd()
{
if (isScoreEnd) return;
// 音效:结算转盘——时来运转语音(PW_Settle_Dial)
AudioManager.Instance.PlayerFXByName("PW_Settle_Dial");
base.OnScoreEnd();
}
/// <summary>清理三圈 Tween 后再走基类清理</summary>
protected override void CleanupBeforeDie()
{
KillTween(0);
KillTween(1);
KillTween(2);
base.CleanupBeforeDie();
}
private void OnDestroy()
{
_waiTween?.Kill();
_zhongTween?.Kill();
_neiTween?.Kill();
}
#endregion
}