497 lines
18 KiB
C#
497 lines
18 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Threading.Tasks;
|
||
using asap.core;
|
||
using cfg;
|
||
using Event1v1Battle.Data;
|
||
using game;
|
||
using GameCore;
|
||
using TimeManager;
|
||
using TMPro;
|
||
using UIExtend.Component;
|
||
using UniRx;
|
||
using UnityEngine;
|
||
using UnityEngine.Playables;
|
||
using UnityEngine.UI;
|
||
|
||
namespace Event1v1Battle.Panel.MainPanel
|
||
{
|
||
/// <summary>
|
||
/// 1v1扇耳光大赛主战斗面板 Event1v1BattleSlapPanel
|
||
/// </summary>
|
||
public class Event1v1BattleSlapPanel : BasePanel
|
||
{
|
||
#region UI Elements
|
||
|
||
// Top
|
||
private TMP_Text _textEventTime;
|
||
private TMP_Text _textStageHP;
|
||
private Image _imgStageHPBar;
|
||
private TMP_Text _textPlayerHP;
|
||
private Image _imgPlayerHPBar;
|
||
private Image[] _lifeImages;
|
||
/// <summary> fx_hploss 下与子节点,与 p_lives_icon 中心形顺序一一对应(本次扣第几颗心就开哪一个) </summary>
|
||
private GameObject[] _fxHpLossSlots;
|
||
private int? _overrideLifeDisplayCount;
|
||
private Head _myself;
|
||
private Head _enemy;
|
||
private GameObject _iconEmpty;
|
||
private Button _btnInfo;
|
||
private Button _btnPack; // 礼包按钮
|
||
|
||
//buff
|
||
private GameObject _saleObj;
|
||
private TMP_Text _textSaleTime;
|
||
private GameObject fx_ui_event1v1battle_slap_buff;
|
||
|
||
// Bottom
|
||
private Button _btnBattle;
|
||
private Button _btnStart;
|
||
private Button _btnReward;
|
||
private Button _btnClose;
|
||
private Button _btnRevive;
|
||
private TMP_Text _textRound;
|
||
private TMP_Text _textReviveBtn;
|
||
private StageItemCell[] _stageItemCells;
|
||
|
||
//chest
|
||
private PlayableDirector _chestDirector;
|
||
#endregion
|
||
|
||
#region Manager
|
||
|
||
private Event1v1BattleSlapManager _manager;
|
||
private Event1v1BattleSlapData Data => _manager?.LoadData();
|
||
|
||
#endregion
|
||
|
||
[Header("chest reward")]
|
||
[SerializeField] private float chestRewardTime = 1f;
|
||
|
||
[Header("fx_hploss")]
|
||
[Tooltip("子节点上无 Animation 默认 clip 时的等待时长(秒)")]
|
||
[SerializeField] private float _fxHpLossWaitSeconds = 0.5f;
|
||
|
||
private void Awake()
|
||
{
|
||
// Top
|
||
_textEventTime = gameObject.FindChildGameObject("p_text_event_time")?.GetComponent<TMP_Text>();
|
||
_lifeImages = gameObject.FindChildGameObject("p_lives_icon")?.GetComponentsInChildren<Image>(true);
|
||
var fxHpLossRoot = gameObject.FindChildGameObject("fx_hploss");
|
||
if (fxHpLossRoot != null)
|
||
{
|
||
var t = fxHpLossRoot.transform;
|
||
int childCount = t.childCount;
|
||
int slotCount = Mathf.Min(3, childCount);
|
||
_fxHpLossSlots = new GameObject[slotCount];
|
||
for (int i = 0; i < slotCount; i++)
|
||
{
|
||
_fxHpLossSlots[i] = t.GetChild(i).gameObject;
|
||
_fxHpLossSlots[i].SetActive(false);
|
||
}
|
||
}
|
||
_textStageHP = gameObject.FindChildGameObject("p_text_blood")?.GetComponent<TMP_Text>(); // NPC血量文本
|
||
_imgStageHPBar = gameObject.FindChildGameObject("p_bar_enermy")?.GetComponent<Image>(); // NPC血条
|
||
_enemy = gameObject.FindChildGameObject("head_enermy")?.GetComponent<Head>();
|
||
_iconEmpty = gameObject.FindChildGameObject("p_icon_empty");
|
||
|
||
_textPlayerHP = gameObject.FindChildGameObject("p_text_blood_myself")?.GetComponent<TMP_Text>(); // 玩家血量文本(在myself下)
|
||
_imgPlayerHPBar = gameObject.FindChildGameObject("p_bar_myself")?.GetComponent<Image>(); // 玩家血条
|
||
_myself = gameObject.FindChildGameObject("head_myself")?.GetComponent<Head>();
|
||
|
||
_btnPack = gameObject.FindChildGameObject("btn_pack")?.GetComponent<Button>();
|
||
_btnInfo = gameObject.FindChildGameObject("p_btn_questionmark")?.GetComponentInChildren<Button>();
|
||
|
||
// Bottom
|
||
_btnBattle = gameObject.FindChildGameObject("btn_battle")?.GetComponentInChildren<Button>();
|
||
_btnStart = gameObject.FindChildGameObject("btn_start")?.GetComponentInChildren<Button>();
|
||
_btnRevive = gameObject.FindChildGameObject("btn_revive")?.GetComponentInChildren<Button>();
|
||
_textReviveBtn = _btnRevive != null ? _btnRevive.GetComponentInChildren<TMP_Text>(true) : null;
|
||
|
||
_btnReward = gameObject.FindChildGameObject("btn_reward")?.GetComponent<Button>();
|
||
_btnClose = gameObject.FindChildGameObject("p_btn_close")?.GetComponent<Button>();
|
||
_textRound = gameObject.FindChildGameObject("p_text_rond")?.GetComponent<TMP_Text>(); // 修正:prefab中是p_text_rond
|
||
_stageItemCells = gameObject.FindChildGameObject("p_bar_root")?.GetComponentsInChildren<StageItemCell>();
|
||
|
||
//buff
|
||
_saleObj = gameObject.FindChildGameObject("text_sale");
|
||
_textSaleTime = gameObject.FindChildGameObject("text_time").GetComponent<TMP_Text>();
|
||
fx_ui_event1v1battle_slap_buff = gameObject.FindChildGameObject("fx_ui_event1v1battle_slap_buff");
|
||
|
||
_chestDirector = gameObject.FindChildGameObject("spine_root").GetComponentInChildren<PlayableDirector>();
|
||
// Button listeners
|
||
if (_btnInfo)
|
||
_btnInfo.onClick.AddListener(OnClickInfo);
|
||
if (_btnPack)
|
||
_btnPack.onClick.AddListener(OnClickPack);
|
||
|
||
if (_btnBattle)
|
||
_btnBattle.onClick.AddListener(OnClickBattle);
|
||
if (_btnStart)
|
||
_btnStart.onClick.AddListener(OnClickStart);
|
||
if (_btnRevive)
|
||
_btnRevive.onClick.AddListener(OnClickRevive);
|
||
if (_btnReward)
|
||
_btnReward.onClick.AddListener(OnClickReward);
|
||
if (_btnClose)
|
||
_btnClose.onClick.AddListener(OnClickClose);
|
||
}
|
||
|
||
protected override void OnDestroy()
|
||
{
|
||
base.OnDestroy();
|
||
|
||
if (_btnInfo)
|
||
_btnInfo.onClick.RemoveAllListeners();
|
||
if (_btnBattle)
|
||
_btnBattle.onClick.RemoveAllListeners();
|
||
if (_btnPack)
|
||
_btnPack.onClick.RemoveAllListeners();
|
||
if (_btnClose)
|
||
_btnClose.onClick.RemoveAllListeners();
|
||
if (_btnReward)
|
||
_btnReward.onClick.RemoveAllListeners();
|
||
if (_btnStart)
|
||
_btnStart.onClick.RemoveAllListeners();
|
||
if (_btnRevive)
|
||
_btnRevive.onClick.RemoveAllListeners();
|
||
|
||
}
|
||
|
||
protected override void Start()
|
||
{
|
||
StartTimers();
|
||
GContext.OnEvent<AddGlobalBuffEvent>().Subscribe(OnAddGlobalBuffEvent).AddTo(disposables);
|
||
}
|
||
|
||
private void OnAddGlobalBuffEvent(AddGlobalBuffEvent e)
|
||
{
|
||
UpdateSaleBuff();
|
||
}
|
||
|
||
#region UI Refresh
|
||
|
||
private void RefreshUI()
|
||
{
|
||
if (Data == null) return;
|
||
UpdateButtons();
|
||
UpdateEventTime();
|
||
UpdateRoundInfo();
|
||
UpdateHP();
|
||
UpdateRemainingLives();
|
||
UpdateStageItem();
|
||
UpdateAvatar();
|
||
UpdateSaleBuff();
|
||
}
|
||
private void UpdateAvatar()
|
||
{
|
||
IUserService userService = GContext.container.Resolve<IUserService>();
|
||
_myself.SetData(userService.AvatarUrl);
|
||
if (Data is not { IsOnGoing: true })
|
||
{
|
||
_iconEmpty.SetActive(true);
|
||
}
|
||
else
|
||
{
|
||
_iconEmpty.SetActive(false);
|
||
_enemy.SetData(Data.NpcIcon);
|
||
}
|
||
}
|
||
private void UpdateStageItem()
|
||
{
|
||
if (Data == null || _stageItemCells == null) return;
|
||
var stageDatas = Data.StageHistory?.Select(history => (false, history.Value)).ToList() ?? new List<(bool, bool)>();
|
||
stageDatas.Add((true, false));
|
||
_stageItemCells.SetData(stageDatas);
|
||
}
|
||
|
||
private void UpdateButtons()
|
||
{
|
||
if (Data == null) return;
|
||
bool awaitingRevive = _manager != null && _manager.IsAwaitingPaidRevive(Data);
|
||
|
||
if (_btnBattle != null)
|
||
{
|
||
_btnBattle.transform.parent.SetActive(Data.IsOnGoing);
|
||
_btnBattle.SetActive(Data.IsOnGoing);
|
||
}
|
||
if (_btnStart != null)
|
||
{
|
||
_btnStart.transform.parent.SetActive(!Data.IsOnGoing && !awaitingRevive);
|
||
_btnStart.SetActive(!Data.IsOnGoing && !awaitingRevive);
|
||
}
|
||
if (_btnRevive != null)
|
||
{
|
||
_btnRevive.transform.parent.SetActive(!Data.IsOnGoing && awaitingRevive);
|
||
_btnRevive.SetActive(!Data.IsOnGoing && awaitingRevive);
|
||
}
|
||
|
||
if (_textReviveBtn != null && _manager != null && awaitingRevive)
|
||
{
|
||
int iapId = _manager.GetPaidReviveIapTableId(Data);
|
||
GContext.container.Resolve<PlayerItemData>().ResolveIapId(iapId, out _, out var price);
|
||
_textReviveBtn.text = LocalizationMgr.GetFormatTextValue("UI_Event1v1BattleSlapPanel_15", price);
|
||
}
|
||
}
|
||
|
||
private void UpdateEventTime()
|
||
{
|
||
if (_textEventTime == null || Data == null) return;
|
||
_textEventTime.text = ConvertTools.ConvertTime2(Data.RemainingTime);
|
||
}
|
||
|
||
private void UpdateRoundInfo()
|
||
{
|
||
if (_textRound == null || Data == null) return;
|
||
_textRound.text = LocalizationMgr.GetFormatTextValue("UI_Event1v1BattleSlapPanel_5", Data.StageHistory?.Count + 1 ?? 1,
|
||
Data.GetAllStageCount());
|
||
}
|
||
|
||
|
||
private void UpdateHP()
|
||
{
|
||
if (Data == null) return;
|
||
|
||
// NPC HP - 使用DOTween动画
|
||
if (_textStageHP)
|
||
_textStageHP.text = $"{Data.NpcHp}";
|
||
|
||
if (_imgStageHPBar)
|
||
_imgStageHPBar.fillAmount = Data.MaxHp > 0 ? (float)Data.NpcHp / Data.MaxHp : 0; ;
|
||
|
||
// Player HP - 使用DOTween动画
|
||
int playerMaxHP = _manager.GetPlayerMaxHp(Data);
|
||
if (_textPlayerHP)
|
||
_textPlayerHP.text = $"{Data.PlayerHp}";
|
||
|
||
if (_imgPlayerHPBar)
|
||
_imgPlayerHPBar.fillAmount = playerMaxHP > 0 ? (float)Data.PlayerHp / playerMaxHP : 0;
|
||
}
|
||
|
||
private void UpdateRemainingLives()
|
||
{
|
||
if (_lifeImages == null || Data == null) return;
|
||
int count = _overrideLifeDisplayCount.HasValue
|
||
? Mathf.Clamp(_overrideLifeDisplayCount.Value, 0, _lifeImages.Length)
|
||
: Mathf.Clamp(Data.PlayerLifeCount, 0, _lifeImages.Length);
|
||
for (int i = 0; i < _lifeImages.Length; i++)
|
||
{
|
||
_lifeImages[i].gameObject.SetActive(i >= _lifeImages.Length - count);
|
||
}
|
||
}
|
||
|
||
private void UpdateSaleBuff()
|
||
{
|
||
if (_saleObj == null || _textSaleTime == null) return;
|
||
|
||
var buffDataCenter = GContext.container.Resolve<BuffDataCenter>();
|
||
var targetBuffData = buffDataCenter.GetBuffTimeDataByType<EventSlapDamageBuff>();
|
||
if (targetBuffData == null) return;
|
||
if (targetBuffData is { isEnd: false } && (targetBuffData.buffEndTime - ZZTimeHelper.UtcNow().UtcNowOffset()).TotalSeconds > 0)
|
||
{
|
||
_saleObj.SetActive(false);
|
||
_textSaleTime.SetActive(true);
|
||
fx_ui_event1v1battle_slap_buff.SetActive(true);
|
||
var remaining = targetBuffData.buffEndTime - ZZTimeHelper.UtcNow().UtcNowOffset();
|
||
_textSaleTime.text = ConvertTools.ConvertTime2(remaining);
|
||
}
|
||
else
|
||
{
|
||
_saleObj.SetActive(true);
|
||
_textSaleTime.SetActive(false);
|
||
fx_ui_event1v1battle_slap_buff.SetActive(false);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Timers
|
||
|
||
private void StartTimers()
|
||
{
|
||
GContext.container.Resolve<ITimeTickService>()?.SecondTick?.Subscribe(UpdateTimer).AddTo(disposables);
|
||
}
|
||
|
||
private void UpdateTimer(long obj)
|
||
{
|
||
UpdateEventTime();
|
||
UpdateSaleBuff();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Player Actions
|
||
|
||
private void OnClickStart()
|
||
{
|
||
if (Data == null || _manager == null) return;
|
||
_manager.ShowStagePanel(Data);
|
||
}
|
||
|
||
private async void OnClickRevive()
|
||
{
|
||
if (Data == null || _manager == null) return;
|
||
if (!_manager.IsAwaitingPaidRevive(Data))
|
||
return;
|
||
await _manager.TryPurchasePaidReviveAsync(Data);
|
||
RefreshUI();
|
||
}
|
||
private void OnClickBattle()
|
||
{
|
||
GContext.container.Resolve<GuideDataCenter>().TriggerGuide(GroupName.Slap01.ToString(), "HomePanel", false);
|
||
UIManager.Instance.DestroyUI(UITypes.Event1v1BattleSlapPanel);
|
||
}
|
||
|
||
|
||
#endregion
|
||
|
||
#region Panel Navigation
|
||
|
||
private void OnClickInfo()
|
||
{
|
||
if (Data?.InitConfig == null) return;
|
||
UITypes.Event1v1BattleSlapInfoPopupPanel.SetType(Data.InitConfig.InfoPanel);
|
||
_ = UIManager.Instance.ShowUI(UITypes.Event1v1BattleSlapInfoPopupPanel);
|
||
}
|
||
|
||
private void OnClickPack()
|
||
{
|
||
_manager?.ShowFestPackPanel();
|
||
}
|
||
private void OnClickReward()
|
||
{
|
||
_manager?.ShowAllStageRewardPanel(Data);
|
||
}
|
||
|
||
private void OnClickClose()
|
||
{
|
||
UIManager.Instance.DestroyUI(UITypes.Event1v1BattleSlapPanel);
|
||
}
|
||
|
||
public async Task PlayStageRewardSendAni()
|
||
{
|
||
await PlayChestOpenAni();
|
||
_manager.SendStageReward(Data);
|
||
await Awaiters.Seconds(0.2f);
|
||
// 停表并回到与首次 Play 前一致的起点,Evaluate 才会把绑定对象刷回该时刻姿态(仅 Stop/time 往往画面仍停在结尾)
|
||
_chestDirector.Stop();
|
||
_chestDirector.time = _chestDirector.initialTime;
|
||
_chestDirector.Evaluate();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Public Methods
|
||
/// <summary>
|
||
/// 面板开始时调用
|
||
/// </summary>
|
||
public async void OnSetData()
|
||
{
|
||
try
|
||
{
|
||
int pendingHeartBefore = _manager?.PresentationData?.PendingMainPanelHeartVisualBefore ?? 0;
|
||
if (pendingHeartBefore > 0)
|
||
{
|
||
_overrideLifeDisplayCount = pendingHeartBefore;
|
||
RefreshUI();
|
||
await PlayReturnFromTvDefeatLifeLossIntroAsync(pendingHeartBefore);
|
||
_manager?.PresentationData.ClearPendingMainPanelHeartLossVisual();
|
||
_overrideLifeDisplayCount = null;
|
||
}
|
||
|
||
RefreshUI();
|
||
if (Data.IsNeedSendReward)
|
||
{
|
||
await PlayStageRewardSendAni();
|
||
}
|
||
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError(e);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// TV 战败回到主界面:先保持「扣心前」心形显示,在 fx_hploss 下打开对应槽位特效,再与存档对齐。
|
||
/// 槽位与主界面心形 Image 顺序一致:本次失去的是下标 (n - 扣前命数),与 UpdateRemainingLives 规则相同。
|
||
/// </summary>
|
||
async Task PlayReturnFromTvDefeatLifeLossIntroAsync(int heartCountBeforeLoss)
|
||
{
|
||
if (_lifeImages == null || _lifeImages.Length == 0 || heartCountBeforeLoss <= 0)
|
||
{
|
||
await Awaiters.Seconds(0.35f);
|
||
return;
|
||
}
|
||
|
||
int n = _lifeImages.Length;
|
||
int slotIndex = Mathf.Clamp(n - heartCountBeforeLoss, 0, n - 1);
|
||
if (_fxHpLossSlots == null || slotIndex >= _fxHpLossSlots.Length || _fxHpLossSlots[slotIndex] == null)
|
||
{
|
||
await Awaiters.Seconds(0.35f);
|
||
return;
|
||
}
|
||
|
||
var fxSlot = _fxHpLossSlots[slotIndex];
|
||
fxSlot.SetActive(true);
|
||
await WaitFxHpLossSlotAsync(fxSlot);
|
||
fxSlot.SetActive(false);
|
||
}
|
||
|
||
async Task WaitFxHpLossSlotAsync(GameObject fxSlot)
|
||
{
|
||
var anim = fxSlot.GetComponentInChildren<Animation>(true);
|
||
if (anim != null && anim.clip != null)
|
||
{
|
||
await anim.PlayAndWaitAsync(anim.clip.name);
|
||
return;
|
||
}
|
||
|
||
await Awaiters.Seconds(Mathf.Max(0.05f, _fxHpLossWaitSeconds));
|
||
}
|
||
|
||
public void SetData(Event1v1BattleSlapManager manager)
|
||
{
|
||
_manager = manager;
|
||
OnSetData();
|
||
}
|
||
|
||
public async Task PlayChestOpenAni()
|
||
{
|
||
try
|
||
{
|
||
if (_chestDirector == null || _chestDirector.playableAsset == null) return;
|
||
_chestDirector.Play();
|
||
await Awaiters.Seconds((float)_chestDirector.playableAsset.duration);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError(e);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#if UNITY_EDITOR
|
||
#region debug
|
||
[Header("debug")]
|
||
[SerializeField] private int debug_stageID = 1001;
|
||
[ContextMenu("Debug_SetStage")]
|
||
public void Debug_SetStage()
|
||
{
|
||
_manager.Debug_SetStage(debug_stageID);
|
||
RefreshUI();
|
||
}
|
||
[SerializeField] private int debug_lifeCount;
|
||
[ContextMenu("Debug_SetLife")]
|
||
public void Debug_SetLife()
|
||
{
|
||
_manager.Debug_SetLife(debug_lifeCount);
|
||
}
|
||
#endregion
|
||
#endif
|
||
}
|
||
|
||
}
|