using System.Threading; using Cysharp.Threading.Tasks; using LitMotion; using LitMotion.Extensions; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; [RequireComponent(typeof(CanvasGroup))] public class CardView : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler { [Header("Visual Elements")] [SerializeField] private GameObject frontFace; [SerializeField] private GameObject backFace; [SerializeField] private Image frontBackground; [SerializeField] private TMP_Text titleText; public CardModel Model { get; private set; } public CardContainer CurrentContainer { get; set; } public bool IsFaceUp { get; private set; } = true; public bool IsInteractable { get; set; } = true; private RectTransform _rectTransform; private Canvas _rootCanvas; private CanvasGroup _canvasGroup; private GameObject _placeholder; private CardContainer _originContainer; private MotionHandle _moveMotion; private MotionHandle _scaleMotion; private MotionHandle _rotMotion; private void Awake() { _rectTransform = GetComponent(); _canvasGroup = GetComponent(); _rootCanvas = GetComponentInParent(); } public void Initialize(CardModel model, bool faceUp) { Model = model; titleText.text = model.GetTitle(); frontBackground.color = model.GetSuitColor(); SetFaceInstant(faceUp); } public void SetFaceInstant(bool faceUp) { IsFaceUp = faceUp; frontFace.SetActive(faceUp); backFace.SetActive(!faceUp); _rectTransform.localEulerAngles = Vector3.zero; } public async UniTask FlipAsync(bool faceUp, float duration = 0.25f, CancellationToken ct = default) { if (IsFaceUp == faceUp) return; IsFaceUp = faceUp; CancelMotion(ref _rotMotion); SoundManager.Instance?.PlayFlip(); _rotMotion = LMotion.Create(0f, 90f, duration * 0.5f) .WithEase(Ease.InQuad) .BindToLocalEulerAnglesY(_rectTransform); await _rotMotion.ToUniTask(ct); frontFace.SetActive(faceUp); backFace.SetActive(!faceUp); _rotMotion = LMotion.Create(90f, 0f, duration * 0.5f) .WithEase(Ease.OutQuad) .BindToLocalEulerAnglesY(_rectTransform); await _rotMotion.ToUniTask(ct); } public async UniTask AnimateToTargetAsync(CardContainer targetContainer, int siblingIndex = -1, float duration = 0.3f, CancellationToken ct = default) { CancelMotion(ref _moveMotion); CancelMotion(ref _scaleMotion); IsInteractable = false; _canvasGroup.blocksRaycasts = false; var tempSlot = new GameObject("TempSlot", typeof(RectTransform), typeof(LayoutElement)); var tempRect = tempSlot.GetComponent(); tempRect.SetParent(targetContainer.cardHolder, false); tempRect.sizeDelta = _rectTransform.sizeDelta; if (siblingIndex >= 0 && siblingIndex <= targetContainer.cardHolder.childCount) tempRect.SetSiblingIndex(siblingIndex); LayoutRebuilder.ForceRebuildLayoutImmediate(targetContainer.cardHolder); var targetWorldPos = tempRect.position; transform.SetParent(_rootCanvas.transform, true); _scaleMotion = LMotion.Create(_rectTransform.localScale, Vector3.one, duration) .WithEase(Ease.OutQuad) .BindToLocalScale(_rectTransform); _moveMotion = LMotion.Create(_rectTransform.position, targetWorldPos, duration) .WithEase(Ease.OutCubic) .BindToPosition(_rectTransform); await _moveMotion.ToUniTask(ct); var finalIndex = tempRect.GetSiblingIndex(); Destroy(tempSlot); transform.SetParent(targetContainer.cardHolder, false); transform.SetSiblingIndex(finalIndex); _rectTransform.localPosition = Vector3.zero; _rectTransform.localScale = Vector3.one; CurrentContainer = targetContainer; _canvasGroup.blocksRaycasts = true; IsInteractable = true; } public void OnPointerEnter(PointerEventData eventData) { if (!IsInteractable || eventData.dragging) return; CancelMotion(ref _scaleMotion); _scaleMotion = LMotion.Create(_rectTransform.localScale, Vector3.one * 1.08f, 0.15f) .WithEase(Ease.OutQuad) .BindToLocalScale(_rectTransform); } public void OnPointerExit(PointerEventData eventData) { if (!IsInteractable || eventData.dragging) return; CancelMotion(ref _scaleMotion); _scaleMotion = LMotion.Create(_rectTransform.localScale, Vector3.one, 0.15f) .WithEase(Ease.OutQuad) .BindToLocalScale(_rectTransform); } public void OnBeginDrag(PointerEventData eventData) { if (!IsInteractable || !GameManager.Instance.CanStartDrag(this)) { eventData.pointerDrag = null; return; } CancelMotion(ref _moveMotion); CancelMotion(ref _scaleMotion); SoundManager.Instance?.PlayDraw(); _originContainer = CurrentContainer; _originContainer.RemoveCardListReference(this); _placeholder = new GameObject("Placeholder", typeof(RectTransform), typeof(LayoutElement)); var rt = _placeholder.GetComponent(); rt.SetParent(_originContainer.cardHolder, false); rt.sizeDelta = _rectTransform.sizeDelta; rt.SetSiblingIndex(transform.GetSiblingIndex()); var le = _placeholder.GetComponent(); le.preferredWidth = _rectTransform.rect.width; le.preferredHeight = _rectTransform.rect.height; transform.SetParent(_rootCanvas.transform, true); _canvasGroup.blocksRaycasts = false; _scaleMotion = LMotion.Create(_rectTransform.localScale, Vector3.one * 1.15f, 0.15f) .WithEase(Ease.OutBack) .BindToLocalScale(_rectTransform); } public void OnDrag(PointerEventData eventData) { if (!IsInteractable) return; if (RectTransformUtility.ScreenPointToWorldPointInRectangle( _rootCanvas.GetComponent(), eventData.position, eventData.pressEventCamera, out var globalMousePos)) { _rectTransform.position = globalMousePos; } if (_originContainer != null && _originContainer.type == ContainerType.Hand && _placeholder != null) { UpdatePlaceholderPositionInHand(); } } private void UpdatePlaceholderPositionInHand() { Transform holder = _originContainer.cardHolder; var newSiblingIndex = holder.childCount; for (var i = 0; i < holder.childCount; i++) { var child = holder.GetChild(i); if (child == _placeholder.transform) continue; if (_rectTransform.position.x < child.position.x) { newSiblingIndex = i; if (_placeholder.transform.GetSiblingIndex() < newSiblingIndex) newSiblingIndex--; break; } } _placeholder.transform.SetSiblingIndex(newSiblingIndex); } public void OnEndDrag(PointerEventData eventData) { if (_placeholder == null) return; var returnIndex = _placeholder.transform.GetSiblingIndex(); Destroy(_placeholder); CardContainer targetContainer = null; if (eventData.pointerEnter != null) { targetContainer = eventData.pointerEnter.GetComponentInParent(); } GameManager.Instance.HandleCardDropAsync(this, _originContainer, targetContainer, returnIndex).Forget(); } public void OnPointerClick(PointerEventData eventData) { if (eventData.dragging || CurrentContainer == null) return; switch (CurrentContainer.type) { case ContainerType.Deck: GameManager.Instance.DrawCardByClickAsync().Forget(); break; case ContainerType.Hand: GameManager.Instance.PlayCardFromHandByClickAsync(this).Forget(); break; case ContainerType.Discard: GameManager.Instance.RecycleDiscardByClickAsync().Forget(); break; } } private void CancelMotion(ref MotionHandle handle) { if (handle.IsActive()) handle.Cancel(); } private void OnDestroy() { CancelMotion(ref _moveMotion); CancelMotion(ref _scaleMotion); CancelMotion(ref _rotMotion); } }