Files
ft/Client/Assets/Scripts/Event1v1Battle/Panel/TVPanel/Event1v1BattleSlapTvPopupPanel.cs
2026-06-29 21:18:33 +08:00

880 lines
35 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.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
/// 真正的战斗演示面板本版本只实现基础UITimeline/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 → 循环 idleLoop不阻塞后续逻辑击杀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
}
}