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