Files
ft/Client/Assets/Scripts/MiniBP/UI/MiniBattlePassSecondVideoPanel.cs
2026-06-29 21:18:33 +08:00

495 lines
16 KiB
C#
Raw 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.Collections;
using asap.core;
using DG.Tweening;
using Game;
using UnityEngine;
/// <summary>
/// <see cref="MiniBattlePassSecondPanel"/> 的视频角色版本:用 <see cref="LeviathanVideoDecoderBase"/> 替代 Spine。
/// 下方各槽位分别绑定 .bytes。Idle 与 Smash 均为朝下表现。向上走后、砸锤前插一段向下转身(专用片段或回退 TurnToFaceDown走位仍用 TurnToFaceUp / TurnToFaceDown。
/// 不在 Start 里抢播 Idle避免盖住 JumpToIndex 入场的 CoCharacterScrollToReward 走路视频。
/// 入场与一键领取的走位目标为 <see cref="MiniBattlePassDataModel.GetHighestClaimableTierIndex"/>Spine 基类入场仍为 rewardIndex
/// 一键领取与单行领取均走 CoMoveTurnSmashAtTierThen一键可单独配置重锤片段 clipSmashClaimAll留空则用 clipSmash。砸锤时可配 sfxSmash / sfxSmashClaimAllAddressable 名),与对应视频同步触发 EventUISound。
/// </summary>
public class MiniBattlePassSecondVideoPanel : MiniBattlePassSecondPanel, IMiniBattlePassAnimatedClaim
{
public bool ClaimAnimationInProgress => _claimSequenceRunning;
/// <inheritdoc />
public bool TryScheduleClaim(int tierIndex, bool isFreeReward)
{
if (videoDecoder == null || !HasMinimumClips())
{
return false;
}
if (_claimSequenceRunning)
{
return false;
}
StartCoroutine(CoSingleTierClaimWithSmash(tierIndex, isFreeReward));
return true;
}
public enum MiniBattlePassSecondVideoClip
{
Idle = 0,
WalkUp = 1,
WalkDown = 2,
TurnToFaceUp = 3,
TurnToFaceDown = 4,
Smash = 5,
}
[SerializeField]
private LeviathanVideoDecoderBase videoDecoder;
[Header("视频片段 (.bytes)")]
[SerializeField]
[Tooltip("待机循环")]
private TextAsset clipIdle;
[SerializeField]
[Tooltip("朝局部 +Y列表向上行走")]
private TextAsset clipWalkUp;
[SerializeField]
[Tooltip("朝局部 Y列表向下行走")]
private TextAsset clipWalkDown;
[SerializeField]
[Tooltip("转身至可走 WalkUp可空不播转身但仍会更新朝向状态")]
private TextAsset clipTurnToFaceUp;
[SerializeField]
[Tooltip("仅作砸前向下转身回退clipTurnDownBeforeSmash 留空时);向下走位不再播本段")]
private TextAsset clipTurnToFaceDown;
[SerializeField]
[Tooltip("向上走完、砸锤前的向下转身;留空则沿用 TurnToFaceDown")]
private TextAsset clipTurnDownBeforeSmash;
[SerializeField]
[Tooltip("砸(单次);单行领奖与一键领取默认共用;与 Idle 同为朝下")]
private TextAsset clipSmash;
[SerializeField]
[Tooltip("一键领取专用重锤;留空则一键也播 clipSmash")]
private TextAsset clipSmashClaimAll;
[Header("砸锤音效Addressable 音频名,与视频同步)")]
[SerializeField]
[Tooltip("播 clipSmash 时触发;留空不播")]
private string sfxSmash;
[SerializeField]
[Tooltip("播 clipSmashClaimAll 时触发;留空则回退为 sfxSmash。未配专用锤片段时仍播 clipSmash只用 sfxSmash")]
private string sfxSmashClaimAll;
[Tooltip("与 Idle 朝向一致:默认关=面朝下Y开=面朝上(+Y。向下走位不播转身")]
[SerializeField]
private bool initiallyFacesTowardPositiveY = false;
[Tooltip("角色沿 Y 走向目标行时的局部坐标速度(与 item 行高同单位)")]
[SerializeField]
private float walkLocalUnitsPerSecond = 400f;
[SerializeField]
private LeviathanVideoDecoderBase.PlayAlphaType playAlphaType = LeviathanVideoDecoderBase.PlayAlphaType.None;
[SerializeField]
private bool multithreadedDecode = true;
private bool _claimSequenceRunning;
/// <summary>当前是否已面朝「下一步沿局部 +Y 移动」。</summary>
private bool _facesTowardPositiveY;
private bool HasMinimumClips()
{
return clipIdle != null
&& clipWalkUp != null
&& clipWalkDown != null
&& clipSmash != null;
}
private TextAsset ResolveClip(MiniBattlePassSecondVideoClip clip)
{
switch (clip)
{
case MiniBattlePassSecondVideoClip.Idle:
return clipIdle;
case MiniBattlePassSecondVideoClip.WalkUp:
return clipWalkUp;
case MiniBattlePassSecondVideoClip.WalkDown:
return clipWalkDown;
case MiniBattlePassSecondVideoClip.TurnToFaceUp:
return clipTurnToFaceUp;
case MiniBattlePassSecondVideoClip.TurnToFaceDown:
return clipTurnToFaceDown;
case MiniBattlePassSecondVideoClip.Smash:
return clipSmash;
default:
return null;
}
}
protected override void Awake()
{
base.Awake();
_facesTowardPositiveY = initiallyFacesTowardPositiveY;
if (videoDecoder == null && spineRoot != null)
{
videoDecoder = spineRoot.GetComponentInChildren<LeviathanVideoDecoderBase>(true);
}
}
protected override void ClickClaimAll()
{
if (_claimSequenceRunning)
{
return;
}
if (videoDecoder == null || !HasMinimumClips())
{
base.ClickClaimAll();
return;
}
StartCoroutine(CoClaimAllWithSmash());
}
protected override void StartCharacterScrollToReward(int startRow, int endRow, float endScrollPos, float duration)
{
if (!HasMinimumClips() || videoDecoder == null || spineRoot == null || scrollRect == null)
{
base.StartCharacterScrollToReward(startRow, endRow, endScrollPos, duration);
return;
}
StartCoroutine(CoCharacterScrollToReward(startRow, endRow, endScrollPos, duration));
}
/// <summary>
/// 视频版入场列表与角色走到「最高可领取档」Spine 版仍沿用基类 <see cref="MiniBattlePassSecondPanel.JumpToIndex"/> 的 rewardIndex 逻辑。
/// </summary>
protected override void JumpToIndex()
{
if (spineRoot == null)
{
return;
}
int index = miniBP.Data.showIndex;
float startPos = ((index - 2) * 1f / (miniBPItemDatas.Count - showCount));
if (startPos > 1f)
{
startPos = 1f;
}
if (startPos < 0f)
{
startPos = 0f;
}
scrollRect.verticalNormalizedPosition = startPos;
int startRow = GetDisplayRowIndexForDataIndex(miniBP.Data.showIndex);
SetCharacterRootY(startRow);
int anchor = miniBP.GetHighestClaimableTierIndex();
if (anchor != index)
{
miniBP.SetShowIndexForPanelView(anchor);
float endPos = ((anchor - 2) * 1f / (miniBPItemDatas.Count - showCount));
if (endPos > 1f)
{
endPos = 1f;
}
if (endPos < 0f)
{
endPos = 0f;
}
const float scrollDuration = 1f;
int endRow = GetDisplayRowIndexForDataIndex(anchor);
StartCharacterScrollToReward(startRow, endRow, endPos, scrollDuration);
}
else
{
PlayCharacterIdleAnimation();
}
}
private IEnumerator CoCharacterScrollToReward(int startRow, int endRow, float endScrollPos, float duration)
{
bool towardPositive = endRow > startRow;
yield return CoEnsureFacingAndPlayWalk(towardPositive);
scrollRect.DOVerticalNormalizedPos(endScrollPos, duration).SetEase(Ease.Linear);
spineRoot.DOLocalMoveY(endRow * itemHeight, duration).OnComplete(OnCharacterScrollArrived);
}
/// <summary>
/// 向上走:朝向不对时先 TurnToFaceUp 再 WalkUp。向下走不播转身直接 WalkDown 并将逻辑朝向置为下(与砸/Idle 一致)。
/// </summary>
private IEnumerator CoEnsureFacingAndPlayWalk(bool towardPositive)
{
if (towardPositive)
{
if (!_facesTowardPositiveY)
{
TextAsset turnClip = ResolveClip(MiniBattlePassSecondVideoClip.TurnToFaceUp);
if (turnClip != null)
{
yield return CoPlayDecoderOnce(turnClip);
}
_facesTowardPositiveY = true;
}
}
else
{
_facesTowardPositiveY = false;
}
TextAsset walkClip = towardPositive
? ResolveClip(MiniBattlePassSecondVideoClip.WalkUp)
: ResolveClip(MiniBattlePassSecondVideoClip.WalkDown);
if (walkClip != null)
{
PlayDecoderRaw(walkClip, loop: true);
}
else
{
PlayCharacterIdleAnimation();
}
}
private IEnumerator CoClaimAllWithSmash()
{
_claimSequenceRunning = true;
if (btn_claim != null)
{
btn_claim.interactable = false;
}
try
{
yield return CoClaimAllWithSmashCore();
}
finally
{
if (btn_claim != null)
{
btn_claim.interactable = true;
}
_claimSequenceRunning = false;
}
}
private IEnumerator CoClaimAllWithSmashCore()
{
yield return CoMoveTurnSmashAtTierThen(miniBP.GetHighestClaimableTierIndex(), () => base.ClickClaimAll(), clipSmashClaimAll);
}
private IEnumerator CoSingleTierClaimWithSmash(int tierIndex, bool isFreeReward)
{
_claimSequenceRunning = true;
try
{
yield return CoMoveTurnSmashAtTierThen(tierIndex, () =>
{
miniBP.ReceiveReward(tierIndex, isFreeReward);
GContext.Publish(new RefreshPanelEvent());
});
}
finally
{
_claimSequenceRunning = false;
}
}
/// <summary>
/// 将角色走到 <paramref name="rewardTierIndex"/> 对应行;若刚向上走完仍面朝上,先播向下转身再砸,再 <paramref name="afterSmash"/>,最后 Idle。
/// </summary>
/// <param name="smashClipOverride">非空时用作砸片段(一键领取传 <see cref="clipSmashClaimAll"/>);空则用 <see cref="clipSmash"/>。</param>
private IEnumerator CoMoveTurnSmashAtTierThen(int rewardTierIndex, Action afterSmash, TextAsset smashClipOverride = null)
{
if (spineRoot != null)
{
float targetY = GetDisplayRowIndexForDataIndex(rewardTierIndex) * itemHeight;
float currentY = spineRoot.localPosition.y;
float delta = targetY - currentY;
bool towardPositive = delta > 0.001f;
float distance = Mathf.Abs(delta);
float walkDuration = walkLocalUnitsPerSecond > 0.01f ? distance / walkLocalUnitsPerSecond : 0f;
if (distance > 0.5f)
{
yield return CoEnsureFacingAndPlayWalk(towardPositive);
Tween moveTween = spineRoot.DOLocalMoveY(targetY, walkDuration).SetEase(Ease.Linear);
bool moveDone = false;
moveTween.OnComplete(() => moveDone = true);
while (!moveDone && moveTween.IsActive())
{
yield return null;
}
}
else
{
SetCharacterRootY(GetDisplayRowIndexForDataIndex(rewardTierIndex));
if (!towardPositive)
{
_facesTowardPositiveY = false;
}
}
}
if (_facesTowardPositiveY)
{
TextAsset turnDown = clipTurnDownBeforeSmash != null ? clipTurnDownBeforeSmash : clipTurnToFaceDown;
if (turnDown != null)
{
yield return CoPlayDecoderOnce(turnDown);
}
}
_facesTowardPositiveY = false;
TextAsset smashClip = smashClipOverride != null ? smashClipOverride : ResolveClip(MiniBattlePassSecondVideoClip.Smash);
if (smashClip != null)
{
TryPlaySmashSfx(smashClipOverride);
yield return CoPlayDecoderOnce(smashClip);
}
afterSmash?.Invoke();
if (ClipExists(MiniBattlePassSecondVideoClip.Idle))
{
PlayDecoderIdleLoopSyncFacing();
}
}
/// <summary>
/// 播 Idle 循环,并把逻辑朝向与 Inspector 中「Idle 朝向」<see cref="initiallyFacesTowardPositiveY"/> 对齐(默认 false=朝下)。
/// </summary>
private void PlayDecoderIdleLoopSyncFacing()
{
_facesTowardPositiveY = initiallyFacesTowardPositiveY;
PlayDecoderClip(MiniBattlePassSecondVideoClip.Idle, loop: true);
}
protected override void PlayCharacterIdleAnimation()
{
if (videoDecoder != null && ClipExists(MiniBattlePassSecondVideoClip.Idle))
{
PlayDecoderIdleLoopSyncFacing();
return;
}
base.PlayCharacterIdleAnimation();
}
protected override void PlayCharacterMoveAnimation()
{
if (videoDecoder != null && HasMinimumClips())
{
var w = _facesTowardPositiveY
? ResolveClip(MiniBattlePassSecondVideoClip.WalkUp)
: ResolveClip(MiniBattlePassSecondVideoClip.WalkDown);
if (w != null)
{
PlayDecoderRaw(w, loop: true);
return;
}
}
base.PlayCharacterMoveAnimation();
}
protected override void OnCharacterScrollArrived()
{
if (videoDecoder != null && ClipExists(MiniBattlePassSecondVideoClip.Idle))
{
PlayDecoderIdleLoopSyncFacing();
return;
}
base.OnCharacterScrollArrived();
}
private bool ClipExists(MiniBattlePassSecondVideoClip clip)
{
return ResolveClip(clip) != null;
}
private void PlayDecoderClip(MiniBattlePassSecondVideoClip clip, bool loop)
{
var ta = ResolveClip(clip);
if (videoDecoder == null || ta == null)
{
return;
}
PlayDecoderRaw(ta, loop);
}
private void PlayDecoderRaw(TextAsset ta, bool loop)
{
if (videoDecoder == null || ta == null)
{
return;
}
videoDecoder.SetLooping(loop);
videoDecoder.ChangeVideo(ta, playAlphaType, multithreadedDecode);
}
/// <summary>
/// 与 <see cref="clipSmash"/> / <see cref="clipSmashClaimAll"/> 对齐:<paramref name="smashClipOverride"/> 非空表示一键专用锤片段,否则为单次/一键共用锤。
/// </summary>
private void TryPlaySmashSfx(TextAsset smashClipOverride)
{
string name = smashClipOverride != null
? (string.IsNullOrEmpty(sfxSmashClaimAll) ? sfxSmash : sfxSmashClaimAll)
: sfxSmash;
if (string.IsNullOrEmpty(name))
{
return;
}
GContext.Publish(new EventUISound(name));
}
private IEnumerator CoPlayDecoderOnce(TextAsset clip)
{
if (videoDecoder == null || clip == null)
{
yield break;
}
bool completed = false;
void Handler()
{
completed = true;
}
videoDecoder.OnPlayCompleted += Handler;
try
{
videoDecoder.SetLooping(false);
videoDecoder.ChangeVideo(clip, playAlphaType, multithreadedDecode);
yield return new WaitUntil(() => completed);
}
finally
{
videoDecoder.OnPlayCompleted -= Handler;
}
}
}