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
{
///
/// 1v1扇耳光大赛TV演示面板 Event1v1BattleSlapTvPopupPanel
/// 真正的战斗演示面板,本版本只实现基础UI,Timeline/Spine系统留到后续版本
///
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();
_imgBarEnemy = gameObject.FindChildGameObject("p_bar_enermy")?.GetComponent();
// 血量文本 - 在myself和enermy下
_textMyselfBlood = gameObject.FindChildGameObject("myself")?.FindChildGameObject("p_text_blood")?.GetComponent();
_textEnemyBlood = gameObject.FindChildGameObject("enermy")?.FindChildGameObject("p_text_blood")?.GetComponent();
_textPlayerHpDeduct = gameObject.FindChildGameObject("p_text_player_hp_deduct")?.GetComponent();
_textEnemyHpDeduct = gameObject.FindChildGameObject("p_text_enermy_hp_deduct")?.GetComponent();
// 角色对象
_enemy = gameObject.FindChildGameObject("head_enermy")?.GetComponent();
_myself = gameObject.FindChildGameObject("head_myself")?.GetComponent();
//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();
// 生命显示
_lifeImages = gameObject.FindChildGameObject("p_lives_icon")?.GetComponentsInChildren();
// 动画
_uiAnimation = GetComponent();
_resDirector = gameObject.FindChildGameObject("root")?.GetComponentInChildren(true);
_playerDirector = gameObject.FindChildGameObject("spine_player")?.GetComponentInChildren(true);
_enemyDirector = gameObject.FindChildGameObject("spine_enermy")?.GetComponentInChildren(true);
_tvCan = gameObject.GetComponentInChildren(true);
_signalReceiver = gameObject.GetComponent();
_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();
RefreshUI();
StartTvAfterEntranceAsync();
}
private async void StartTvAfterEntranceAsync()
{
try
{
if (_uiAnimation != null)
{
SetSpineFrozen(true);
var cg = GetComponent();
if (cg == null) cg = gameObject.AddComponent();
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();
_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 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 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;
}
/// 仅玩家回合:UI 罐头 → 可选 eat_player Timeline。
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);
}
///
/// 交错攻击+受击:唯一时序耦合是「攻击侧 Timeline 先播,经 秒后再开受击侧」。
/// 受击未死:hit → hit2idle → 循环 idle(Loop,不阻塞后续逻辑);击杀:hit → hit2ko → ko。
/// 攻击侧:攻击结束后同样循环 idle 且不等待。胜利/失败由 PlayVictoryAnimation / PlayDefeatAnimation 另行表现。
///
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());
}
///
/// 与 Timeline KO 同步在 上播放结果层 Timeline(如 / ),支持延时后打开节点并按时长等待结束。
///
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(true);
var enemySpine = _enemyDirector?.GetComponentInChildren(true);
if (playerSpine != null)
{
playerSpine.freeze = frozen;
if (frozen) playerSpine.AnimationState?.ClearTracks();
}
if (enemySpine != null)
{
enemySpine.freeze = frozen;
if (frozen) enemySpine.AnimationState?.ClearTracks();
}
}
/// 仅 / 使用 Loop;其它时间线一律 Hold。
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);
}
///
/// 不阻塞的 Timeline 播放:仅 idle 为 Loop,其余为 Hold(与 一致)。
///
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);
}
}
/// 等待 director 的动画时间推进指定量(受 PlayableGraph 速度影响,慢动作时自动延长)。
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;
}
}
/// 等待单个 Director 当前片段播放完毕。
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);
}
}
///
/// 播放超时战败表现(30分钟倒计时结束,玩家判负):双方 idle 循环播不阻塞,仅等待 ()后结算。
///
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
///
/// 扣血飘字:显示本次减少量,打开节点并播放挂载在同一物体上的 Animation。
///
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();
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);
}
///
/// 玩家段敌人血条终点:避免同一帧内 NPC 反击致死触发续命重置关卡后, 已回满导致「打了不掉血」。
///
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
}
}