Files
ft/Client/Assets/Scripts/EventSmash/UIPanel/EventSmashPanel.cs
2026-06-29 21:18:33 +08:00

714 lines
28 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using asap.core;
using EventSmash.Manager;
using game;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using Activity;
using GameCore;
using TimeManager;
using UniRx;
using UIExtend.Component;
using Game;
namespace EventSmash.UIPanel
{
public class EventSmashPanel : BasePanel
{
#region
private TMP_Text _tokenText;
private Image _tokenIcon;
private TMP_Text _txtTime;
private EventSmashProgressCell[] _progressCells;
private EventSmashCell[] _cellPrefabs;
private RewardFlyBatchController _rewardFlyBatch;
private Button _btnSmashAll;
private Button _btnClose;
private Button _btnInfo;
private Button _btnAdd;
private Button _btnBattlePass;
[Header("九格全开通关(节点名 Image_door_mask")]
[Tooltip("通关遮罩激活后,经过此时长再 RefreshData")]
[SerializeField] private float _stageCompleteRefreshDataDelay = 0.3f;
[Tooltip("已确定领奖队列非空但 ShowUI 异步时,最多再等这么久直到 RewardPopup 显示;仅影响「有弹窗」分支")]
[SerializeField] private float _rewardPopupAsyncOpenMaxWaitSeconds = 2f;
[Tooltip("Legacy Animation 的 clip 名,或 Animator 的状态名(哈希);留空则播默认片段 / 第 0 层默认态")]
[SerializeField]
private string _doorMaskAnimClipName = "EventSmashScrapStageRefresh";
// Prefab/场景里序列化的字符串会覆盖上面默认值;通关遮罩应对齐 Animation 里的「关卡刷新」clip如 EventSmashScrapStageRefresh勿填 ProgressCell 的 EventSmashScrapPiplineCheckRefresh。
private GameObject _imageDoorMask;
#endregion
private EventSmashManager _manager;
private EventSmashData _data;
private IDeferredRewardStashService _stashService;
/// <summary>与砸罐回调同帧写入,<see cref="RefreshAfterSmashAnimAsync"/> 首帧后读取并清除;不依赖 Manager 跨帧标志。</summary>
private bool _stageCompletePendingDoorAnim;
/// <summary>按收集线加锁:不同 collectionId 可并行领奖/涨条;同一条仍串行(避免同一进度条并发写)。</summary>
private readonly object _collectionLocksRoot = new object();
private readonly Dictionary<int, SemaphoreSlim> _collectionClaimLocks = new Dictionary<int, SemaphoreSlim>();
/// <summary>引导表 GuidanceGroupDefine 首步 <c>panelName</c> 须与此一致(与预制体名 EventSmashScrapPanel 对齐)。</summary>
private const string SmashScrapGuidePanelKey = "EventSmashScrapPanel";
#region
private void Awake()
{
_txtTime = gameObject.FindChildGameObject("text_time").GetComponent<TMP_Text>();
_tokenText = gameObject.FindChildGameObject("txt_num").GetComponent<TMP_Text>();
_tokenIcon = gameObject.FindChildGameObject("Img_ticket").GetComponent<Image>();
_progressCells = gameObject.GetComponentsInChildren<EventSmashProgressCell>(true);
_cellPrefabs = gameObject.GetComponentsInChildren<EventSmashCell>(true);
_rewardFlyBatch = gameObject.GetComponentInChildren<RewardFlyBatchController>(true);
_rewardFlyBatch.SetActive(true);
_btnSmashAll = gameObject.FindChildGameObject("btn_smash_all").GetComponent<Button>();
_btnSmashAll.onClick.AddListener(OnSmashAllClick);
_btnClose = gameObject.FindChildGameObject("btn_close").GetComponent<Button>();
_btnClose.onClick.AddListener(OnCloseClick);
_btnInfo = gameObject.FindChildGameObject("p_btn_questionmark").GetComponent<Button>();
_btnInfo.onClick.AddListener(OnInfoClick);
_btnAdd = gameObject.FindChildGameObject("btn_add").GetComponent<Button>();
_btnAdd.onClick.AddListener(OnAddClick);
var battlePassGo = gameObject.FindChildGameObject("btn_mini_battlepass");
if (battlePassGo != null)
{
_btnBattlePass = battlePassGo.GetComponent<Button>();
if (_btnBattlePass != null)
_btnBattlePass.onClick.AddListener(OnBattlePassClick);
}
// 与 EventBossFightMainPanel 一致:礼包/通用领奖面板关闭后同步代币(购买加票等)
GContext.OnEvent<RewardPanelClose>().Subscribe(OnRewardPanelClose).AddTo(disposables);
_imageDoorMask = gameObject.FindChildGameObject("Image_door_mask");
if (_imageDoorMask != null)
_imageDoorMask.SetActive(false);
}
protected override void Start()
{
base.Start();
InitManager();
RefreshData();
TryTriggerSmashScrapOpenGuide();
TrySettleAllPendingCollectionClaimsOnOpen();
UpdateTimer(0L);
GContext.container.Resolve<ITimeTickService>()?.SecondTick?.Subscribe(UpdateTimer).AddTo(disposables);
disposables.Add(UniRx.Disposable.Create(() =>
{
lock (_collectionLocksRoot)
{
foreach (var kv in _collectionClaimLocks)
{
try { kv.Value.Dispose(); }
catch (Exception) { /* ignored */ }
}
_collectionClaimLocks.Clear();
}
}));
}
private void UpdateTimer(long obj)
{
_txtTime.text = ConvertTools.ConvertTime2(_data.RemainingTime);
}
private void InitManager()
{
_manager = ActivityResolver.Resolve<EventSmashManager>();
_data = _manager.LoadData();
_stashService = GContext.container.Resolve<IDeferredRewardStashService>();
}
/// <summary>每期活动在新数据初始化时已 ClearGuide此处打开面板尝试触发一次引导组 Smash01。</summary>
private void TryTriggerSmashScrapOpenGuide()
{
if (_data == null || !_data.IsActive || _data.MainConfig == null)
return;
GContext.container.Resolve<GuideDataCenter>()
.TriggerGuide(GroupName.Smash01.ToString(), SmashScrapGuidePanelKey, false, gameObject.name);
}
private void RefreshData()
{
if (_data == null || !_data.IsActive) return;
var mainConfig = _data.MainConfig;
if (mainConfig == null) return;
UpdateTokenDisplay();
UpdateProgressDisplay();
UpdateGrids();
UpdateSmashAllButton();
UpdateBattlePassButton();
}
private void UpdateTokenDisplay()
{
if (_tokenText != null && _data != null)
{
int tokenCount = _manager.GetCurrentTokenCount(_data);
_tokenText.text = tokenCount.ToString();
}
if (_tokenIcon != null)
{
var itemConfig = _manager.Tables.TbItem.GetOrDefault(_data.MainConfig.ItemID);
GContext.container.Resolve<IUIService>().SetImageSprite(_tokenIcon, itemConfig.Icon);
}
}
/// <summary>代币数量变化时刷新顶部数量与「一键砸」可用状态(与 BossFight 消耗/领奖后立刻刷新一致)。</summary>
private void RefreshTokenUiAfterBalanceChange()
{
if (_data == null || !_data.IsActive) return;
UpdateTokenDisplay();
UpdateSmashAllButton();
}
private void OnRewardPanelClose(RewardPanelClose _)
{
RefreshTokenUiAfterBalanceChange();
}
private void UpdateProgressDisplay()
{
if (_progressCells == null || _data == null || _manager == null) return;
if (_data.MainConfig == null) return;
var collectionList = _data.Tables.TbEventSmashCollection.DataList;
for (int i = 0; i < _progressCells.Length && i < collectionList.Count; i++)
{
var cell = _progressCells[i];
if (cell == null) continue;
cell.Initialize(collectionList[i].ID, _manager, _data);
}
}
private void UpdateGrids()
{
if (_cellPrefabs == null || _data == null) return;
for (int i = 0; i < _cellPrefabs.Length; i++)
{
var cell = _cellPrefabs[i];
if (cell == null) continue;
cell.Initialize(_data, i, TrySmashSingle, OnSmashAnimComplete,
(collectionId, fromReward, count) => RunCollectionRewardFlyAsync(collectionId, fromReward, count),
ProcessCollectionClaimsAfterFlyAsync);
}
}
private EventSmashProgressCell GetProgressCellByCollectionId(int collectionId)
{
if (_progressCells == null || _data?.Tables.TbEventSmashCollection?.DataList == null)
return null;
var list = _data.Tables.TbEventSmashCollection.DataList;
for (int i = 0; i < list.Count && i < _progressCells.Length; i++)
{
if (list[i].ID == collectionId)
return _progressCells[i];
}
return null;
}
private SemaphoreSlim GetCollectionClaimLock(int collectionId)
{
lock (_collectionLocksRoot)
{
if (!_collectionClaimLocks.TryGetValue(collectionId, out var sem))
{
sem = new SemaphoreSlim(1, 1);
_collectionClaimLocks[collectionId] = sem;
}
return sem;
}
}
private async Task RunClaimLoopForCollectionLockedAsync(int collectionId, int flyGrantCount = 0,
bool isSingleSmashOpen = false, bool flushStashAfter = true)
{
var sem = GetCollectionClaimLock(collectionId);
await sem.WaitAsync();
try
{
await RunClaimLoopForCollectionIdAsync(collectionId, flyGrantCount, isSingleSmashOpen, flushStashAfter);
}
finally
{
sem.Release();
}
}
/// <summary>收集物飞入后处理达标领奖;不同收集线互不阻塞,可并行涨进度。</summary>
private Task ProcessCollectionClaimsAfterFlyAsync(int collectionId, int flyGrantCount, bool isSingleSmashOpen)
{
if (_manager == null || _data == null)
return Task.CompletedTask;
return RunClaimLoopForCollectionLockedAsync(collectionId, flyGrantCount, isSingleSmashOpen);
}
private async Task RunClaimLoopForCollectionIdAsync(int collectionId, int flyGrantCount,
bool isSingleSmashOpen, bool flushStashAfter = true)
{
var progressCell = GetProgressCellByCollectionId(collectionId);
if (progressCell != null && !_manager.CanClaimCollectionTier(_data, collectionId))
{
await progressCell.PlayCollectionProgressVisualCatchUpAsync(_manager, _data, collectionId);
}
int tierCount = _manager.GetClaimableCollectionTierCount(_data, collectionId);
if (tierCount <= 0)
return;
var collectionConfig = _manager.Tables.TbEventSmashCollection.GetOrDefault(collectionId);
if (collectionConfig == null || collectionConfig.CollectingRequirement.Count == 0)
return;
if (progressCell != null)
{
progressCell.EnterClaimPresentationMode();
try
{
int amt = _data.GetCollectionData(collectionId);
int round = _data.CollectionProgressPairs.TryGetValue(collectionId, out var pair) ? pair.round : 0;
for (int i = 0; i < tierCount; i++)
{
int req = collectionConfig.CollectingRequirement[round % collectionConfig.CollectingRequirement.Count];
if (i == 0)
{
await progressCell.PlayCollectionFillAndCheckFirstTierAsync(
flyGrantCount,
isSingleSmashOpen && flyGrantCount > 0);
}
else
{
await progressCell.PlayCollectionFillAndCheckSubsequentTierAsync(amt, req, collectionConfig);
}
amt -= req;
round++;
}
for (int i = 0; i < tierCount; i++)
_manager.ClaimOneCollectionTierToStash(_data, collectionId);
await progressCell.PlayCollectionPostGrantVisualAsync();
}
finally
{
progressCell.ExitClaimPresentationMode();
}
if (flushStashAfter)
_stashService?.Flush();
return;
}
while (_manager.CanClaimCollectionTier(_data, collectionId))
_manager.ClaimOneCollectionTierToStash(_data, collectionId);
if (flushStashAfter)
_stashService?.Flush();
UpdateProgressDisplay();
}
/// <summary>进面板时补结算(例如上次领奖动效中途关闭);各收集线并行处理。</summary>
private async void TrySettleAllPendingCollectionClaimsOnOpen()
{
if (_manager == null || _data == null)
return;
var list = _data.Tables.TbEventSmashCollection?.DataList;
if (list == null || list.Count == 0)
return;
try
{
await Task.WhenAll(list.Select(cfg =>
RunClaimLoopForCollectionLockedAsync(cfg.ID, 0, false, flushStashAfter: false)));
_stashService?.Flush(RewardType.NormalOne, true);
}
catch (Exception e)
{
Debug.LogError(e);
}
}
private RectTransform GetCollectionFlyTarget(int collectionId)
{
if (_progressCells == null || _data?.Tables.TbEventSmashCollection?.DataList == null)
return null;
var list = _data.Tables.TbEventSmashCollection.DataList;
for (int i = 0; i < list.Count && i < _progressCells.Length; i++)
{
if (list[i].ID == collectionId)
return _progressCells[i] != null ? _progressCells[i].GetProgressIconRect() : null;
}
return null;
}
/// <summary>收集物:从格子 reward 飞到对应进度图标。</summary>
private Task RunCollectionRewardFlyAsync(int collectionId, RewardItemNew fromReward, int count)
{
if (_rewardFlyBatch == null || fromReward == null || _data?.MainConfig == null)
return Task.CompletedTask;
var targetRt = GetCollectionFlyTarget(collectionId);
if (targetRt == null)
return Task.CompletedTask;
var iconName = _data.MainConfig.GetCollectionIcon(_data.Tables.TbEventSmashCollection, collectionId);
if (string.IsNullOrEmpty(iconName))
return Task.CompletedTask;
var request = new BatchedRewardFlyRequest
{
Icon = new BatchedRewardFlyRequestIcon { iconName = iconName },
Quantity = count > 1 ? count : null,
StartPoint = new BatchedRewardFlyStartPoint(fromReward),
EndPoint = new BatchedRewardFlyEndPoint(targetRt),
IsDestinationRewardStash = false,
AnimationParamIndex = 0,
};
return _rewardFlyBatch.OnRewardFlyRequestAsync(request);
}
private void UpdateSmashAllButton()
{
if (_data.MainConfig == null) return;
int unopenedCount = 9 - _data.OpenedGrids.Count;
_btnSmashAll.enabled = unopenedCount>0;
}
private void UpdateBattlePassButton()
{
if (_btnBattlePass == null) return;
var main = _data?.MainConfig;
bool show = main != null && main.BattlePassId > 0 && !string.IsNullOrEmpty(main.BattlePassPanel);
_btnBattlePass.gameObject.SetActive(show);
}
#endregion
#region
private bool TrySmashSingle(int gridIndex)
{
if (_manager == null || _data == null) return false;
bool success = _manager.SmashSingle(_data, gridIndex, out bool stageJustCompleted);
if (success)
{
if (stageJustCompleted)
_stageCompletePendingDoorAnim = true;
SetInteractable(false);
RefreshTokenUiAfterBalanceChange();
}
return success;
}
private void OnSmashAnimComplete()
{
// 推迟一帧再刷新:避免刚结束 Timeline/Animation 的格子与 Initialize 在同一帧写 Transform 竞态(常见表现为最后一格通关后不回到未开启态)。
_ = RefreshAfterSmashAnimAsync();
}
private async Task RefreshAfterSmashAnimAsync()
{
var stageCompletedThisRefresh = false;
var didRefreshGridsThisRefresh = false;
try
{
await Awaiters.NextFrame;
bool stageComplete = _stageCompletePendingDoorAnim;
_stageCompletePendingDoorAnim = false;
stageCompletedThisRefresh = stageComplete;
if (stageComplete && _imageDoorMask != null)
{
try
{
// ShowData 后由 WaitUntil… 用 CurRewardQCount 判断是否真有领奖弹窗,再决定等不等关闭
await WaitUntilSmashRewardPopupReleasedAsync();
_imageDoorMask.SetActive(true);
float clipDuration = await StartDoorMaskVisualAndGetDurationAsync();
await Awaiters.Seconds(_stageCompleteRefreshDataDelay);
// 须含 UpdateGrids通关时数据已清空 OpenedGrids 并生成新 GridRewardMap。
RefreshData();
didRefreshGridsThisRefresh = true;
float remaining = Mathf.Max(0f, clipDuration - _stageCompleteRefreshDataDelay);
if (remaining > 0f)
await Awaiters.Seconds(remaining);
else if (clipDuration <= 0f)
await Awaiters.NextFrame;
}
finally
{
_imageDoorMask.SetActive(false);
}
}
else
{
RefreshData();
didRefreshGridsThisRefresh = true;
}
}
catch (Exception e)
{
Debug.LogException(e);
}
finally
{
// 通关流程中若等待弹窗/播片异常提前退出,避免九格永远停在上一关表现且按钮永久禁用
if (stageCompletedThisRefresh && !didRefreshGridsThisRefresh)
{
try
{
RefreshData();
}
catch (Exception e)
{
Debug.LogException(e);
}
}
SetInteractable(true);
}
}
/// <summary>
/// 砸罐 Cell 已 Publish ShowData同帧或上一帧。用 <see cref="CurRewardQCount"/> 同步读 FishingStage 领奖队列深度(与 HomePanel 一致),
/// 仅当「队列有待展示」或「弹窗已在显示」时才等待关闭;无队列则立即继续,不用时间窗猜弹窗。
/// </summary>
private async Task WaitUntilSmashRewardPopupReleasedAsync(float maxWaitSeconds = 15f)
{
await Awaiters.NextFrame;
await Awaiters.NextFrame;
var ui = UIManager.Instance;
if (ui == null)
return;
int queueCount = PublishAndReadRewardQueueCount();
bool popupVisible = ui.IsShowingUI(UITypes.RewardPopupPanel);
if (queueCount <= 0 && !popupVisible)
return;
float hardDeadline = Time.realtimeSinceStartup + Mathf.Max(3f, maxWaitSeconds);
// 队列非空时 ShowPeek→ShowUI 可能尚未完成,短等到真正显示(串行:有队列才等打开)
if (!popupVisible)
{
float openDeadline = Time.realtimeSinceStartup +
Mathf.Clamp(_rewardPopupAsyncOpenMaxWaitSeconds, 0.25f, 8f);
while (Time.realtimeSinceStartup < openDeadline &&
Time.realtimeSinceStartup < hardDeadline &&
!ui.IsShowingUI(UITypes.RewardPopupPanel))
{
queueCount = PublishAndReadRewardQueueCount();
if (queueCount <= 0)
return;
await Awaiters.NextFrame;
}
}
if (!ui.IsShowingUI(UITypes.RewardPopupPanel))
{
Debug.LogWarning(
"[EventSmashPanel] 领奖队列曾非空但限时内 RewardPopup 未显示,继续通关遮罩/刷新。");
return;
}
while (Time.realtimeSinceStartup < hardDeadline &&
ui.IsShowingUI(UITypes.RewardPopupPanel))
{
var gotClose = false;
var sub = GContext.OnEvent<RewardPanelClose>().Subscribe(_ => gotClose = true);
try
{
while (Time.realtimeSinceStartup < hardDeadline &&
ui.IsShowingUI(UITypes.RewardPopupPanel) &&
!gotClose)
await Awaiters.NextFrame;
}
finally
{
sub.Dispose();
}
await Awaiters.NextFrame;
}
if (ui.IsShowingUI(UITypes.RewardPopupPanel))
Debug.LogWarning(
"[EventSmashPanel] 通关等待 RewardPopup 关闭超时,继续播关门/刷新;请检查 IsShowingUI 是否误报。");
}
/// <summary>FishingStage.GetShowDataCount 在 Publish 时同步写入 count含 fishCard/fishBox 占位)。</summary>
private static int PublishAndReadRewardQueueCount()
{
var q = new CurRewardQCount();
GContext.Publish(q);
return q.count;
}
/// <summary>在已激活的 Image_door_mask 上启动 Legacy Animation 或 Animator并返回用于等待的总时长。</summary>
private async Task<float> StartDoorMaskVisualAndGetDurationAsync()
{
if (_imageDoorMask == null) return 0f;
var legacy = _imageDoorMask.GetComponentInChildren<Animation>(true);
if (legacy != null)
{
if (!TryPlayLegacyDoorMaskClip(legacy, out var st))
return 0f;
float spd = Mathf.Abs(st.speed) > 0.001f ? Mathf.Abs(st.speed) : 1f;
return st.clip.length / spd;
}
var animator = _imageDoorMask.GetComponentInChildren<Animator>(true);
if (animator != null && animator.runtimeAnimatorController != null)
{
animator.enabled = true;
if (!string.IsNullOrEmpty(_doorMaskAnimClipName))
animator.Play(Animator.StringToHash(_doorMaskAnimClipName), 0, 0f);
else
{
var clips = animator.runtimeAnimatorController.animationClips;
if (clips != null && clips.Length > 0)
animator.Play(clips[0].name, 0, 0f);
else
return 0f;
}
await Awaiters.NextFrame;
var info = animator.GetCurrentAnimatorStateInfo(0);
float spd = Mathf.Abs(info.speed) > 0.001f ? Mathf.Abs(info.speed) : 1f;
float len = info.length > 0.01f ? info.length / spd : 0f;
return len;
}
return 0f;
}
/// <summary>
/// 1) 配置 clip 名 2) 组件默认 clip 3) 遍历 Animation 列表第一个有 clip 的状态(解决预制体未设默认、名称与配置不一致)。
/// </summary>
private bool TryPlayLegacyDoorMaskClip(Animation legacy, out AnimationState st)
{
st = null;
if (!string.IsNullOrEmpty(_doorMaskAnimClipName) && legacy[_doorMaskAnimClipName] != null)
{
st = legacy[_doorMaskAnimClipName];
legacy.Play(_doorMaskAnimClipName);
return true;
}
if (legacy.clip != null)
{
st = legacy[legacy.clip.name];
legacy.Play();
return true;
}
foreach (AnimationState state in legacy)
{
if (state == null || state.clip == null) continue;
st = state;
legacy.Play(state.name);
return true;
}
return false;
}
private void OnSmashAllClick()
{
if (_manager == null || _data == null) return;
if (!_manager.SmashAll(_data, out bool stageJustCompleted)) return;
if (stageJustCompleted)
_stageCompletePendingDoorAnim = true;
SetInteractable(false);
RefreshTokenUiAfterBalanceChange();
EventSmashCell.PlayOpenAll(_cellPrefabs, OnSmashAnimComplete);
GContext.Publish(new EventUISound("audio_ui_eventsmashyard_all_prop"));
}
private void OnCloseClick()
{
if (GContext.container.ResolveAct(EventSmashAct.ActAddress) != null)
GContext.Publish(new UnloadActToNextAct());
}
private void OnInfoClick()
{
// 显示活动说明面板
_=UIManager.Instance.ShowUI(new UIType(_data.MainConfig.InfoPanel));
}
private void OnAddClick()
{
_manager.ShowFestPackPanel(_data);
}
private async void OnBattlePassClick()
{
if (_data?.MainConfig == null) return;
var main = _data.MainConfig;
if (main.BattlePassId <= 0 || string.IsNullOrEmpty(main.BattlePassPanel)) return;
await UIManager.Instance.ShowUILoad(new UIType(main.BattlePassPanel));
}
/// <summary>单砸/全砸动效进行中关闭顶部与导航按钮,避免重复操作;结束后再按数据恢复(一键砸仍由 <see cref="UpdateSmashAllButton"/> 决定)。</summary>
private void SetInteractable(bool interactable)
{
if (_btnClose != null)
_btnClose.enabled = interactable;
if (_btnInfo != null)
_btnInfo.enabled = interactable;
if (_btnAdd != null)
_btnAdd.enabled = interactable;
if (_btnBattlePass != null)
_btnBattlePass.enabled = interactable;
if (interactable)
UpdateSmashAllButton();
else if (_btnSmashAll != null)
_btnSmashAll.enabled = false;
if (_cellPrefabs != null)
{
foreach (var cell in _cellPrefabs)
cell?.SetClickable(interactable);
}
}
#endregion
}
}