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

168 lines
6.6 KiB
C#
Raw Permalink 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 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("指针旋转的角度范围(-2020")]
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<IWheelReward> rewards);
public abstract void ShowUI();
#endregion
#region Initialization
protected virtual void Awake()
{
simulator = this.GetComponent<AnimationSimulatorComponent>();
wheelRewardCells = this.GetComponentsInChildren<AbstractWheelRewardCell>();
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
}
}