using System; using AppleHills.Data.CardSystem; using Pixelplacement; using Pixelplacement.TweenSystem; using UnityEngine; using UnityEngine.EventSystems; namespace UI.CardSystem { /// /// Flippable card wrapper that shows a card back, then flips to reveal the CardDisplay front. /// This component nests an existing CardDisplay prefab to reuse card visuals everywhere. /// public class FlippableCard : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler { [Header("Card References")] [SerializeField] private GameObject cardBackObject; // The card back visual [SerializeField] private GameObject cardFrontObject; // Your CardDisplay prefab instance [SerializeField] private CardDisplay cardDisplay; // Reference to CardDisplay component [SerializeField] private AlbumCard albumCard; // Reference to nested AlbumCard (for album placement flow) [Header("Idle Hover Animation")] [SerializeField] private bool enableIdleHover = true; [SerializeField] private float idleHoverHeight = 10f; [SerializeField] private float idleHoverDuration = 1.5f; [SerializeField] private float hoverScaleMultiplier = 1.05f; [Header("Flip Animation")] [SerializeField] private float flipDuration = 0.6f; [SerializeField] private float flipScalePunch = 1.1f; [Header("New/Repeat Card Display")] [SerializeField] private GameObject newCardText; [SerializeField] private GameObject newCardIdleText; [SerializeField] private GameObject repeatText; [SerializeField] private GameObject progressBarContainer; [SerializeField] private int cardsToUpgrade = 5; [SerializeField] private float enlargedScale = 1.5f; // State private bool _isFlipped = false; private bool _isFlipping = false; private bool _isHovering = false; private TweenBase _idleHoverTween; private CardData _cardData; private Vector2 _originalPosition; // Track original spawn position private bool _isWaitingForTap = false; // Waiting for tap after reveal private bool _isNew = false; // Is this a new card private int _ownedCount = 0; // Owned count for repeat cards private bool _isClickable = true; // Can this card be clicked // Events public event Action OnCardRevealed; public event Action OnCardTappedAfterReveal; public event Action OnClickedWhileInactive; // Fired when clicked but not clickable public event Action OnFlipStarted; // Fired when flip animation begins public bool IsFlipped => _isFlipped; public CardData CardData => _cardData; public int CardsToUpgrade => cardsToUpgrade; // Expose upgrade threshold private void Awake() { // Auto-find CardDisplay if not assigned if (cardDisplay == null && cardFrontObject != null) { cardDisplay = cardFrontObject.GetComponent(); } // Auto-find AlbumCard if not assigned if (albumCard == null) { albumCard = GetComponentInChildren(); } // Card back: starts at 0° rotation (normal, facing camera, clickable) // Card front: starts at 180° rotation (flipped away, will rotate to 0° when revealed) if (cardBackObject != null) { cardBackObject.transform.localRotation = Quaternion.Euler(0, 0, 0); cardBackObject.SetActive(true); } if (cardFrontObject != null) { cardFrontObject.transform.localRotation = Quaternion.Euler(0, 180, 0); cardFrontObject.SetActive(false); } // Hide all new/repeat UI elements initially if (newCardText != null) newCardText.SetActive(false); if (newCardIdleText != null) newCardIdleText.SetActive(false); if (repeatText != null) repeatText.SetActive(false); if (progressBarContainer != null) progressBarContainer.SetActive(false); } private void Start() { // Save the original position so we can return to it after hover RectTransform rectTransform = GetComponent(); if (rectTransform != null) { _originalPosition = rectTransform.anchoredPosition; } // Start idle hover animation if (enableIdleHover && !_isFlipped) { StartIdleHover(); } } /// /// Setup the card data (stores it but doesn't reveal until flipped) /// public void SetupCard(CardData data) { _cardData = data; // Setup the CardDisplay but keep it hidden if (cardDisplay != null) { cardDisplay.SetupCard(data); } } /// /// Flip the card to reveal the front /// public void FlipToReveal() { if (_isFlipped || _isFlipping) return; _isFlipping = true; // Fire flip started event IMMEDIATELY (before animations) OnFlipStarted?.Invoke(this); // Stop idle hover StopIdleHover(); // Flip animation: Rotate the visual children (back from 0→90, front from 180→0) // ...existing code... // Card back: 0° → 90° (rotates away) // Card front: 180° → 90° → 0° (rotates into view) // Phase 1: Rotate both to 90 degrees (edge view) if (cardBackObject != null) { Tween.LocalRotation(cardBackObject.transform, Quaternion.Euler(0, 90, 0), flipDuration * 0.5f, 0f, Tween.EaseInOut); } if (cardFrontObject != null) { Tween.LocalRotation(cardFrontObject.transform, Quaternion.Euler(0, 90, 0), flipDuration * 0.5f, 0f, Tween.EaseInOut, completeCallback: () => { // At edge (90°), switch visibility if (cardBackObject != null) cardBackObject.SetActive(false); if (cardFrontObject != null) cardFrontObject.SetActive(true); // Phase 2: Rotate front from 90 to 0 (show at correct orientation) Tween.LocalRotation(cardFrontObject.transform, Quaternion.Euler(0, 0, 0), flipDuration * 0.5f, 0f, Tween.EaseInOut, completeCallback: () => { _isFlipped = true; _isFlipping = false; // Fire revealed event OnCardRevealed?.Invoke(this, _cardData); }); }); } // Scale punch during flip for extra juice Vector3 originalScale = transform.localScale; Tween.LocalScale(transform, originalScale * flipScalePunch, flipDuration * 0.5f, 0f, Tween.EaseOutBack, completeCallback: () => { Tween.LocalScale(transform, originalScale, flipDuration * 0.5f, 0f, Tween.EaseInBack); }); } /// /// Start idle hover animation (gentle bobbing) /// private void StartIdleHover() { if (_idleHoverTween != null) return; RectTransform rectTransform = GetComponent(); if (rectTransform == null) return; Vector2 originalPos = rectTransform.anchoredPosition; Vector2 targetPos = originalPos + Vector2.up * idleHoverHeight; _idleHoverTween = Tween.Value(0f, 1f, (val) => { if (rectTransform != null) { float t = Mathf.Sin(val * Mathf.PI * 2f) * 0.5f + 0.5f; // Smooth sine wave rectTransform.anchoredPosition = Vector2.Lerp(originalPos, targetPos, t); } }, idleHoverDuration, 0f, Tween.EaseInOut, Tween.LoopType.Loop); } /// /// Stop idle hover animation /// private void StopIdleHover() { if (_idleHoverTween != null) { _idleHoverTween.Stop(); _idleHoverTween = null; // Reset to ORIGINAL position (not Vector2.zero!) RectTransform rectTransform = GetComponent(); if (rectTransform != null) { Tween.AnchoredPosition(rectTransform, _originalPosition, 0.3f, 0f, Tween.EaseOutBack); } } } #region Pointer Event Handlers public void OnPointerEnter(PointerEventData eventData) { if (_isFlipped || _isFlipping) return; _isHovering = true; // Scale up slightly on hover Tween.LocalScale(transform, Vector3.one * hoverScaleMultiplier, 0.2f, 0f, Tween.EaseOutBack); } public void OnPointerExit(PointerEventData eventData) { if (_isFlipped || _isFlipping) return; _isHovering = false; // Scale back to normal Tween.LocalScale(transform, Vector3.one, 0.2f, 0f, Tween.EaseOutBack); } public void OnPointerClick(PointerEventData eventData) { Debug.Log($"[CLICK-TRACE-FLIPPABLE] OnPointerClick on {name}, _isClickable={_isClickable}, _isWaitingForTap={_isWaitingForTap}, _isFlipped={_isFlipped}, position={eventData.position}"); // If not clickable, notify and return if (!_isClickable) { Debug.Log($"[CLICK-TRACE-FLIPPABLE] {name} - Not clickable, firing OnClickedWhileInactive"); OnClickedWhileInactive?.Invoke(this); return; } // If waiting for tap after reveal, handle that if (_isWaitingForTap) { Debug.Log($"[CLICK-TRACE-FLIPPABLE] {name} - Waiting for tap, dismissing enlarged state"); OnCardTappedAfterReveal?.Invoke(this); _isWaitingForTap = false; return; } if (_isFlipped || _isFlipping) { Debug.Log($"[CLICK-TRACE-FLIPPABLE] {name} - Ignoring click (flipped={_isFlipped}, flipping={_isFlipping})"); return; } Debug.Log($"[CLICK-TRACE-FLIPPABLE] {name} - Processing click, starting flip"); // Flip on click FlipToReveal(); } #endregion #region New/Repeat Card Display /// /// Show this card as a new card (enlarge, show "NEW CARD" text, wait for tap) /// public void ShowAsNew() { _isNew = true; _isWaitingForTap = true; // Show new card text if (newCardText != null) newCardText.SetActive(true); // Enlarge the card EnlargeCard(); } /// /// Show this card as a repeat that will trigger an upgrade (enlarge, show progress, auto-transition to upgrade) /// /// Number of copies owned BEFORE this one /// The existing card data at lower rarity (for upgrade reference) public void ShowAsRepeatWithUpgrade(int ownedCount, AppleHills.Data.CardSystem.CardData lowerRarityCard) { _isNew = false; _ownedCount = ownedCount; _isWaitingForTap = false; // Don't wait yet - upgrade will happen automatically // Show repeat text if (repeatText != null) repeatText.SetActive(true); // Enlarge the card EnlargeCard(); // Show progress bar with owned count, then auto-trigger upgrade ShowProgressBar(ownedCount, () => { // Progress animation complete - trigger upgrade! TriggerUpgradeTransition(lowerRarityCard); }); } /// /// Trigger the upgrade transition (called after progress bar fills) /// private void TriggerUpgradeTransition(AppleHills.Data.CardSystem.CardData lowerRarityCard) { Debug.Log($"[FlippableCard] Triggering upgrade transition from {lowerRarityCard.Rarity}!"); AppleHills.Data.CardSystem.CardRarity oldRarity = lowerRarityCard.Rarity; AppleHills.Data.CardSystem.CardRarity newRarity = oldRarity + 1; // Reset the lower rarity count to 0 lowerRarityCard.CopiesOwned = 0; // Create upgraded card data AppleHills.Data.CardSystem.CardData upgradedCardData = new AppleHills.Data.CardSystem.CardData(_cardData); upgradedCardData.Rarity = newRarity; upgradedCardData.CopiesOwned = 1; // Check if we already have this card at the higher rarity bool isNewAtHigherRarity = Data.CardSystem.CardSystemManager.Instance.IsCardNew(upgradedCardData, out AppleHills.Data.CardSystem.CardData existingHigherRarity); // Add the higher rarity card to inventory Data.CardSystem.CardSystemManager.Instance.GetCardInventory().AddCard(upgradedCardData); // Update our displayed card data _cardData.Rarity = newRarity; // Transition to appropriate display if (isNewAtHigherRarity || newRarity == AppleHills.Data.CardSystem.CardRarity.Legendary) { // Show as NEW at higher rarity TransitionToNewCardView(newRarity); } else { // Show progress for higher rarity, then transition to NEW int ownedAtHigherRarity = existingHigherRarity.CopiesOwned; ShowProgressBar(ownedAtHigherRarity, () => { TransitionToNewCardView(newRarity); }); } } /// /// Show this card as a repeat (enlarge, show progress bar, wait for tap) /// /// Number of copies owned BEFORE this one public void ShowAsRepeat(int ownedCount) { _isNew = false; _ownedCount = ownedCount; _isWaitingForTap = true; // Show repeat text if (repeatText != null) repeatText.SetActive(true); // Enlarge the card EnlargeCard(); // Show progress bar with owned count, then blink new element ShowProgressBar(ownedCount, () => { // Progress animation complete }); } /// /// Show this card as upgraded (hide progress bar, show as new with upgraded rarity) /// public void ShowAsUpgraded(AppleHills.Data.CardSystem.CardRarity oldRarity, AppleHills.Data.CardSystem.CardRarity newRarity) { _isNew = true; _isWaitingForTap = true; // Update the CardDisplay to show new rarity if (cardDisplay != null && _cardData != null) { _cardData.Rarity = newRarity; cardDisplay.SetupCard(_cardData); } // Hide progress bar and repeat text if (progressBarContainer != null) progressBarContainer.SetActive(false); if (repeatText != null) repeatText.SetActive(false); // Show new card text (it's now a "new" card at the higher rarity) if (newCardText != null) newCardText.SetActive(true); Debug.Log($"[FlippableCard] Card upgraded from {oldRarity} to {newRarity}! Showing as NEW."); // Card is already enlarged from the repeat display, so no need to enlarge again } /// /// Show this card as upgraded with progress bar (already have copies at higher rarity) /// public void ShowAsUpgradedWithProgress(AppleHills.Data.CardSystem.CardRarity oldRarity, AppleHills.Data.CardSystem.CardRarity newRarity, int ownedAtNewRarity) { _isNew = false; _isWaitingForTap = false; // Don't wait for tap yet, progress bar will complete first // Hide new card text if (newCardText != null) newCardText.SetActive(false); // Show repeat text (it's a repeat at the new rarity) if (repeatText != null) repeatText.SetActive(true); // Show progress bar for the new rarity ShowProgressBar(ownedAtNewRarity, () => { // Progress animation complete - now transition to "NEW CARD" view TransitionToNewCardView(newRarity); }); Debug.Log($"[FlippableCard] Card upgraded from {oldRarity} to {newRarity}! Showing progress {ownedAtNewRarity}/5"); } /// /// Transition to "NEW CARD" view after upgrade progress completes /// private void TransitionToNewCardView(AppleHills.Data.CardSystem.CardRarity newRarity) { Debug.Log($"[FlippableCard] Transitioning to NEW CARD view at {newRarity} rarity"); // Update the CardDisplay to show new rarity if (cardDisplay != null && _cardData != null) { _cardData.Rarity = newRarity; cardDisplay.SetupCard(_cardData); } // Hide progress bar and repeat text if (progressBarContainer != null) progressBarContainer.SetActive(false); if (repeatText != null) repeatText.SetActive(false); // Show "NEW CARD" text if (newCardText != null) newCardText.SetActive(true); // Now wait for tap _isNew = true; _isWaitingForTap = true; Debug.Log($"[FlippableCard] Now showing as NEW CARD at {newRarity}, waiting for tap"); } /// /// Enlarge the card /// private void EnlargeCard() { Tween.LocalScale(transform, Vector3.one * enlargedScale, 0.3f, 0f, Tween.EaseOutBack); } /// /// Return card to normal size /// public void ReturnToNormalSize() { Tween.LocalScale(transform, Vector3.one, 0.3f, 0f, Tween.EaseOutBack, completeCallback: () => { // After returning to normal, hide new card text, show idle text if (_isNew) { if (newCardText != null) newCardText.SetActive(false); if (newCardIdleText != null) newCardIdleText.SetActive(true); } // Keep repeat text visible }); } /// /// Show progress bar with owned count, then blink the new element /// private void ShowProgressBar(int ownedCount, System.Action onComplete) { if (progressBarContainer == null) { onComplete?.Invoke(); return; } progressBarContainer.SetActive(true); // Get all child Image components UnityEngine.UI.Image[] progressElements = progressBarContainer.GetComponentsInChildren(true); // Check if we have the required number of elements (should match cardsToUpgrade) if (progressElements.Length < cardsToUpgrade) { Debug.LogWarning($"[FlippableCard] Not enough Image components in progress bar! Expected {cardsToUpgrade}, found {progressElements.Length}"); onComplete?.Invoke(); return; } // Disable all elements first foreach (var img in progressElements) { img.enabled = false; } // Show owned count (from the END, going backwards) // E.g., if owned 3 cards, enable elements at index [4], [3], [2] (last 3 elements) int startIndex = Mathf.Max(0, cardsToUpgrade - ownedCount); for (int i = startIndex; i < cardsToUpgrade && i < progressElements.Length; i++) { progressElements[i].enabled = true; } // Wait a moment, then blink the new element // New element is at index (cardsToUpgrade - ownedCount - 1) int newElementIndex = Mathf.Max(0, cardsToUpgrade - ownedCount - 1); if (newElementIndex >= 0 && newElementIndex < progressElements.Length) { Tween.Value(0f, 1f, (val) => { }, 0.3f, 0f, completeCallback: () => { BlinkProgressElement(newElementIndex, progressElements, onComplete); }); } else { onComplete?.Invoke(); } } /// /// Blink a progress element (enable/disable rapidly) /// private void BlinkProgressElement(int index, UnityEngine.UI.Image[] elements, System.Action onComplete) { if (index < 0 || index >= elements.Length) { onComplete?.Invoke(); return; } UnityEngine.UI.Image element = elements[index]; int blinkCount = 0; const int maxBlinks = 3; void Blink() { element.enabled = !element.enabled; blinkCount++; if (blinkCount >= maxBlinks * 2) { element.enabled = true; // End on enabled onComplete?.Invoke(); } else { Tween.Value(0f, 1f, (val) => { }, 0.15f, 0f, completeCallback: Blink); } } Blink(); } /// /// Enable or disable clickability of this card /// public void SetClickable(bool clickable) { _isClickable = clickable; } /// /// Jiggle the card (shake animation) /// public void Jiggle() { // Quick shake animation - rotate left, then right, then center Transform cardTransform = transform; Quaternion originalRotation = cardTransform.localRotation; // Shake sequence: 0 -> -5 -> +5 -> 0 Tween.LocalRotation(cardTransform, Quaternion.Euler(0, 0, -5), 0.05f, 0f, Tween.EaseInOut, completeCallback: () => { Tween.LocalRotation(cardTransform, Quaternion.Euler(0, 0, 5), 0.1f, 0f, Tween.EaseInOut, completeCallback: () => { Tween.LocalRotation(cardTransform, originalRotation, 0.05f, 0f, Tween.EaseInOut); }); }); } /// /// Extract the nested AlbumCard and reparent it to a new parent /// Used when placing card in album slot - extracts the AlbumCard from this wrapper /// The caller is responsible for tweening it to the final position /// /// The transform to reparent the AlbumCard to (typically the AlbumCardSlot) /// The extracted AlbumCard component, or null if not found public AlbumCard ExtractAlbumCard(Transform newParent) { if (albumCard == null) { Debug.LogWarning("[FlippableCard] Cannot extract AlbumCard - none found!"); return null; } // Reparent AlbumCard to new parent (maintain world position temporarily) // The caller will tween it to the final position albumCard.transform.SetParent(newParent, true); // Setup the card data on the AlbumCard if (_cardData != null) { albumCard.SetupCard(_cardData); } Debug.Log($"[FlippableCard] Extracted AlbumCard '{_cardData?.Name}' to {newParent.name} - ready for tween"); return albumCard; } #endregion private void OnDestroy() { StopIdleHover(); } } }