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 } }