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; /// PointerDown(按下时触发;点击确认由 TarotDeck 在 MouseButtonUp 时统一处理,避免 hover 位移动导致 PointerExit 阻断 PointerClick) 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(); // 运行时若 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(); 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(); if (_eventTrigger == null) { _eventTrigger = gameObject.AddComponent(); } 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; } /// /// 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°). /// 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(); } /// /// Change orientation of an already face-up card (rotate 180 degrees around local Z). /// 结束后与翻牌一致:用本地 Z 旋转表示逆位,避免动画末尾用负 scale.y 与 Rz(180°) 不等价造成镜像闪变。 /// 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); } /// 正立/逆位统一用本地 Z(0° / 180°),scale.y 恒为正。 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; } /// 仅处理旧版负 scale.y:改为正 scale 并在本地绕 Z 补 180°,不覆盖扇形等已有 localRotation。 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(); 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.sortingOrder = order; } #endregion } }