495 lines
16 KiB
C#
495 lines
16 KiB
C#
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 / sfxSmashClaimAll(Addressable 名),与对应视频同步触发 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;
|
||
}
|
||
}
|
||
}
|