Files
back_cantanBuilding/Assets/Scripts/OfferChainsChest/OfferChainsChestPanel.cs
2026-05-26 16:15:54 +08:00

543 lines
22 KiB
C#
Raw Permalink 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 System;
using System.Collections.Generic;
using System.Threading.Tasks;
using asap.core;
using cfg;
using DataCenter;
using game;
using GameCore;
using TMPro;
using UniRx;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.Events;
using UnityEngine.Playables;
using UnityEngine.Serialization;
using UnityEngine.UI;
public class OfferChainsRefreshEvent {}
namespace OfferChainsChest
{
public class OfferChainsChestPanel : MonoBehaviour
{
private OfferChainsChestManager _offerChainsChestManager;
private Timer _timer;
private CompositeDisposable _disposable = new();
// 获得商品结束逻辑
private TaskCompletionSource<bool> _rewardGetTask;
//
private struct MyReward
{
public RewardItemNew RewardItem;
public Transform RewardTrans;
public Transform ChainTrans;
public Transform Chest;
//
public int Status;
}
private List<MyReward> _rewardEntities;
// 有效的数据,随机的时候从这里选
private List<int> _validIds;
private int _purchasedId;
private int _purchasedIndex;
// UI
private TMP_Text _textTime;
private TMP_Text _textTitle;
private Button _btnBuyGreen;
private TMP_Text _textBuyGreen;
// private Button _btnClose;
private Button _btnProbability;
private Transform _reward;
private CanvasGroup _canvasGroup;
private Transform _rewardMain;
private RewardItemNew _mainRewardItemNew;
[Space]
[Header("奖励组件")]
public Transform[] rewardTransGroups;
[Header("播放动画")]
public string aniChestNml;
public string aniChestLocked;
public string aniChestUnlock;
[Header("中间播放解锁动画")]
public string aniChestChanged;
[Header("解锁动画时长")]
public float lotteryUnlockTime = 1.5f;
[Header("抽将到最终奖的间隔")]
public float unlockToFinalDelay = 1.0f;
[Header("最终奖到弹出结算(播放中间箱子解锁的动画)")]
public float finalTimeToPopup = 1.0f;
[Header("—— 基础时长(秒) ——")]
[Tooltip("总时长基础项T = baseTime + perItemTime * n + 随机抖动")]
public float baseTime = 0.80f; // 可调0.72~0.90
[Tooltip("每个剩余格带来的时长增量")]
public float perItemTime = 0.1f; // 可调0.055~0.08
[Tooltip("时长随机抖动幅度(±)")]
public float timeJitter = 0.12f;
[Header("—— 步数估计 ——")]
[Tooltip("期望步数S_desired ≈ stepK * n + stepB ± stepRand")]
public float stepK = 1.5f; // 可调1.5~1.6
public int stepB = 8; // 可调8~10
public int stepRandNeg = -2; // 随机下限
public int stepRandPos = +2; // 随机上限
[Header("—— 步间隔系数(相对 T/S——")]
[Tooltip("最短步间隔系数dt_min = minFactor * (T/S)")]
public float minFactor = 0.35f; // 原建议 0.35(更快);保守 0.5
[Tooltip("最长步间隔系数dt_max = maxFactor * (T/S)")]
public float maxFactor = 3.0f; // 原建议 2.10;保守 2.50
[Header("—— 安全下限(避免卡慢/频闪)——")]
[Tooltip("大池子n≥8最短步间隔下限")]
public float hardMinLargePool = 0.035f; // 35ms保守 0.040
[Tooltip("双格n=2最短步间隔下限")]
public float hardMinTwoPool = 0.075f; // 75ms可 0.070~0.090 之间微调
[Header("—— 仪式感的最小整圈数 ——")]
[Tooltip("n≥6 时的最小整圈数 L_min")]
public int minLapsLarge = 2; // 2 圈
[Tooltip("3≤n≤5 时的最小整圈数 L_min")]
public int minLapsMid = 1; // 1 圈
[Tooltip("n=2 时的最小整圈数 L_min建议更高以打散可预测")]
public int minLapsTwo = 3; // 3 圈
[Header("—— 起步爆发与快段占比 ——")]
[Tooltip("起步爆发的步数前几步更快0 关闭")]
public int burstSteps = 2;
[Tooltip("快段占比0~1剩余为减速段")]
[Range(0.4f, 0.9f)] public float fastPhaseRatio = 0.70f;
[Header("—— n=2 特化 ——")]
[Tooltip("n=2是否进行奇偶反制避免可预测")]
public bool antiParityForTwo = true;
[Tooltip("n=2落点前是否微悬停只加时间不加步")]
public bool microPauseForTwo = true;
[Tooltip("n=2微悬停范围最小值")]
public float microPauseTwoMin = 0.1f;
[Tooltip("n=2微悬停范围最大值")]
public float microPauseTwoMax = 0.15f;
private System.Random rng = new ();
//是否再抽奖
private bool _isRunning = false;
private int _currentIndex = -1; // 当前高亮的索引
private List<ItemData> _buyItems;
private void Awake()
{
_offerChainsChestManager = GContext.container.Resolve<OfferChainsChestManager>();
//
_canvasGroup = transform.Find("root").GetComponent<CanvasGroup>();
// _btnClose = transform.Find("root/btn_close").GetComponent<Button>();
_btnProbability = transform.Find("root/btn_probability").GetComponent<Button>();
_rewardMain = transform.Find("root/reward_main");
_mainRewardItemNew = _rewardMain.Find("reward").GetComponent<RewardItemNew>();
_textTime = transform.Find("root/text_time").GetComponent<TMP_Text>();
_textTitle = transform.Find("root/text_title").GetComponent<TMP_Text>();
_btnBuyGreen = transform.Find("root/btn_buy/btn_green").GetComponent<Button>();
_textBuyGreen = transform.Find("root/btn_buy/btn_green/Ani_Container/p_text").GetComponent<TMP_Text>();
//reward_2 = transform.Find("root/reward_2").GetComponent<RewardItemNew>();
// #region Resolve
// _tables = GContext.container.Resolve<Tables>();
// #endregion
// GContext.OnEvent
GContext.OnEvent<OfferChainsRefreshEvent>().Subscribe(OnRefreshByEvent).AddTo(_disposable);
GContext.OnEvent<RewardPanelClose>().Subscribe(RewardPanelClose).AddTo(_disposable);
}
private void Start()
{
_offerChainsChestManager.IsFirstOpen = false;
_rewardEntities = new List<MyReward>();
for (var i = 0; i < 8; ++i)
{
var kIdx = (i + 1);
var chest = transform.Find($"root/chests/chest{kIdx}");
var trans = //transform.Find($"root/chests/chest{kIdx}/OfferChainsChest_VaultReward");
rewardTransGroups[i];
var chains = transform.Find($"root/chests/chest{kIdx}/chain");
var rewardItem = trans.Find("reward").GetComponent<RewardItemNew>();
_rewardEntities.Add(new MyReward
{
RewardTrans = trans,
ChainTrans = chains,
RewardItem = rewardItem,
Chest = chest,
});
}
AddEvents();
// 填充游戏内容
UpdateItemUI();
//
StartCountDown();
}
private void StartCountDown()
{
//
var timeRemain = _offerChainsChestManager.GetTimeRemain();
var seconds = timeRemain.TotalSeconds;
_timer = this.AttachTimer((float)seconds, null,
async (elapsed) =>
{
var now = _offerChainsChestManager.GetTimeRemain();
_textTime.text = ConvertTools.ConvertTime2(now.Days, now.Hours, now.Minutes, now.Seconds);
if (now.TotalMilliseconds < 0)
{
if (_canvasGroup.blocksRaycasts)
{
_canvasGroup.blocksRaycasts = false;
Settle(); // 活动结束了
_canvasGroup.blocksRaycasts = true;
}
}
}, useRealTime: true);
}
private void AddEvents()
{
// _btnClose.onClick.AddListener(OnClickClose);
_btnBuyGreen.onClick.AddListener(OnClickBuy);
_btnProbability.onClick.AddListener(OnClickProbability);
}
private void Update()
{
#if UNITY_EDITOR
if (Input.GetKeyDown(KeyCode.F))
{
_ = RunFinalRewardLogic();
}
if (Input.GetKeyDown(KeyCode.D))
{
_offerChainsChestManager.ClearOfferChainsData();
_offerChainsChestManager.Init();
}
#endif
}
private void UpdateItemUI()
{
var items = _offerChainsChestManager.OfferChainsData.Items;
for(var i = 0;i < _rewardEntities.Count;++i)
{
var itemData = new ItemData
{
id = items[i].Item,
count = items[i].Number,
};
_rewardEntities[i].RewardItem.SetData(itemData);
UpdateItemUIByStatus(_rewardEntities[i],items[i].Status);
}
_mainRewardItemNew.SetData( new ItemData
{
id = _offerChainsChestManager.OfferChainsData.ChestReward,
count = 1,
});
;
UpdateBuyStatus();
}
private void UpdateBuyStatus()
{
//
var itemId = _offerChainsChestManager.OfferChainsData.ItemId;
if (itemId == -1)
{
_btnBuyGreen.enabled = false;
}
else
{
var iAPItemList = _offerChainsChestManager.GetIapItemListThisTime();
var skuDetailDataEvent = new SKUDetailDataEvent(iAPItemList);
GContext.Publish(skuDetailDataEvent);
_textBuyGreen.text = LocalizationMgr.GetFormatTextValue("OfferChainsChest_VinePanel_1",skuDetailDataEvent.price);
}
}
private void UpdateValidItemUIByStatusAndIndex(int validIdx, int status)
{
var idx = _validIds[validIdx] - 1;
UpdateItemUIByStatus(_rewardEntities[idx],status);
}
private void UpdateItemUIByStatus(MyReward rewardEntity, int status)
{
if (status == 0)
{
rewardEntity.RewardTrans.Find("bg_locked").gameObject.SetActive(true);
rewardEntity.RewardTrans.Find("selected").gameObject.SetActive(false);
rewardEntity.RewardTrans.Find("bg_done").gameObject.SetActive(false);
rewardEntity.RewardTrans.Find("mask_done").gameObject.SetActive(false);
// rewardEntity.ChainTrans.gameObject.SetActive(true);
rewardEntity.Chest.GetComponent<Animation>().Play(aniChestNml);
}
else if (status == 1)
{
rewardEntity.RewardTrans.Find("bg_locked").gameObject.SetActive(false);
rewardEntity.RewardTrans.Find("selected").gameObject.SetActive(true);
rewardEntity.RewardTrans.Find("bg_done").gameObject.SetActive(false);
rewardEntity.RewardTrans.Find("mask_done").gameObject.SetActive(false);
// rewardEntity.ChainTrans.gameObject.SetActive(true);
rewardEntity.Chest.GetComponent<Animation>().Play(aniChestNml);
}
else if (status == 2)
{
rewardEntity.RewardTrans.Find("bg_locked").gameObject.SetActive(false);
rewardEntity.RewardTrans.Find("selected").gameObject.SetActive(false);
rewardEntity.RewardTrans.Find("bg_done").gameObject.SetActive(true);
rewardEntity.RewardTrans.Find("mask_done").gameObject.SetActive(true);
// rewardEntity.ChainTrans.gameObject.SetActive(false);
rewardEntity.Chest.GetComponent<Animation>().Play(aniChestLocked);
}
}
private void OnDestroy()
{
// _btnClose.onClick.RemoveAllListeners();
_disposable?.Dispose();
_disposable = null;
_timer?.Cancel();
_timer = null;
}
private static void Log(object t)
{
Debug.Log($"<color=purple>OfferChainsChestPanel ->{t}</color>");
}
private async Task StartLotteryAsync(int targetIndex)
{
if (_isRunning)
return;
targetIndex = Mathf.Clamp(targetIndex, 0, _validIds.Count - 1);
await LotteryAsync(targetIndex);
}
// —— 工具函数 ——
private static float EaseOutQuad(float t) => 1f - (1f - t) * (1f - t);
private static float Lerp(float a, float b, float t) => a + (b - a) * t;
// RandInt
private int RandInt(int aInclusive, int bInclusive)
{
int lo = Mathf.Min(aInclusive, bInclusive);
int hi = Mathf.Max(aInclusive, bInclusive);
return rng.Next(lo, hi + 1);
}
private float RandFloat(float a, float b) {
float lo = Mathf.Min(a, b);
float hi = Mathf.Max(a, b);
double r = rng.NextDouble(); // [0,1)
return (float)(lo + (hi - lo) * r);
}
private void AdvanceStep(int step ,int maxStep)
{
UpdateValidItemUIByStatusAndIndex(_currentIndex, 0);
_currentIndex = (_currentIndex + step) % maxStep;
UpdateValidItemUIByStatusAndIndex(_currentIndex, 1);
}
private async Task LotteryAsync(int targetIndex)
{
if (_validIds == null || _validIds.Count == 0)
{
_offerChainsChestManager.UIRunning = false;
return;
}
// onAnimationStart?.Invoke();
_isRunning = true;
_canvasGroup.blocksRaycasts = false;
// var n = 8;
var n = _validIds.Count;
_currentIndex = UnityEngine.Random.Range(0, n);
if (n > 1)
{
await RunLotteryLogic(targetIndex);
}
for (int i = 0; i < n; i++)
{
UpdateValidItemUIByStatusAndIndex(i, i == targetIndex ? 1 : 0);
}
Log($"LotteryAsync: {targetIndex} {_currentIndex} {_validIds[_currentIndex] -1}");
await RunGetLotteryAni(targetIndex);
await _rewardGetTask.Task;
if (_offerChainsChestManager.IsCanGetFinalReward())
{
await RunFinalRewardLogic();
}
Log($"LotteryAsync: 状态复原");
_offerChainsChestManager.UIRunning = false;
_canvasGroup.blocksRaycasts = true;
_isRunning = false;
}
private async Task RunLotteryLogic(int targetIndex)
{
var n = _validIds.Count;
// _currentIndex = UnityEngine.Random.Range(0, n);
var currIndex = _currentIndex;
// —— 1) 计算期望步数/总时长(仍按你的既有公式/参数)——
int sDesired = Mathf.RoundToInt(stepK * n + stepB) + RandInt(stepRandNeg, stepRandPos);
float totalTime = baseTime + perItemTime * n + RandFloat(-timeJitter, +timeJitter);
totalTime = Mathf.Max(0.2f, totalTime); // 安全夹紧
// —— 2) 目标对齐S = d + L*n必定落在目标 ——
int d = (targetIndex - currIndex + n) % n;
if (d == 0) d = n; // 避免原地落点(至少走一圈)
int minLaps = (n >= 6) ? minLapsLarge : (n >= 3 ? minLapsMid : minLapsTwo);
int L = Mathf.Max(minLaps, Mathf.CeilToInt((sDesired - d) / (float)n));
int S = d + L * n; // ★ 之后都以 S 为准
// —— 3) 计算步间隔范围 ——
float dtAvg = totalTime / Mathf.Max(1, S);
float dtMin = minFactor * dtAvg;
float dtMax = maxFactor * dtAvg;
// 安全下限:避免过慢/频闪
if (n >= 8) dtMin = Mathf.Max(dtMin, hardMinLargePool);
if (n == 2) dtMin = Mathf.Max(dtMin, hardMinTwoPool);
// —— 4) 主循环:前段快、尾段 easeOutQuad 减速 ——
int burst = Mathf.Clamp(burstSteps, 0, Mathf.Max(0, S - 1));
int fastSteps = Mathf.Clamp(Mathf.RoundToInt(fastPhaseRatio * S), 0, S);
for (int step = 1; step <= S; step++)
{
// UI 层:推进 1 格
// advanceStep?.Invoke();
// UpdateValidItemUIByStatusAndIndex(_currentIndex, 0);
// _currentIndex = (_currentIndex + 1) % n;
// UpdateValidItemUIByStatusAndIndex(_currentIndex, 1);
AdvanceStep(1, n);
float waitSec;
if (step <= burst && burst > 0)
{
// 起步爆发:更快
waitSec = dtMin * 0.70f;
}
else if (step <= fastSteps)
{
// 快段:近似匀速,可加入极轻微抖动
waitSec = dtMin;
}
else
{
// 减速段easeOutQuad 从 dtMin -> dtMax
float t = (step - fastSteps) / Mathf.Max(1f, (S - fastSteps));
t = Mathf.Clamp01(t);
waitSec = Lerp(dtMin, dtMax, EaseOutQuad(t));
}
// n=2最后一步前微悬停只加时间不加步
bool doMicroPause = microPauseForTwo && (n==2 ||step == S - 1);
if (doMicroPause)
{
float extra = UnityEngine.Random.Range(microPauseTwoMin, microPauseTwoMax);
waitSec += Mathf.Max(0f, extra);
}
await Awaiters.Seconds(waitSec);
}
}
private async Task RunFinalRewardLogic()
{
Log("RunFinalRewardLogic-> ");
_rewardMain.GetComponent<Animation>().Play(aniChestChanged);
await Awaiters.Seconds(finalTimeToPopup);
var openPrefab = _offerChainsChestManager.OfferChainsChestMain.ChestOpenPrefab;
var openGo = await Addressables.InstantiateAsync(openPrefab, transform).Task;
openGo.transform.Find("root").GetComponent<PlayableDirector>().stopped += _ =>
{
// 展示最终奖励
Log("RunFinalRewardLogic Finished-> ");
Destroy(openGo);
_offerChainsChestManager.OnGetFinalReward();
UIManager.Instance.DestroyUI(gameObject.name);
};
}
private async Task RunGetLotteryAni(int targetIndex)
{
Log("RunGetLotteryAni-> ");
var targetId = _validIds[targetIndex];
_rewardEntities[targetId - 1].Chest.GetComponent<Animation>().Play(aniChestUnlock);
await Awaiters.Seconds(lotteryUnlockTime);
UpdateValidItemUIByStatusAndIndex(targetIndex, 2);
if (_buyItems is { Count: > 0 })
{
GContext.Publish(new ShowData());
}
_rewardGetTask = new TaskCompletionSource<bool>();
_offerChainsChestManager.FinishLottery(targetId);
UpdateBuyStatus();
Log($"RunGetLotteryAni {targetIndex},{targetId}");
await Awaiters.Seconds(unlockToFinalDelay);
}
// 处理触发概率
private void OnClickProbability()
{
_ = UIManager.Instance.ShowUI(UITypes.OfferChainsRewardsPopupPanel);
}
public async void OnClickBuy()
{
_btnBuyGreen.enabled = false;
var item = _offerChainsChestManager.GetTItemById(_offerChainsChestManager.OfferChainsData.ItemId);
var itemDatas = new List<ItemData>
{
new() { count = item.Number, id = item.Item,curCount = (ulong)GContext.container.Resolve<PlayerItemData>().GetItemCount(item.Item) }
};
_buyItems = itemDatas;
var iapItemList = _offerChainsChestManager.GetIapItemListThisTime();
//.GetIapItemListById(_offerChainsChestManager.OfferChainsData.ItemId);
var dropId = _offerChainsChestManager.GetDropIdById(_offerChainsChestManager.OfferChainsData.ItemId);
var shopBuyTypeData = new ShopBuyTypeData
{
type = ShopBuyType.EventPack,
ID = _offerChainsChestManager.OfferChainsData.EventId,
NoShow = true,
};
SaveLotteryContext();
_offerChainsChestManager.UIRunning = true;
//注意: 购买完成之后,会保存数据到管理器,从Manager 获取数据可能会变化
var result = await GContext.container.Resolve<PlayerShopData>().OnBuy(dropId, shopBuyTypeData, iapItemList, itemDatas);
if (result)
{
await StartLotteryAsync(_purchasedIndex);
}
_btnBuyGreen.enabled = true;
}
private void SaveLotteryContext()
{
_validIds = _offerChainsChestManager.ValidIds;
_purchasedId = _offerChainsChestManager.OfferChainsData.ItemId;
_purchasedIndex = _validIds.IndexOf(_purchasedId);
}
private void OnRefreshByEvent(OfferChainsRefreshEvent evt)
{
Log("OnRefreshByEvent -> ");
// if (_offerChainsChestManager.IsFinished())
// {
// UIManager.Instance.DestroyUI(gameObject.name);
// }
}
private void RewardPanelClose(RewardPanelClose rewardPanelClose)
{
Log("RewardPanelClose -> ");
_rewardGetTask.SetResult(true);
}
private void Settle()
{
UIManager.Instance.DestroyUI(gameObject.name);
}
}
}