using System;
using System.Collections;
using asap.core;
using DG.Tweening;
using Game;
using UnityEngine;
///
/// 的视频角色版本:用 替代 Spine。
/// 下方各槽位分别绑定 .bytes。Idle 与 Smash 均为朝下表现。向上走后、砸锤前插一段向下转身(专用片段或回退 TurnToFaceDown);走位仍用 TurnToFaceUp / TurnToFaceDown。
/// 不在 Start 里抢播 Idle,避免盖住 JumpToIndex 入场的 CoCharacterScrollToReward 走路视频。
/// 入场与一键领取的走位目标为 (Spine 基类入场仍为 rewardIndex)。
/// 一键领取与单行领取均走 CoMoveTurnSmashAtTierThen;一键可单独配置重锤片段 clipSmashClaimAll(留空则用 clipSmash)。砸锤时可配 sfxSmash / sfxSmashClaimAll(Addressable 名),与对应视频同步触发 EventUISound。
///
public class MiniBattlePassSecondVideoPanel : MiniBattlePassSecondPanel, IMiniBattlePassAnimatedClaim
{
public bool ClaimAnimationInProgress => _claimSequenceRunning;
///
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;
/// 当前是否已面朝「下一步沿局部 +Y 移动」。
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(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));
}
///
/// 视频版入场:列表与角色走到「最高可领取档」;Spine 版仍沿用基类 的 rewardIndex 逻辑。
///
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);
}
///
/// 向上走:朝向不对时先 TurnToFaceUp 再 WalkUp。向下走:不播转身,直接 WalkDown 并将逻辑朝向置为下(与砸/Idle 一致)。
///
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;
}
}
///
/// 将角色走到 对应行;若刚向上走完仍面朝上,先播向下转身再砸,再 ,最后 Idle。
///
/// 非空时用作砸片段(一键领取传 );空则用 。
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();
}
}
///
/// 播 Idle 循环,并把逻辑朝向与 Inspector 中「Idle 朝向」 对齐(默认 false=朝下)。
///
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);
}
///
/// 与 / 对齐: 非空表示一键专用锤片段,否则为单次/一键共用锤。
///
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;
}
}
}