252 lines
8.9 KiB
C#
252 lines
8.9 KiB
C#
using System;
|
||
using System.Collections;
|
||
using System.Linq;
|
||
using AibisDream.Framework;
|
||
using DG.Tweening;
|
||
using UnityEngine;
|
||
using UnityEngine.EventSystems;
|
||
|
||
namespace AibisDream.MiniGame.Tarot
|
||
{
|
||
public enum TarotOrientation
|
||
{
|
||
Normal,
|
||
Inverted
|
||
}
|
||
|
||
[RequireComponent(typeof(SpriteRenderer))]
|
||
[RequireComponent(typeof(BoxCollider2D))]
|
||
public class TarotCard : MonoBehaviour, IInteraction
|
||
{
|
||
#region IInteraction
|
||
|
||
public bool IsActive => _isInteractable;
|
||
public bool IsAvailable => _isInteractable;
|
||
public GameObject GetGameObject() => gameObject;
|
||
|
||
#endregion
|
||
|
||
#region Public State
|
||
|
||
public bool IsFaceUp { get; private set; }
|
||
public TarotOrientation Orientation { get; private set; } = TarotOrientation.Normal;
|
||
public int Index { get; private set; }
|
||
|
||
#endregion
|
||
|
||
#region Events
|
||
|
||
public Action OnHoverEnter;
|
||
public Action OnHoverExit;
|
||
/// <summary>PointerDown(按下时触发;点击确认由 TarotDeck 在 MouseButtonUp 时统一处理,避免 hover 位移动导致 PointerExit 阻断 PointerClick)</summary>
|
||
public Action OnPointerDown;
|
||
|
||
#endregion
|
||
|
||
private SpriteRenderer _spriteRenderer;
|
||
private EventTriggerEx _eventTrigger;
|
||
private Sprite _frontSprite;
|
||
private Sprite _backSprite;
|
||
private bool _isInteractable;
|
||
private Tweener _flipTween;
|
||
private Tweener _orientationTween;
|
||
|
||
public void Init(int index, Sprite frontSprite, Sprite backSprite)
|
||
{
|
||
Index = index;
|
||
_spriteRenderer = GetComponent<SpriteRenderer>();
|
||
// 运行时若 TarotManager 未拖入 Sprite,不要用 null 覆盖预制体上的图,否则牌会完全消失
|
||
_frontSprite = frontSprite;
|
||
_backSprite = backSprite != null ? backSprite : _spriteRenderer.sprite;
|
||
if (backSprite != null)
|
||
_spriteRenderer.sprite = backSprite;
|
||
else if (_spriteRenderer.sprite == null)
|
||
Debug.LogWarning("[TarotCard] 背面与预制体均无 Sprite,牌将不可见。请在 TarotManager 上指定 Back / Front Sprite。", this);
|
||
|
||
IsFaceUp = false;
|
||
Orientation = TarotOrientation.Normal;
|
||
transform.localScale = Vector3.one;
|
||
transform.localRotation = Quaternion.identity;
|
||
}
|
||
|
||
private void Awake()
|
||
{
|
||
_spriteRenderer = GetComponent<SpriteRenderer>();
|
||
SetupEventTrigger();
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
if (_eventTrigger != null)
|
||
{
|
||
_eventTrigger.UnRegister(EventTriggerType.PointerEnter, HandlePointerEnter);
|
||
_eventTrigger.UnRegister(EventTriggerType.PointerExit, HandlePointerExit);
|
||
_eventTrigger.UnRegister(EventTriggerType.PointerDown, HandlePointerDown);
|
||
}
|
||
|
||
_flipTween?.Kill();
|
||
_orientationTween?.Kill();
|
||
}
|
||
|
||
#region EventTriggerEx Setup
|
||
|
||
private void SetupEventTrigger()
|
||
{
|
||
_eventTrigger = GetComponent<EventTriggerEx>();
|
||
if (_eventTrigger == null)
|
||
{
|
||
_eventTrigger = gameObject.AddComponent<EventTriggerEx>();
|
||
}
|
||
|
||
EnsureEventTriggerEntries();
|
||
_eventTrigger.Register(EventTriggerType.PointerEnter, HandlePointerEnter);
|
||
_eventTrigger.Register(EventTriggerType.PointerExit, HandlePointerExit);
|
||
_eventTrigger.Register(EventTriggerType.PointerDown, HandlePointerDown);
|
||
}
|
||
|
||
private void EnsureEventTriggerEntries()
|
||
{
|
||
if (_eventTrigger == null) return;
|
||
var triggers = _eventTrigger.triggers;
|
||
var needed = new[]
|
||
{
|
||
EventTriggerType.PointerEnter,
|
||
EventTriggerType.PointerExit,
|
||
EventTriggerType.PointerDown
|
||
};
|
||
foreach (var t in needed)
|
||
{
|
||
if (triggers.Any(e => e.eventID == t)) continue;
|
||
triggers.Add(new EventTrigger.Entry { eventID = t });
|
||
}
|
||
}
|
||
|
||
private void HandlePointerEnter(BaseEventData data)
|
||
{
|
||
if (!_isInteractable) return;
|
||
OnHoverEnter?.Invoke();
|
||
}
|
||
|
||
private void HandlePointerExit(BaseEventData data)
|
||
{
|
||
if (!_isInteractable) return;
|
||
OnHoverExit?.Invoke();
|
||
}
|
||
|
||
private void HandlePointerDown(BaseEventData data)
|
||
{
|
||
if (!_isInteractable) return;
|
||
OnPointerDown?.Invoke();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Public API
|
||
|
||
public void SetInteractable(bool interactable)
|
||
{
|
||
_isInteractable = interactable;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Flip the card to reveal front side with a target orientation.
|
||
/// X-scale: 1 -> 0 (swap sprite) -> 1, then apply orientation via local Z rotation (0° / 180°).
|
||
/// </summary>
|
||
public IEnumerator Flip(TarotOrientation targetOrientation, float duration = 0.5f)
|
||
{
|
||
_flipTween?.Kill();
|
||
_orientationTween?.Kill();
|
||
float half = duration * 0.5f;
|
||
var scale = transform.localScale;
|
||
|
||
var seq = DOTween.Sequence();
|
||
seq.Append(transform.DOScaleX(0f, half).SetEase(Ease.InQuad));
|
||
seq.AppendCallback(() =>
|
||
{
|
||
IsFaceUp = !IsFaceUp;
|
||
var next = IsFaceUp ? _frontSprite : _backSprite;
|
||
if (next != null)
|
||
_spriteRenderer.sprite = next;
|
||
else if (IsFaceUp)
|
||
Debug.LogWarning("[TarotCard] Front Sprite 未设置,翻牌后无法显示正面。", this);
|
||
|
||
ApplyOrientationToTransform(targetOrientation);
|
||
});
|
||
seq.Append(transform.DOScaleX(scale.x, half).SetEase(Ease.OutQuad));
|
||
|
||
yield return seq.WaitForCompletion();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Change orientation of an already face-up card (rotate 180 degrees around local Z).
|
||
/// 结束后与翻牌一致:用本地 Z 旋转表示逆位,避免动画末尾用负 scale.y 与 Rz(180°) 不等价造成镜像闪变。
|
||
/// </summary>
|
||
public IEnumerator SetOrientationAnimated(TarotOrientation targetOrientation, float duration = 0.7f)
|
||
{
|
||
if (Orientation == targetOrientation) yield break;
|
||
|
||
_flipTween?.Kill();
|
||
_orientationTween?.Kill();
|
||
|
||
// 旧版用负 scale.y 表示逆位时,收束为绕本地 Z 再转 180°,避免与后续 Rz 动画叠加错乱
|
||
EnsureOrientationUsesRotationNotNegativeScale();
|
||
|
||
// 使用本地旋转,避免 DORotate(世界欧拉)与 localEulerAngles 混用导致几乎不转或乱跳
|
||
var endLocal = transform.localRotation * Quaternion.Euler(0f, 0f, 180f);
|
||
_orientationTween = transform.DOLocalRotateQuaternion(endLocal, duration)
|
||
.SetEase(Ease.InOutSine);
|
||
|
||
yield return _orientationTween.WaitForCompletion();
|
||
_orientationTween = null;
|
||
|
||
ApplyOrientationToTransform(targetOrientation);
|
||
}
|
||
|
||
public void SetOrientationImmediate(TarotOrientation orientation)
|
||
{
|
||
ApplyOrientationToTransform(orientation);
|
||
}
|
||
|
||
/// <summary>正立/逆位统一用本地 Z(0° / 180°),scale.y 恒为正。</summary>
|
||
private void ApplyOrientationToTransform(TarotOrientation orientation)
|
||
{
|
||
Orientation = orientation;
|
||
transform.localRotation = Quaternion.Euler(0f, 0f, orientation == TarotOrientation.Inverted ? 180f : 0f);
|
||
var s = transform.localScale;
|
||
s.y = Mathf.Abs(s.y);
|
||
transform.localScale = s;
|
||
}
|
||
|
||
/// <summary>仅处理旧版负 scale.y:改为正 scale 并在本地绕 Z 补 180°,不覆盖扇形等已有 localRotation。</summary>
|
||
private void EnsureOrientationUsesRotationNotNegativeScale()
|
||
{
|
||
var s = transform.localScale;
|
||
if (s.y >= 0f) return;
|
||
s.y = Mathf.Abs(s.y);
|
||
transform.localScale = s;
|
||
transform.localRotation = transform.localRotation * Quaternion.Euler(0f, 0f, 180f);
|
||
}
|
||
|
||
public void SetAlpha(float alpha)
|
||
{
|
||
if (_spriteRenderer == null) _spriteRenderer = GetComponent<SpriteRenderer>();
|
||
var c = _spriteRenderer.color;
|
||
c.a = alpha;
|
||
_spriteRenderer.color = c;
|
||
}
|
||
|
||
public Tweener DOFade(float target, float duration)
|
||
{
|
||
return _spriteRenderer.DOFade(target, duration);
|
||
}
|
||
|
||
public void SetSortingOrder(int order)
|
||
{
|
||
if (_spriteRenderer == null) _spriteRenderer = GetComponent<SpriteRenderer>();
|
||
_spriteRenderer.sortingOrder = order;
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
}
|