using asap.core; using Assets.Scripts.UI.AnimationSimulator; using Game; using System; using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; namespace UI.Wheel { public abstract class AbstractWheelRewardComponent : MonoBehaviour { #region Protected Fields protected AbstractWheelRewardCell[] wheelRewardCells; protected AnimationSimulatorComponent simulator; protected Transform rewardContainer; protected virtual string StartSound => string.Empty; protected virtual string LoopedSound => string.Empty; protected virtual string EndSound => string.Empty; private EventUISound _startUISound; private EventUISound _loopedUISound; private EventUISound _endUISound; #endregion #region Serialized Fields [SerializeField, Min(0f)] protected float delayShowRewardPopup = 0.1f; [SerializeField, Min(0f),Tooltip("指针旋转的角度范围(-20,20)")] protected float pointerAngle = 20f; [SerializeField, Min(0f)] protected float wheelAngle = 14f; [SerializeField] protected float revertAngleTime = 0.01f; [SerializeField, Min(0f)] protected float cdEffectVelocity = 5f; #endregion #region Private Fields protected const float FULL_CIRCLE = 360f; protected float SegmentAngle => FULL_CIRCLE / wheelRewardCells?.Length??1 ; protected int MaxSegment => (int)(FULL_CIRCLE / SegmentAngle) -1; #endregion #region Abstract Methods public abstract void SetData(List rewards); public abstract void ShowUI(); #endregion #region Initialization protected virtual void Awake() { simulator = this.GetComponent(); wheelRewardCells = this.GetComponentsInChildren(); rewardContainer = this.gameObject.FindChildGameObject(nameof(rewardContainer))?.transform; } protected virtual void InitUI() { } protected virtual void Start() { InitUI(); } #endregion #region Wheel Animation public virtual async Task PlayEffect(int rewardIndex, IWheelReward reward, Action endCallback) { EffectStart(); Debug.Log($"[Wheel] PlayEffect - rewardIndex: {rewardIndex}, wheelRewardCells.Length: {wheelRewardCells.Length}, SegmentAngle: {SegmentAngle}"); var param = new AnimationSimulatorComponent.Parameter(); param.scale = FULL_CIRCLE; param.startValue = rewardContainer.eulerAngles.z; param.offsetDirection = RandomOffsetDirection(reward); param.AccelerationEndCallback = AccelerationEndCallback; param.updateCallback = EffectUpdate; param.revertTargetEndCallbcak = RevertTargetEndCallbcak; param.uniformCompletionStartCallBack = (currentValue) => CalculateTargetAngle(rewardIndex, currentValue); await simulator.PlayEffect(param); await EffectEnd(reward); endCallback?.Invoke(); } protected virtual void AccelerationEndCallback() { if (string.IsNullOrEmpty(LoopedSound)) return; if (_startUISound != null) GContext.Publish(new FadeOutEvent(_startUISound.Sound)); _loopedUISound= new EventUISound(LoopedSound); GContext.Publish(_loopedUISound); } private float CalculateTargetAngle(int rewardIndex, float currentValue) { int safeIndex = Mathf.Clamp(rewardIndex, 0, wheelRewardCells.Length - 1); float cellAngle = SegmentAngle; // 归一化到 [0, 360) float normalizedCurrent = currentValue % FULL_CIRCLE; if (normalizedCurrent < 0) normalizedCurrent += FULL_CIRCLE; // 目标格子角度(0-360范围内) float targetSlotAngle = safeIndex * cellAngle + cellAngle * 0.5f; // 计算当前圈数(考虑负数圈) float currentRevolutions = Mathf.Floor((currentValue - normalizedCurrent) / FULL_CIRCLE); // 目标角度 = 当前圈数 × 360 + 目标格子角度 float targetAngle = currentRevolutions * FULL_CIRCLE + targetSlotAngle; // 关键:如果targetAngle > currentValue,说明目标在"后面",需要减去360(多转一圈,继续顺时针) // 这样确保targetAngle始终 <= currentValue(在顺时针旋转方向的前方) while (targetAngle > currentValue) { targetAngle -= FULL_CIRCLE; } float normalizedResult = targetAngle % FULL_CIRCLE; if (normalizedResult < 0) normalizedResult += FULL_CIRCLE; Debug.Log($"[Wheel] CalculateTargetAngle - rewardIndex: {rewardIndex}, safeIndex: {safeIndex}, cellAngle: {cellAngle}, currentValue: {currentValue}, currentRevolutions: {currentRevolutions}, targetAngle: {targetAngle}, normalizedResult: {normalizedResult}°"); return targetAngle; } protected virtual void EffectStart() { _ = DelayShow(); if (string.IsNullOrEmpty(StartSound)) return; _startUISound = new EventUISound(StartSound); GContext.Publish(_startUISound); } protected virtual async Task DelayShow() { await Awaiters.Seconds(simulator.GetAccelertationTime() / 3); } protected virtual void EffectUpdate(float currentVelocity, float currentValue) { rewardContainer.rotation = Quaternion.Euler(0, 0, currentValue); } protected virtual async Task EffectEnd(IWheelReward reward) { Debug.Log($"[BossFight] EffectEnd - rewardContainer.eulerAngles.z: {rewardContainer.eulerAngles.z}, normalized: {((rewardContainer.eulerAngles.z % 360 + 360) % 360)}°"); await Awaiters.Seconds(delayShowRewardPopup); } protected virtual void RevertTargetEndCallbcak() { if (string.IsNullOrEmpty(EndSound)) return; if (_loopedUISound!=null) GContext.Publish(new FadeOutEvent(_loopedUISound.Sound)); GContext.Publish(new EventUISound(EndSound)); } private bool RandomOffsetDirection(IWheelReward reward) { var random = UnityEngine.Random.Range(1, 101); return random <= reward.LeftWeight; } #endregion } }