880 lines
35 KiB
C#
880 lines
35 KiB
C#
using System;
|
||
using System.Threading.Tasks;
|
||
using Activity;
|
||
using asap.core;
|
||
using DG.Tweening;
|
||
using Event1v1Battle.Data;
|
||
using GameCore;
|
||
using TMPro;
|
||
using UnityEngine;
|
||
using UnityEngine.Playables;
|
||
using UnityEngine.UI;
|
||
using UniRx;
|
||
using UnityEngine.Timeline;
|
||
using UnityEngine.Events;
|
||
|
||
namespace game
|
||
{
|
||
/// <summary>
|
||
/// 1v1扇耳光大赛TV演示面板 Event1v1BattleSlapTvPopupPanel
|
||
/// 真正的战斗演示面板,本版本只实现基础UI,Timeline/Spine系统留到后续版本
|
||
/// </summary>
|
||
public partial class Event1v1BattleSlapTvPopupPanel : BasePanel
|
||
{
|
||
#region UI Elements
|
||
|
||
// 血条
|
||
private Image _imgBarMyself;
|
||
private Image _imgBarEnemy;
|
||
|
||
// 血量文本(需要区分2个)
|
||
private TMP_Text _textMyselfBlood;
|
||
private TMP_Text _textEnemyBlood;
|
||
private TMP_Text _textPlayerHpDeduct;
|
||
private TMP_Text _textEnemyHpDeduct;
|
||
|
||
// 角色对象
|
||
private Head _myself;
|
||
private Head _enemy;
|
||
|
||
// 生命显示
|
||
private Image[] _lifeImages;
|
||
|
||
//buff
|
||
private TMP_Text text_buff_num;
|
||
|
||
private Event1v1BattleSlapCan _tvCan;
|
||
|
||
//animation
|
||
private Animation _uiAnimation;
|
||
private SignalReceiver _signalReceiver;
|
||
private PlayableDirector _playerDirector;
|
||
private PlayableDirector _enemyDirector;
|
||
private PlayableDirector _resDirector;
|
||
|
||
private RectTransform slowMotionShakeRect => (RectTransform)transform;
|
||
|
||
//fx
|
||
private Transform fx_event1v1battle_slap_pepperenergy;
|
||
private GameObject fx_my_buff;
|
||
private GameObject fx_enemy_buff;
|
||
|
||
private Transform fx_hit_enermy;
|
||
private Transform fx_hit_player;
|
||
|
||
private Transform fx_energy;
|
||
|
||
//result
|
||
private GameObject ko_animation;
|
||
private GameObject defeat_animation;
|
||
private GameObject timeout_animation;
|
||
#endregion
|
||
|
||
#region Manager
|
||
|
||
private Event1v1BattleSlapManager _manager;
|
||
private Event1v1BattleSlapData Data => _manager?.LoadData();
|
||
private Event1v1BattleSlapPresentationData PresentationData => _manager?.PresentationData;
|
||
private bool IsUsingPresentationData => PresentationData?.IsPresenting == true;
|
||
private UnityEvent _onSignalEvent = new UnityEvent();
|
||
private float _currentPlaybackSpeed = 1f;
|
||
private int _currentSlapCanQuality;
|
||
|
||
partial void OnStartDebugHook(ref bool shouldAutoPlay);
|
||
#endregion
|
||
private void Awake()
|
||
{
|
||
// 血条
|
||
_imgBarMyself = gameObject.FindChildGameObject("p_bar_myself")?.GetComponent<Image>();
|
||
_imgBarEnemy = gameObject.FindChildGameObject("p_bar_enermy")?.GetComponent<Image>();
|
||
|
||
// 血量文本 - 在myself和enermy下
|
||
_textMyselfBlood = gameObject.FindChildGameObject("myself")?.FindChildGameObject("p_text_blood")?.GetComponent<TMP_Text>();
|
||
_textEnemyBlood = gameObject.FindChildGameObject("enermy")?.FindChildGameObject("p_text_blood")?.GetComponent<TMP_Text>();
|
||
_textPlayerHpDeduct = gameObject.FindChildGameObject("p_text_player_hp_deduct")?.GetComponent<TMP_Text>();
|
||
_textEnemyHpDeduct = gameObject.FindChildGameObject("p_text_enermy_hp_deduct")?.GetComponent<TMP_Text>();
|
||
|
||
// 角色对象
|
||
_enemy = gameObject.FindChildGameObject("head_enermy")?.GetComponent<Head>();
|
||
_myself = gameObject.FindChildGameObject("head_myself")?.GetComponent<Head>();
|
||
|
||
//buff
|
||
fx_my_buff = _myself.gameObject.FindChildGameObject("fx_ui_event1v1battle_slap_buff");
|
||
fx_enemy_buff = _enemy.gameObject.FindChildGameObject("fx_ui_event1v1battle_slap_buff");
|
||
text_buff_num = gameObject.FindChildGameObject("text_buff_num")?.GetComponent<TMP_Text>();
|
||
// 生命显示
|
||
_lifeImages = gameObject.FindChildGameObject("p_lives_icon")?.GetComponentsInChildren<Image>();
|
||
|
||
// 动画
|
||
_uiAnimation = GetComponent<Animation>();
|
||
_resDirector = gameObject.FindChildGameObject("root")?.GetComponentInChildren<PlayableDirector>(true);
|
||
_playerDirector = gameObject.FindChildGameObject("spine_player")?.GetComponentInChildren<PlayableDirector>(true);
|
||
_enemyDirector = gameObject.FindChildGameObject("spine_enermy")?.GetComponentInChildren<PlayableDirector>(true);
|
||
_tvCan = gameObject.GetComponentInChildren<Event1v1BattleSlapCan>(true);
|
||
_signalReceiver = gameObject.GetComponent<SignalReceiver>();
|
||
_signalReceiver.AddReaction(signalAsset, _onSignalEvent);
|
||
_onSignalEvent.AddListener(OnSignal);
|
||
|
||
ko_animation = gameObject.FindChildGameObject("ko_animation")?.gameObject;
|
||
defeat_animation = gameObject.FindChildGameObject("defeat_animation")?.gameObject;
|
||
timeout_animation = gameObject.FindChildGameObject("timeout_animation")?.gameObject;
|
||
if (ko_animation != null) ko_animation.SetActive(false);
|
||
if (defeat_animation != null) defeat_animation.SetActive(false);
|
||
if (timeout_animation != null) timeout_animation.SetActive(false);
|
||
|
||
|
||
fx_event1v1battle_slap_pepperenergy = gameObject.FindChildGameObject("fx_event1v1battle_slap_pepperenergy")?.transform;
|
||
if (fx_event1v1battle_slap_pepperenergy != null)
|
||
{
|
||
fx_event1v1battle_slap_pepperenergy.gameObject.SetActive(false);
|
||
for (int i = 0; i < fx_event1v1battle_slap_pepperenergy.childCount; i++)
|
||
{
|
||
fx_event1v1battle_slap_pepperenergy.GetChild(i).gameObject.SetActive(false);
|
||
}
|
||
}
|
||
fx_hit_enermy = _enemyDirector.gameObject.FindChildGameObject("fx_hit")?.transform;
|
||
fx_hit_player = _playerDirector.gameObject.FindChildGameObject("fx_hit")?.transform;
|
||
if (fx_hit_enermy != null)
|
||
{
|
||
fx_hit_enermy.gameObject.SetActive(false);
|
||
for (int i = 0; i < fx_hit_enermy.childCount; i++)
|
||
{
|
||
fx_hit_enermy.GetChild(i).gameObject.SetActive(false);
|
||
}
|
||
}
|
||
if (fx_hit_player != null)
|
||
{
|
||
fx_hit_player.gameObject.SetActive(false);
|
||
for (int i = 0; i < fx_hit_player.childCount; i++)
|
||
{
|
||
fx_hit_player.GetChild(i).gameObject.SetActive(false);
|
||
}
|
||
}
|
||
fx_energy = gameObject.FindChildGameObject("fx_energy")?.transform;
|
||
if (fx_energy != null)
|
||
{
|
||
fx_energy.gameObject.SetActive(false);
|
||
for (int i = 0; i < fx_energy.childCount; i++)
|
||
{
|
||
fx_energy.GetChild(i).gameObject.SetActive(false);
|
||
}
|
||
}
|
||
}
|
||
|
||
protected override void OnDestroy()
|
||
{
|
||
base.OnDestroy();
|
||
if (slowMotionShakeRect != null)
|
||
DOTween.Kill(slowMotionShakeRect, false);
|
||
_signalReceiver.Remove(signalAsset);
|
||
_onSignalEvent.RemoveAllListeners();
|
||
_onSignalEvent = null;
|
||
_signalReceiver = null;
|
||
}
|
||
|
||
protected override void Start()
|
||
{
|
||
_manager = ActivityResolver.Resolve<Event1v1BattleSlapManager>();
|
||
RefreshUI();
|
||
StartTvAfterEntranceAsync();
|
||
}
|
||
|
||
private async void StartTvAfterEntranceAsync()
|
||
{
|
||
try
|
||
{
|
||
if (_uiAnimation != null)
|
||
{
|
||
SetSpineFrozen(true);
|
||
var cg = GetComponent<CanvasGroup>();
|
||
if (cg == null) cg = gameObject.AddComponent<CanvasGroup>();
|
||
cg.alpha = 0;
|
||
_uiAnimation.Play("1v1battleslap_popup_show");
|
||
await Awaiters.NextFrame;
|
||
cg.alpha = 1;
|
||
await _uiAnimation.PlayAndWaitAsync("1v1battleslap_popup_show");
|
||
SetSpineFrozen(false);
|
||
}
|
||
#if UNITY_EDITOR
|
||
bool shouldAutoPlay = true;
|
||
OnStartDebugHook(ref shouldAutoPlay);
|
||
if (shouldAutoPlay)
|
||
PlayTvAni();
|
||
#else
|
||
PlayTvAni();
|
||
#endif
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError(e);
|
||
}
|
||
}
|
||
|
||
#region UI Refresh
|
||
|
||
private void RefreshUI()
|
||
{
|
||
if (Data == null) return;
|
||
|
||
UpdateHPBars();
|
||
UpdateBloodText();
|
||
UpdateRemainingLives();
|
||
UpdateAvatar();
|
||
fx_my_buff.SetActive(_manager.BuffIsValid(Data));
|
||
fx_enemy_buff.SetActive(PresentationData.IsNpcBuffValid);
|
||
text_buff_num.text = "+" + Mathf.RoundToInt(_manager.GetBuffBenefit(Data) * 100).ToString() + "%";
|
||
}
|
||
|
||
private void UpdateAvatar()
|
||
{
|
||
var userService = GContext.container.Resolve<IUserService>();
|
||
_myself.SetData(userService.AvatarUrl);
|
||
_enemy.SetData(IsUsingPresentationData ? PresentationData.NpcIcon : Data.NpcIcon);
|
||
}
|
||
|
||
private void UpdateHPBars()
|
||
{
|
||
if (Data == null) return;
|
||
// NPC血条 - 优先使用表现数据
|
||
if (_imgBarEnemy)
|
||
{
|
||
int npcHp = IsUsingPresentationData ? PresentationData.NpcHp : Data.NpcHp;
|
||
int npcMaxHp = IsUsingPresentationData ? PresentationData.MaxHp : Data.MaxHp;
|
||
float enemyFillAmount = npcMaxHp > 0 ? (float)npcHp / npcMaxHp : 0;
|
||
_imgBarEnemy.fillAmount = enemyFillAmount;
|
||
}
|
||
|
||
// 玩家血条
|
||
if (_imgBarMyself)
|
||
{
|
||
int playerHp = IsUsingPresentationData ? PresentationData.PlayerHp : Data.PlayerHp;
|
||
int playerMaxHp = IsUsingPresentationData ? PresentationData.PlayerMaxHp : Data.MaxHp;
|
||
float myselfFillAmount = playerMaxHp > 0 ? (float)playerHp / playerMaxHp : 0;
|
||
_imgBarMyself.fillAmount = myselfFillAmount;
|
||
}
|
||
}
|
||
|
||
private void UpdateBloodText()
|
||
{
|
||
if (Data == null) return;
|
||
|
||
// NPC血量 - 优先使用表现数据
|
||
if (_textEnemyBlood != null)
|
||
{
|
||
int npcHp = IsUsingPresentationData ? PresentationData.NpcHp : Data.NpcHp;
|
||
_textEnemyBlood.text = $"{npcHp}";
|
||
}
|
||
|
||
// 玩家血量
|
||
if (_textMyselfBlood != null)
|
||
{
|
||
int playerHp = IsUsingPresentationData ? PresentationData.PlayerHp : Data.PlayerHp;
|
||
_textMyselfBlood.text = $"{playerHp}";
|
||
}
|
||
}
|
||
|
||
private void UpdateRemainingLives()
|
||
{
|
||
if (_lifeImages == null || Data == null) return;
|
||
for (int i = 0; i < _lifeImages.Length; i++)
|
||
{
|
||
_lifeImages[i].gameObject.SetActive(i < Data.PlayerLifeCount);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Panel Navigation
|
||
|
||
public async Task ClosePanel(float? delaySeconds = null)
|
||
{
|
||
// if (_uiAnimation != null)
|
||
// _uiAnimation.Play("popup_common_out");//暂时没有这个动画
|
||
|
||
// 表现完成,清理表现数据
|
||
try
|
||
{
|
||
PresentationData?.Clear();
|
||
if (delaySeconds == null) delaySeconds = closePanelDelaySeconds;
|
||
if (delaySeconds.Value > 0f)
|
||
await Awaiters.Seconds(delaySeconds.Value);
|
||
UIManager.Instance.DestroyUI(UITypes.Event1v1BattleSlapTvPopupPanel);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError(e);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Timeline
|
||
private async void PlayTvAni()
|
||
{
|
||
try
|
||
{
|
||
PlayIdleAsync();
|
||
if (PresentationData?.IsPresenting != true)
|
||
{
|
||
bool isFirstMatchIdle = Data.CurrentRoundForm1 == 1 && (Data.StageHistory?.Count ?? 0) == 0;
|
||
float? delaySeconds = isFirstMatchIdle ? firstMatchShowSeconds : null;
|
||
await ClosePanel(delaySeconds);
|
||
return;
|
||
}
|
||
|
||
if (PresentationData.IsTimeoutDefeat)
|
||
{
|
||
await PlayTimeoutDefeat();
|
||
await ClosePanel();
|
||
return;
|
||
}
|
||
|
||
var killedNpc= await TryPlayPlayerAttack();
|
||
if(killedNpc) return;
|
||
|
||
var killedPlayer= await TryPlayNpcAttack();
|
||
if(killedPlayer) return;
|
||
await ClosePanel();
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError(e);
|
||
}
|
||
}
|
||
private async void PlayIdleAsync()
|
||
{
|
||
try
|
||
{
|
||
await PlayDualClipsAsync(idle_player, idle_enemy, 0);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError(e);
|
||
}
|
||
}
|
||
private async Task<bool> TryPlayPlayerAttack()
|
||
{
|
||
if (!PresentationData.IsPlayAttacking) return false;
|
||
|
||
await PlayPlayerCanAndEatAsync();
|
||
|
||
await PlayPlayerAttack();
|
||
|
||
if (PresentationData.KilledNpc)
|
||
{
|
||
await PlayVictoryAnimation();
|
||
await ShowResult(true);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private async Task<bool> TryPlayNpcAttack()
|
||
{
|
||
if (!PresentationData.IsNpcAttacking) return false;
|
||
if (PresentationData.IsNpcBuffValid)
|
||
{
|
||
fx_enemy_buff.SetActive(true);
|
||
Debug.LogWarning($"BattleSlap {this.GetType().Name} buff 生效中 todo 播rewardFly动画"); //todo 播动画
|
||
}
|
||
await PlayNpcAttack();
|
||
|
||
if (PresentationData.KilledPlayer)
|
||
{
|
||
await PlayDefeatAnimation();
|
||
await ShowResult(false);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// <summary>仅玩家回合:UI 罐头 → 可选 eat_player Timeline。</summary>
|
||
private async Task PlayPlayerCanAndEatAsync()
|
||
{
|
||
_tvCan.ShowCan(PresentationData.CanQuality-1,PresentationData.Multiplier,_manager.GetBuffBenefit(Data));
|
||
var asset=_manager.BuffIsValid(Data)?eat_player_buff:eat_player;
|
||
if (asset != null)
|
||
await PlayDirectorClipAsync(_playerDirector, asset, PresentationData.CanQuality);
|
||
}
|
||
|
||
private async Task ShowResult(bool isVictory)
|
||
{
|
||
if (!isVictory)
|
||
_manager.PresentationData.SetPendingMainPanelHeartLossVisual(Data.PlayerLifeCount + 1);
|
||
await ClosePanel();
|
||
await _manager.ShowSlapPanel(Data);
|
||
}
|
||
private bool IsHeavyFromQuality(int quality) => quality >= heavyHitQualityMin;
|
||
|
||
private float GetAttackHitGapSeconds(bool attackerIsPlayer, bool heavy)
|
||
{
|
||
if (attackerIsPlayer)
|
||
return heavy ? attackHitGapPlayerHeavy : attackHitGapPlayerLight;
|
||
return heavy ? attackHitGapNpcHeavy : attackHitGapNpcLight;
|
||
}
|
||
|
||
public Task PlayPlayerAttack()
|
||
{
|
||
return PlayPlayerAttack(PresentationData.CanQuality, PresentationData.KilledNpc);
|
||
}
|
||
|
||
public Task PlayNpcAttack()
|
||
{
|
||
return PlayNpcAttack(PresentationData.NpcAttackCanQuality, PresentationData.KilledPlayer);
|
||
}
|
||
|
||
private async Task PlayPlayerAttack(int shakeQ, bool killedNpc)
|
||
{
|
||
bool heavy = IsHeavyFromQuality(shakeQ);
|
||
await PlayStaggeredSlapWithPhasedRecoveryAsync(
|
||
shakeQ,
|
||
attackerIsPlayer: true,
|
||
defenderKilled: killedNpc,
|
||
attackerAttackAsset: heavy ? attack_player_2 : attack_player_1,
|
||
defenderHitAsset: heavy ? hit_enemy_2 : hit_enemy_1,
|
||
defenderKoAsset: heavy ? hit2ko_enemy_2 : hit2ko_enemy_1,
|
||
defenderHit2IdleAsset: heavy ? hit2idle_enemy_2 : hit2idle_enemy_1,
|
||
attackerIdleAfterSlapAsset: idle_player);
|
||
}
|
||
|
||
private async Task PlayNpcAttack(int shakeQ, bool killedPlayer)
|
||
{
|
||
bool heavy = IsHeavyFromQuality(shakeQ);
|
||
await PlayStaggeredSlapWithPhasedRecoveryAsync(
|
||
shakeQ,
|
||
attackerIsPlayer: false,
|
||
defenderKilled: killedPlayer,
|
||
attackerAttackAsset: heavy ? attack_enemy_2 : attack_enemy_1,
|
||
defenderHitAsset: heavy ? hit_player_2 : hit_player_1,
|
||
defenderKoAsset: heavy ? hit2ko_player_2 : hit2ko_player_1,
|
||
defenderHit2IdleAsset: heavy ? hit2idle_player_2 : hit2idle_player_1,
|
||
attackerIdleAfterSlapAsset: idle_enemy);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 交错攻击+受击:唯一时序耦合是「攻击侧 Timeline 先播,经 <see cref="GetAttackHitGapSeconds"/> 秒后再开受击侧」。
|
||
/// 受击未死:hit → hit2idle → 循环 idle(Loop,不阻塞后续逻辑);击杀:hit → hit2ko → ko。
|
||
/// 攻击侧:攻击结束后同样循环 idle 且不等待。胜利/失败由 PlayVictoryAnimation / PlayDefeatAnimation 另行表现。
|
||
/// </summary>
|
||
private async Task PlayStaggeredSlapWithPhasedRecoveryAsync(
|
||
int shakeQuality,
|
||
bool attackerIsPlayer,
|
||
bool defenderKilled,
|
||
PlayableAsset attackerAttackAsset,
|
||
PlayableAsset defenderHitAsset,
|
||
PlayableAsset defenderKoAsset,
|
||
PlayableAsset defenderHit2IdleAsset,
|
||
PlayableAsset attackerIdleAfterSlapAsset)
|
||
{
|
||
ApplyShakeVariant(shakeQuality);
|
||
bool heavy = IsHeavyFromQuality(shakeQuality);
|
||
float gap = GetAttackHitGapSeconds(attackerIsPlayer, heavy);
|
||
|
||
var attackerDir = attackerIsPlayer ? _playerDirector : _enemyDirector;
|
||
var defenderDir = attackerIsPlayer ? _enemyDirector : _playerDirector;
|
||
|
||
if (attackerDir != null && attackerAttackAsset != null)
|
||
{
|
||
ApplyDirectorWrapModeForTimelineClip(attackerDir, attackerAttackAsset);
|
||
attackerDir.Play(attackerAttackAsset);
|
||
if (_currentPlaybackSpeed != 1f)
|
||
SetDirectorGraphSpeed(attackerDir, _currentPlaybackSpeed);
|
||
}
|
||
|
||
if (gap > 0f)
|
||
await WaitForAnimationTimeElapsed(attackerDir, gap);
|
||
|
||
if (defenderDir != null && defenderHitAsset != null)
|
||
{
|
||
ApplyDirectorWrapModeForTimelineClip(defenderDir, defenderHitAsset);
|
||
defenderDir.Play(defenderHitAsset);
|
||
if (_currentPlaybackSpeed != 1f)
|
||
SetDirectorGraphSpeed(defenderDir, _currentPlaybackSpeed);
|
||
// 与受击 Timeline 同步播血条/飘字,不阻塞后续 KO / idle 流程
|
||
if (attackerIsPlayer)
|
||
PlayNpcHpChangeAni();
|
||
else
|
||
PlayPlayerHpChangeAni();
|
||
}
|
||
|
||
async Task DefenderAfterHitAsync()
|
||
{
|
||
await WaitForDirectorFinished(defenderDir);
|
||
if (defenderDir == null) return;
|
||
if (defenderKilled)
|
||
{
|
||
ko_animation.SetActive(attackerIsPlayer);
|
||
defeat_animation.SetActive(!attackerIsPlayer);
|
||
timeout_animation.SetActive(false);
|
||
var overlayAsset = attackerIsPlayer ? ko_player_res : defeat_enemy_res;
|
||
var overlayDelay = attackerIsPlayer ? ko_animation_delay : defeat_animation_delay;
|
||
var defenderLoopKo = attackerIsPlayer ? ko_enemy : ko_player;
|
||
async Task DefenderKoChainAsync()
|
||
{
|
||
await PlayDirectorClipAsync(defenderDir, defenderKoAsset, shakeQuality);
|
||
if (defenderLoopKo != null)
|
||
await PlayDirectorClipAsync(defenderDir, defenderLoopKo, shakeQuality);
|
||
}
|
||
|
||
await Task.WhenAll(DefenderKoChainAsync(), PlayResDirectorOverlayAsync(overlayAsset, overlayDelay, shakeQuality));
|
||
}
|
||
else
|
||
{
|
||
await PlayDirectorClipAsync(defenderDir, defenderHit2IdleAsset, shakeQuality);
|
||
var defenderLoopIdle = attackerIsPlayer ? idle_enemy : idle_player;
|
||
PlayDirectorClipLoopingNoWait(defenderDir, defenderLoopIdle, shakeQuality);
|
||
}
|
||
}
|
||
|
||
async Task AttackerAfterAttackAsync()
|
||
{
|
||
await WaitForDirectorFinished(attackerDir);
|
||
if (attackerDir == null || defenderKilled) return;
|
||
PlayDirectorClipLoopingNoWait(attackerDir, attackerIdleAfterSlapAsset, shakeQuality);
|
||
}
|
||
|
||
await Task.WhenAll(DefenderAfterHitAsync(), AttackerAfterAttackAsync());
|
||
}
|
||
|
||
/// <summary>
|
||
/// 与 Timeline KO 同步在 <see cref="_resDirector"/> 上播放结果层 Timeline(如 <see cref="ko_player_res"/> / <see cref="defeat_enemy_res"/>),支持延时后打开节点并按时长等待结束。
|
||
/// </summary>
|
||
private async Task PlayResDirectorOverlayAsync(PlayableAsset asset, float delaySeconds, int shakeQuality)
|
||
{
|
||
if (_resDirector == null || asset == null) return;
|
||
if (delaySeconds > 0f)
|
||
await Awaiters.Seconds(delaySeconds);
|
||
_resDirector.gameObject.SetActive(true);
|
||
await PlayDirectorClipAsync(_resDirector, asset, shakeQuality);
|
||
}
|
||
|
||
private void SetSpineFrozen(bool frozen)
|
||
{
|
||
var playerSpine = _playerDirector?.GetComponentInChildren<Spine.Unity.SkeletonGraphic>(true);
|
||
var enemySpine = _enemyDirector?.GetComponentInChildren<Spine.Unity.SkeletonGraphic>(true);
|
||
if (playerSpine != null)
|
||
{
|
||
playerSpine.freeze = frozen;
|
||
if (frozen) playerSpine.AnimationState?.ClearTracks();
|
||
}
|
||
if (enemySpine != null)
|
||
{
|
||
enemySpine.freeze = frozen;
|
||
if (frozen) enemySpine.AnimationState?.ClearTracks();
|
||
}
|
||
}
|
||
|
||
/// <summary>仅 <see cref="idle_player"/> / <see cref="idle_enemy"/> 使用 Loop;其它时间线一律 Hold。</summary>
|
||
private void ApplyDirectorWrapModeForTimelineClip(PlayableDirector director, PlayableAsset asset)
|
||
{
|
||
if (director == null) return;
|
||
bool isIdle = asset != null && (asset == idle_player || asset == idle_enemy);
|
||
director.extrapolationMode = isIdle ? DirectorWrapMode.Loop : DirectorWrapMode.Hold;
|
||
}
|
||
|
||
private async Task PlayDirectorClipAsync(PlayableDirector director, PlayableAsset asset, int shakeQuality)
|
||
{
|
||
if (director == null || asset == null) return;
|
||
ApplyShakeVariant(shakeQuality);
|
||
// 按时长等待的单次播放:统一 Hold(含 hit2ko/ko 等,非 idle 不循环)
|
||
director.extrapolationMode = DirectorWrapMode.Hold;
|
||
director.Play(asset);
|
||
if (_currentPlaybackSpeed != 1f)
|
||
SetDirectorGraphSpeed(director, _currentPlaybackSpeed);
|
||
var duration = GetPlayableDuration(director, asset);
|
||
if (duration > 0f)
|
||
await Awaiters.Seconds(duration);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 不阻塞的 Timeline 播放:仅 idle 为 Loop,其余为 Hold(与 <see cref="ApplyDirectorWrapModeForTimelineClip"/> 一致)。
|
||
/// </summary>
|
||
private void PlayDirectorClipLoopingNoWait(PlayableDirector director, PlayableAsset asset, int shakeQuality)
|
||
{
|
||
if (director == null || asset == null) return;
|
||
ApplyShakeVariant(shakeQuality);
|
||
ApplyDirectorWrapModeForTimelineClip(director, asset);
|
||
director.Play(asset);
|
||
if (_currentPlaybackSpeed != 1f)
|
||
SetDirectorGraphSpeed(director, _currentPlaybackSpeed);
|
||
}
|
||
|
||
private async Task PlayDualClipsAsync(PlayableAsset playerAsset, PlayableAsset enemyAsset, int shakeQuality)
|
||
{
|
||
ApplyShakeVariant(shakeQuality);
|
||
|
||
float dur = 0f;
|
||
if (_playerDirector != null && playerAsset != null)
|
||
{
|
||
ApplyDirectorWrapModeForTimelineClip(_playerDirector, playerAsset);
|
||
_playerDirector.Play(playerAsset);
|
||
dur = Mathf.Max(dur, GetPlayableDuration(_playerDirector, playerAsset));
|
||
}
|
||
if (_enemyDirector != null && enemyAsset != null)
|
||
{
|
||
ApplyDirectorWrapModeForTimelineClip(_enemyDirector, enemyAsset);
|
||
_enemyDirector.Play(enemyAsset);
|
||
dur = Mathf.Max(dur, GetPlayableDuration(_enemyDirector, enemyAsset));
|
||
}
|
||
if (dur > 0f)
|
||
await Awaiters.Seconds(dur);
|
||
}
|
||
|
||
private static float GetPlayableDuration(PlayableDirector director, PlayableAsset asset)
|
||
{
|
||
if (director == null || asset == null) return 0f;
|
||
|
||
float assetDuration = (float)asset.duration;
|
||
float directorDuration = (float)director.duration;
|
||
return Mathf.Max(assetDuration, directorDuration);
|
||
}
|
||
|
||
private void ApplyShakeVariant(int quality)
|
||
{
|
||
fx_event1v1battle_slap_pepperenergy = gameObject.FindChildGameObject("fx_event1v1battle_slap_pepperenergy")?.transform;
|
||
if (fx_event1v1battle_slap_pepperenergy != null)
|
||
{
|
||
fx_event1v1battle_slap_pepperenergy.gameObject.SetActive(false);
|
||
for (int i = 0; i < fx_event1v1battle_slap_pepperenergy.childCount; i++)
|
||
{
|
||
fx_event1v1battle_slap_pepperenergy.GetChild(i).gameObject.SetActive(i==quality-1);
|
||
}
|
||
}
|
||
if (fx_hit_enermy != null)
|
||
{
|
||
for (int i = 0; i < fx_hit_enermy.childCount; i++)
|
||
{
|
||
fx_hit_enermy.GetChild(i).gameObject.SetActive(i == quality - 1);
|
||
}
|
||
}
|
||
if (fx_hit_player != null)
|
||
{
|
||
for (int i = 0; i < fx_hit_player.childCount; i++)
|
||
{
|
||
fx_hit_player.GetChild(i).gameObject.SetActive(i==quality-1);
|
||
}
|
||
}
|
||
if (fx_energy != null)
|
||
{
|
||
for (int i = 0; i < fx_energy.childCount; i++)
|
||
{
|
||
fx_energy.GetChild(i).gameObject.SetActive(i==quality-1);
|
||
}
|
||
}
|
||
_currentSlapCanQuality = Mathf.Max(0, quality);
|
||
}
|
||
|
||
private bool TryGetTvDataForQuality(int canQualityId, out Event1v1BattleSlapTvData row)
|
||
{
|
||
row = null;
|
||
if (tvDatas == null) return false;
|
||
for (int i = 0; i < tvDatas.Count; i++)
|
||
{
|
||
var r = tvDatas[i];
|
||
if (r != null && r.canQualityId == canQualityId)
|
||
{
|
||
row = r;
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
public async Task PlayVictoryAnimation()
|
||
{
|
||
int shakeQ = PresentationData != null ? PresentationData.CanQuality : 0;
|
||
await PlayDualClipsAsync(win_player, ko_enemy, shakeQ);
|
||
}
|
||
|
||
public async Task PlayDefeatAnimation()
|
||
{
|
||
int shakeQ = PresentationData != null ? PresentationData.NpcAttackCanQuality : 0;
|
||
await PlayDualClipsAsync(ko_player, win_enemy, shakeQ);
|
||
}
|
||
|
||
private static void SetDirectorGraphSpeed(PlayableDirector director, float speed)
|
||
{
|
||
if (director == null) return;
|
||
try
|
||
{
|
||
if (!director.playableGraph.IsValid()) return;
|
||
director.playableGraph.GetRootPlayable(0).SetSpeed(speed);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogWarning(e);
|
||
}
|
||
}
|
||
|
||
/// <summary>等待 director 的动画时间推进指定量(受 PlayableGraph 速度影响,慢动作时自动延长)。</summary>
|
||
private async Task WaitForAnimationTimeElapsed(PlayableDirector director, float animSeconds)
|
||
{
|
||
if (animSeconds <= 0f) return;
|
||
if (director == null || director.state != PlayState.Playing)
|
||
{
|
||
await Awaiters.Seconds(animSeconds);
|
||
return;
|
||
}
|
||
float target = (float)director.time + animSeconds;
|
||
float safetyMax = animSeconds / Mathf.Max(0.01f, _currentPlaybackSpeed) + 5f;
|
||
float realElapsed = 0f;
|
||
while (director != null && director.state == PlayState.Playing && realElapsed < safetyMax)
|
||
{
|
||
if ((float)director.time >= target) break;
|
||
await Awaiters.NextFrame;
|
||
realElapsed += Time.deltaTime;
|
||
}
|
||
}
|
||
|
||
/// <summary>等待单个 Director 当前片段播放完毕。</summary>
|
||
private async Task WaitForDirectorFinished(PlayableDirector dir)
|
||
{
|
||
const float maxWait = 30f;
|
||
float elapsed = 0f;
|
||
while (!IsDirectorFinished(dir) && elapsed < maxWait)
|
||
{
|
||
await Awaiters.NextFrame;
|
||
elapsed += Time.deltaTime;
|
||
}
|
||
}
|
||
|
||
private static bool IsDirectorFinished(PlayableDirector dir)
|
||
{
|
||
if (dir == null) return true;
|
||
if (dir.state != PlayState.Playing) return true;
|
||
if (dir.time >= dir.duration - 0.01f) return true;
|
||
return false;
|
||
}
|
||
|
||
private async void OnSignal()
|
||
{
|
||
try
|
||
{
|
||
float slowT = 0;
|
||
float slowS = 0;
|
||
Event1v1BattleSlapTvData tvRow = null;
|
||
if (TryGetTvDataForQuality(_currentSlapCanQuality, out var matched))
|
||
{
|
||
tvRow = matched;
|
||
if (matched.slowTime > 0f) slowT = matched.slowTime;
|
||
if (matched.slowSpeed > 0f) slowS = matched.slowSpeed;
|
||
}
|
||
|
||
_currentPlaybackSpeed = slowS;
|
||
SetDirectorGraphSpeed(_playerDirector, slowS);
|
||
SetDirectorGraphSpeed(_enemyDirector, slowS);
|
||
|
||
if (tvRow != null && slowMotionShakeRect != null && tvRow.tweenDuration > 0f)
|
||
{
|
||
DOTween.Kill(slowMotionShakeRect, false);
|
||
slowMotionShakeRect.DOShakeAnchorPos(
|
||
tvRow.tweenDuration,
|
||
tvRow.shakeStrength,
|
||
tvRow.shakeVibrato,
|
||
tvRow.shakeRandomness,
|
||
tvRow.shakeSnapping,
|
||
tvRow.shakeFadeOut,
|
||
tvRow.shakeRandomnessMode);
|
||
}
|
||
|
||
await Awaiters.Seconds(slowT);
|
||
|
||
_currentPlaybackSpeed = 1f;
|
||
SetDirectorGraphSpeed(_playerDirector, 1f);
|
||
SetDirectorGraphSpeed(_enemyDirector, 1f);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError(e);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 播放超时战败表现(30分钟倒计时结束,玩家判负):双方 idle 循环播不阻塞,仅等待 <see cref="timeout_player_res"/>(<see cref="_resDirector"/>)后结算。
|
||
/// </summary>
|
||
private async Task PlayTimeoutDefeat()
|
||
{
|
||
PlayDirectorClipLoopingNoWait(_playerDirector, idle_player, 0);
|
||
PlayDirectorClipLoopingNoWait(_enemyDirector, idle_enemy, 0);
|
||
timeout_animation.SetActive(true);
|
||
ko_animation.SetActive(false);
|
||
defeat_animation.SetActive(false);
|
||
await PlayResDirectorOverlayAsync(timeout_player_res, timeout_animation_delay, 0);
|
||
await ShowResult(false);
|
||
}
|
||
|
||
#endregion
|
||
#region Animations
|
||
|
||
/// <summary>
|
||
/// 扣血飘字:显示本次减少量,打开节点并播放挂载在同一物体上的 Animation。
|
||
/// </summary>
|
||
private static void TryPlayHpDeductEffect(TMP_Text deductText, int hpBefore, int hpAfter)
|
||
{
|
||
int deduct = hpBefore - hpAfter;
|
||
if (deductText == null || deduct <= 0) return;
|
||
|
||
deductText.text = $"-{deduct}";
|
||
deductText.gameObject.SetActive(true);
|
||
var anim = deductText.GetComponent<Animation>();
|
||
if (anim == null) return;
|
||
anim.Play();
|
||
}
|
||
|
||
private void PlayPlayerHpTextAni()
|
||
{
|
||
if(PresentationData == null||Data==null) return;
|
||
int hpBefore = PresentationData.PlayerHp;
|
||
int hpAfter = PresentationData.KilledPlayer ? 0 : Data.PlayerHp;
|
||
TryPlayHpDeductEffect(_textPlayerHpDeduct, hpBefore, hpAfter);
|
||
|
||
int currentPlayerShowHp = hpBefore;
|
||
var target = hpAfter;
|
||
DOTween.To(() => currentPlayerShowHp, x => currentPlayerShowHp = x, target, hpTextChangeTime).OnUpdate(() =>
|
||
{
|
||
_textMyselfBlood.text = $"{currentPlayerShowHp}";
|
||
}).SetId(nameof(_textMyselfBlood));
|
||
}
|
||
private void PlayPlayerHpChangeAni()
|
||
{
|
||
if(PresentationData == null||Data==null) return;
|
||
PlayPlayerHpTextAni();
|
||
var fillAmount = Data.MaxHp > 0 ? PresentationData.KilledPlayer ? 0 : (float)Data.PlayerHp / Data.MaxHp : 0;
|
||
_imgBarMyself.DOFillAmount(fillAmount, hpBarChangeTime);
|
||
}
|
||
/// <summary>
|
||
/// 玩家段敌人血条终点:避免同一帧内 NPC 反击致死触发续命重置关卡后,<see cref="Event1v1BattleSlapData.NpcHp"/> 已回满导致「打了不掉血」。
|
||
/// </summary>
|
||
private int GetNpcHpAfterPlayerAttackForDisplay()
|
||
{
|
||
if (PresentationData.KilledNpc) return 0;
|
||
if (PresentationData.NpcHpAfterPlayerAttack >= 0)
|
||
return PresentationData.NpcHpAfterPlayerAttack;
|
||
return Data.NpcHp;
|
||
}
|
||
|
||
private void PlayNpcHpTextAni()
|
||
{
|
||
if(PresentationData == null||Data==null) return;
|
||
int hpBefore = PresentationData.NpcHp;
|
||
int hpAfter = GetNpcHpAfterPlayerAttackForDisplay();
|
||
TryPlayHpDeductEffect(_textEnemyHpDeduct, hpBefore, hpAfter);
|
||
|
||
int currentNpcShowHp = hpBefore;
|
||
var target = hpAfter;
|
||
DOTween.To(() => currentNpcShowHp, x => currentNpcShowHp = x, target, hpTextChangeTime).OnUpdate(() =>
|
||
{
|
||
_textEnemyBlood.text = $"{currentNpcShowHp}";
|
||
}).SetId(nameof(_textEnemyBlood));
|
||
}
|
||
private void PlayNpcHpChangeAni()
|
||
{
|
||
if(PresentationData == null||Data==null) return;
|
||
PlayNpcHpTextAni();
|
||
int npcHpEnd = GetNpcHpAfterPlayerAttackForDisplay();
|
||
var fillAmount = Data.MaxHp > 0 ? (float)npcHpEnd / Data.MaxHp : 0;
|
||
_imgBarEnemy.DOFillAmount(fillAmount, hpBarChangeTime);
|
||
}
|
||
#endregion
|
||
}
|
||
} |