diff --git a/Assets/Editor/CardSystem/CardSystemLivePreview.cs b/Assets/Editor/CardSystem/CardSystemLivePreview.cs new file mode 100644 index 00000000..d1ecebde --- /dev/null +++ b/Assets/Editor/CardSystem/CardSystemLivePreview.cs @@ -0,0 +1,264 @@ +#if UNITY_EDITOR +using UnityEditor; +using UnityEngine; +using Data.CardSystem; +using AppleHills.Data.CardSystem; +using System.Collections.Generic; +using System.Linq; + +namespace AppleHills.Editor +{ + /// + /// Live preview window for the Card System. Shows real-time collection status. + /// + public class CardSystemLivePreview : EditorWindow + { + private Vector2 scrollPosition; + private bool isSubscribed = false; + private double lastUpdateTime = 0; + private const double UPDATE_INTERVAL = 1.0; // Poll every 1 second as backup + + // Cache for display + private Dictionary> cardsByRarity = new Dictionary>(); + private int totalCards = 0; + private int totalUniqueCards = 0; + private int boosterCount = 0; + private string lastEventMessage = ""; + + [MenuItem("Tools/Card System/Live Collection Preview")] + public static void ShowWindow() + { + CardSystemLivePreview window = GetWindow("Card Collection Live"); + window.minSize = new Vector2(400, 300); + window.Show(); + } + + private void OnEnable() + { + EditorApplication.playModeStateChanged += OnPlayModeStateChanged; + + if (Application.isPlaying) + { + SubscribeToEvents(); + RefreshData(); + } + } + + private void OnDisable() + { + EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; + UnsubscribeFromEvents(); + } + + private void OnPlayModeStateChanged(PlayModeStateChange state) + { + if (state == PlayModeStateChange.EnteredPlayMode) + { + SubscribeToEvents(); + RefreshData(); + } + else if (state == PlayModeStateChange.ExitingPlayMode) + { + UnsubscribeFromEvents(); + } + } + + private void SubscribeToEvents() + { + if (isSubscribed || !Application.isPlaying) return; + + if (CardSystemManager.Instance != null) + { + CardSystemManager.Instance.OnBoosterOpened += OnBoosterOpened; + CardSystemManager.Instance.OnCardCollected += OnCardCollected; + CardSystemManager.Instance.OnCardRarityUpgraded += OnCardRarityUpgraded; + CardSystemManager.Instance.OnBoosterCountChanged += OnBoosterCountChanged; + + isSubscribed = true; + Debug.Log("[CardSystemLivePreview] Subscribed to CardSystemManager events"); + } + } + + private void UnsubscribeFromEvents() + { + if (!isSubscribed) return; + + if (CardSystemManager.Instance != null) + { + CardSystemManager.Instance.OnBoosterOpened -= OnBoosterOpened; + CardSystemManager.Instance.OnCardCollected -= OnCardCollected; + CardSystemManager.Instance.OnCardRarityUpgraded -= OnCardRarityUpgraded; + CardSystemManager.Instance.OnBoosterCountChanged -= OnBoosterCountChanged; + } + + isSubscribed = false; + } + + // Event Handlers + private void OnBoosterOpened(List cards) + { + lastEventMessage = $"Booster opened! Drew {cards.Count} cards"; + RefreshData(); + Repaint(); + } + + private void OnCardCollected(CardData card) + { + lastEventMessage = $"New card collected: {card.Name} ({card.Rarity})"; + RefreshData(); + Repaint(); + } + + private void OnCardRarityUpgraded(CardData card) + { + lastEventMessage = $"Card upgraded: {card.Name} → {card.Rarity}!"; + RefreshData(); + Repaint(); + } + + private void OnBoosterCountChanged(int count) + { + boosterCount = count; + lastEventMessage = $"Booster count changed: {count}"; + Repaint(); + } + + private void RefreshData() + { + if (!Application.isPlaying || CardSystemManager.Instance == null) return; + + var allCards = CardSystemManager.Instance.GetAllCollectedCards(); + + // Group by rarity + cardsByRarity.Clear(); + cardsByRarity[CardRarity.Normal] = allCards.Where(c => c.Rarity == CardRarity.Normal).ToList(); + cardsByRarity[CardRarity.Rare] = allCards.Where(c => c.Rarity == CardRarity.Rare).ToList(); + cardsByRarity[CardRarity.Legendary] = allCards.Where(c => c.Rarity == CardRarity.Legendary).ToList(); + + totalCards = allCards.Sum(c => c.CopiesOwned); + totalUniqueCards = allCards.Count; + boosterCount = CardSystemManager.Instance.GetBoosterPackCount(); + } + + private void Update() + { + if (!Application.isPlaying) return; + + // Poll every second as backup (in case events are missed) + if (EditorApplication.timeSinceStartup - lastUpdateTime > UPDATE_INTERVAL) + { + lastUpdateTime = EditorApplication.timeSinceStartup; + + if (!isSubscribed) + { + SubscribeToEvents(); + } + + RefreshData(); + Repaint(); + } + } + + private void OnGUI() + { + if (!Application.isPlaying) + { + EditorGUILayout.HelpBox("Enter Play Mode to view live collection data.", MessageType.Info); + return; + } + + if (CardSystemManager.Instance == null) + { + EditorGUILayout.HelpBox("CardSystemManager instance not found!", MessageType.Warning); + return; + } + + // Header + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + GUILayout.Label("Live Collection Preview", EditorStyles.boldLabel); + EditorGUILayout.Space(); + + // Summary Stats + EditorGUILayout.LabelField("Total Unique Cards:", totalUniqueCards.ToString()); + EditorGUILayout.LabelField("Total Cards Owned:", totalCards.ToString()); + EditorGUILayout.LabelField("Booster Packs:", boosterCount.ToString()); + + if (!string.IsNullOrEmpty(lastEventMessage)) + { + EditorGUILayout.Space(); + EditorGUILayout.HelpBox(lastEventMessage, MessageType.Info); + } + + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(); + + // Collection by Rarity + scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); + + DrawRaritySection(CardRarity.Legendary, Color.yellow); + DrawRaritySection(CardRarity.Rare, new Color(0.5f, 0.5f, 1f)); // Light blue + DrawRaritySection(CardRarity.Normal, Color.white); + + EditorGUILayout.EndScrollView(); + + EditorGUILayout.Space(); + + // Actions + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Refresh Now")) + { + RefreshData(); + } + if (GUILayout.Button("Clear Event Log")) + { + lastEventMessage = ""; + } + EditorGUILayout.EndHorizontal(); + } + + private void DrawRaritySection(CardRarity rarity, Color color) + { + if (!cardsByRarity.ContainsKey(rarity) || cardsByRarity[rarity].Count == 0) + return; + + var cards = cardsByRarity[rarity]; + int totalCopies = cards.Sum(c => c.CopiesOwned); + + EditorGUILayout.Space(); + + // Header + var oldColor = GUI.backgroundColor; + GUI.backgroundColor = color; + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + GUI.backgroundColor = oldColor; + + GUILayout.Label($"{rarity} ({cards.Count} unique, {totalCopies} total)", EditorStyles.boldLabel); + + // Cards + foreach (var card in cards.OrderBy(c => c.Name)) + { + EditorGUILayout.BeginHorizontal(); + + EditorGUILayout.LabelField(card.Name, GUILayout.Width(200)); + EditorGUILayout.LabelField($"x{card.CopiesOwned}", GUILayout.Width(50)); + EditorGUILayout.LabelField(card.Zone.ToString(), GUILayout.Width(100)); + + // Progress to next tier (if not Legendary) + if (rarity < CardRarity.Legendary) + { + int copiesNeeded = 5; + float progress = Mathf.Clamp01((float)card.CopiesOwned / copiesNeeded); + Rect progressRect = GUILayoutUtility.GetRect(100, 18); + EditorGUI.ProgressBar(progressRect, progress, $"{card.CopiesOwned}/{copiesNeeded}"); + } + + EditorGUILayout.EndHorizontal(); + } + + EditorGUILayout.EndVertical(); + } + } +} +#endif + diff --git a/Assets/Editor/CardSystem/CardSystemLivePreview.cs.meta b/Assets/Editor/CardSystem/CardSystemLivePreview.cs.meta new file mode 100644 index 00000000..aea5867b --- /dev/null +++ b/Assets/Editor/CardSystem/CardSystemLivePreview.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8c45a4455c1440069cd9c1ee815f32e0 +timeCreated: 1762464117 \ No newline at end of file diff --git a/Assets/Prefabs/UI/CardsSystem/Cards/FlippableCardPrefab.prefab b/Assets/Prefabs/UI/CardsSystem/Cards/FlippableCardPrefab.prefab index 4cb88074..6748bcf2 100644 --- a/Assets/Prefabs/UI/CardsSystem/Cards/FlippableCardPrefab.prefab +++ b/Assets/Prefabs/UI/CardsSystem/Cards/FlippableCardPrefab.prefab @@ -1022,6 +1022,12 @@ MonoBehaviour: hoverScaleMultiplier: 1.05 flipDuration: 0.6 flipScalePunch: 1.3 + newCardText: {fileID: 3802662965106921097} + newCardIdleText: {fileID: 8335972675955266088} + repeatText: {fileID: 7979281912729275558} + progressBarContainer: {fileID: 1938654216571238436} + cardsToUpgrade: 5 + enlargedScale: 1.5 --- !u!1001 &8620731150807558660 PrefabInstance: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/Data/CardSystem/CardInventory.cs b/Assets/Scripts/Data/CardSystem/CardInventory.cs index 34037780..cb11f1ea 100644 --- a/Assets/Scripts/Data/CardSystem/CardInventory.cs +++ b/Assets/Scripts/Data/CardSystem/CardInventory.cs @@ -11,9 +11,17 @@ namespace AppleHills.Data.CardSystem [Serializable] public class CardInventory { - // Dictionary of collected cards indexed by definition ID + // Dictionary of collected cards indexed by definition ID + rarity (e.g., "Pikachu_Normal", "Pikachu_Rare") [SerializeField] private Dictionary collectedCards = new Dictionary(); + /// + /// Generate a unique key for a card based on definition ID and rarity + /// + private string GetCardKey(string definitionId, CardRarity rarity) + { + return $"{definitionId}_{rarity}"; + } + // Number of unopened booster packs the player has [SerializeField] private int boosterPackCount; @@ -96,7 +104,9 @@ namespace AppleHills.Data.CardSystem { if (card == null) return; - if (collectedCards.TryGetValue(card.DefinitionId, out CardData existingCard)) + string key = GetCardKey(card.DefinitionId, card.Rarity); + + if (collectedCards.TryGetValue(key, out CardData existingCard)) { // Increase copies of existing card existingCard.CopiesOwned++; @@ -105,7 +115,7 @@ namespace AppleHills.Data.CardSystem { // Add new card to collection var newCard = new CardData(card); - collectedCards[card.DefinitionId] = newCard; + collectedCards[key] = newCard; // Add to lookup dictionaries cardsByZone[newCard.Zone].Add(newCard); @@ -133,19 +143,21 @@ namespace AppleHills.Data.CardSystem } /// - /// Get a specific card from the collection by definition ID + /// Get a specific card from the collection by definition ID and rarity /// - public CardData GetCard(string definitionId) + public CardData GetCard(string definitionId, CardRarity rarity) { - return collectedCards.TryGetValue(definitionId, out CardData card) ? card : null; + string key = GetCardKey(definitionId, rarity); + return collectedCards.TryGetValue(key, out CardData card) ? card : null; } /// - /// Check if the player has a specific card + /// Check if the player has a specific card at a specific rarity /// - public bool HasCard(string definitionId) + public bool HasCard(string definitionId, CardRarity rarity) { - return collectedCards.ContainsKey(definitionId); + string key = GetCardKey(definitionId, rarity); + return collectedCards.ContainsKey(key); } /// diff --git a/Assets/Scripts/Data/CardSystem/CardSystemManager.cs b/Assets/Scripts/Data/CardSystem/CardSystemManager.cs index 8e0ddf54..a98c6adc 100644 --- a/Assets/Scripts/Data/CardSystem/CardSystemManager.cs +++ b/Assets/Scripts/Data/CardSystem/CardSystemManager.cs @@ -1,14 +1,10 @@ using System; using System.Collections.Generic; -using System.Linq; using AppleHills.Data.CardSystem; using Bootstrap; using Core; using Core.SaveLoad; using UnityEngine; -#if UNITY_EDITOR -using UnityEditor; -#endif namespace Data.CardSystem { @@ -150,6 +146,7 @@ namespace Data.CardSystem /// /// Opens a booster pack and returns the newly obtained cards + /// NOTE: Cards are NOT added to inventory immediately - they're added after the reveal interaction /// public List OpenBoosterPack() { @@ -165,11 +162,9 @@ namespace Data.CardSystem // Draw 3 cards based on rarity distribution List drawnCards = DrawRandomCards(3); - // Add cards to the inventory - foreach (var card in drawnCards) - { - AddCardToInventory(card); - } + // NOTE: Cards are NOT added to inventory here anymore + // They will be added after the player interacts with each revealed card + // This allows us to show new/repeat status before adding to collection // Notify listeners OnBoosterOpened?.Invoke(drawnCards); @@ -177,33 +172,56 @@ namespace Data.CardSystem Logging.Debug($"[CardSystemManager] Opened a booster pack and obtained {drawnCards.Count} cards. Remaining boosters: {playerInventory.BoosterPackCount}"); return drawnCards; } + + /// + /// Check if a card is new to the player's collection at the specified rarity + /// + /// The card to check + /// Out parameter - the existing card if found, null otherwise + /// True if this is a new card at this rarity, false if already owned + public bool IsCardNew(CardData cardData, out CardData existingCard) + { + if (playerInventory.HasCard(cardData.DefinitionId, cardData.Rarity)) + { + existingCard = playerInventory.GetCard(cardData.DefinitionId, cardData.Rarity); + return false; + } + existingCard = null; + return true; + } + + /// + /// Adds a card to the player's inventory after reveal (delayed add) + /// Public wrapper for AddCardToInventory to support delayed inventory updates + /// + public void AddCardToInventoryDelayed(CardData card) + { + AddCardToInventory(card); + } /// /// Adds a card to the player's inventory, handles duplicates /// private void AddCardToInventory(CardData card) { - // Check if the player already has this card type (definition) - if (playerInventory.HasCard(card.DefinitionId)) + // Check if the player already has this card at this rarity + if (playerInventory.HasCard(card.DefinitionId, card.Rarity)) { - CardData existingCard = playerInventory.GetCard(card.DefinitionId); + CardData existingCard = playerInventory.GetCard(card.DefinitionId, card.Rarity); existingCard.CopiesOwned++; - // Check if the card can be upgraded - if (existingCard.TryUpgradeRarity()) - { - OnCardRarityUpgraded?.Invoke(existingCard); - } + // Note: Upgrades are now handled separately in BoosterOpeningPage + // We don't auto-upgrade here anymore - Logging.Debug($"[CardSystemManager] Added duplicate card '{card.Name}'. Now have {existingCard.CopiesOwned} copies."); + Logging.Debug($"[CardSystemManager] Added duplicate card '{card.Name}' ({card.Rarity}). Now have {existingCard.CopiesOwned} copies."); } else { - // Add new card + // Add new card at this rarity playerInventory.AddCard(card); OnCardCollected?.Invoke(card); - Logging.Debug($"[CardSystemManager] Added new card '{card.Name}' to collection."); + Logging.Debug($"[CardSystemManager] Added new card '{card.Name}' ({card.Rarity}) to collection."); } } @@ -300,11 +318,17 @@ namespace Data.CardSystem } /// - /// Returns whether a specific card definition has been collected + /// Returns whether a specific card definition has been collected (at any rarity) /// public bool IsCardCollected(string definitionId) { - return playerInventory.HasCard(definitionId); + // Check if the card exists at any rarity + foreach (CardRarity rarity in System.Enum.GetValues(typeof(CardRarity))) + { + if (playerInventory.HasCard(definitionId, rarity)) + return true; + } + return false; } /// diff --git a/Assets/Scripts/UI/CardSystem/BoosterOpeningPage.cs b/Assets/Scripts/UI/CardSystem/BoosterOpeningPage.cs index ef0323cb..3afdc3f4 100644 --- a/Assets/Scripts/UI/CardSystem/BoosterOpeningPage.cs +++ b/Assets/Scripts/UI/CardSystem/BoosterOpeningPage.cs @@ -10,7 +10,6 @@ using UI.DragAndDrop.Core; using Unity.Cinemachine; using UnityEngine; using UnityEngine.UI; -using UnityEngine.UI; namespace UI.CardSystem { @@ -39,6 +38,7 @@ namespace UI.CardSystem [SerializeField] private float boosterDisappearDuration = 0.5f; [SerializeField] private CinemachineImpulseSource impulseSource; [SerializeField] private ParticleSystem openingParticleSystem; + [SerializeField] private Transform albumIcon; // Target for card fly-away animation private int _availableBoosterCount; private BoosterPackDraggable _currentBoosterInCenter; @@ -46,9 +46,10 @@ namespace UI.CardSystem private List _currentRevealedCards = new List(); private CardData[] _currentCardData; private int _revealedCardCount; + private int _cardsCompletedInteraction; // Track how many cards finished their new/repeat interaction private bool _isProcessingOpening; private const int MAX_VISIBLE_BOOSTERS = 3; - + private FlippableCard _currentActiveCard; // The card currently awaiting interaction private void Awake() { // Make sure we have a CanvasGroup for transitions @@ -499,6 +500,7 @@ namespace UI.CardSystem _currentRevealedCards.Clear(); _revealedCardCount = 0; + _cardsCompletedInteraction = 0; // Reset interaction count // Calculate positions float totalWidth = (count - 1) * cardSpacing; @@ -521,9 +523,18 @@ namespace UI.CardSystem // Setup the card data (stored but not revealed yet) flippableCard.SetupCard(_currentCardData[i]); - // Subscribe to reveal event to track when flipped + // Subscribe to flip started event (to disable other cards IMMEDIATELY) int cardIndex = i; // Capture for closure + flippableCard.OnFlipStarted += OnCardFlipStarted; + + // Subscribe to reveal event to track when flipped flippableCard.OnCardRevealed += (card, data) => OnCardRevealed(cardIndex); + + // Subscribe to inactive click event (for jiggle effect) + flippableCard.OnClickedWhileInactive += OnCardClickedWhileInactive; + + // Initially, all cards are clickable (for flipping) + flippableCard.SetClickable(true); } else { @@ -538,6 +549,24 @@ namespace UI.CardSystem } } + /// + /// Handle when a card flip starts (disable all other cards IMMEDIATELY) + /// + private void OnCardFlipStarted(FlippableCard flippingCard) + { + Debug.Log($"[BoosterOpeningPage] Card flip started, disabling all other cards."); + + // Disable ALL cards immediately to prevent multi-flip + foreach (GameObject cardObj in _currentRevealedCards) + { + FlippableCard card = cardObj.GetComponent(); + if (card != null) + { + card.SetClickable(false); + } + } + } + /// /// Handle card reveal (when flipped) /// @@ -545,29 +574,182 @@ namespace UI.CardSystem { Debug.Log($"[BoosterOpeningPage] Card {cardIndex} revealed!"); _revealedCardCount++; + + // Get the flippable card and card data + FlippableCard flippableCard = _currentRevealedCards[cardIndex].GetComponent(); + if (flippableCard == null) + { + Debug.LogWarning($"[BoosterOpeningPage] FlippableCard not found for card {cardIndex}!"); + return; + } + + CardData cardData = flippableCard.CardData; + + // Check if this is a new card using CardSystemManager + bool isNew = Data.CardSystem.CardSystemManager.Instance.IsCardNew(cardData, out CardData existingCard); + + if (isNew) + { + Debug.Log($"[BoosterOpeningPage] Card '{cardData.Name}' is NEW!"); + flippableCard.ShowAsNew(); + } + else + { + // Check if card is already Legendary - if so, skip progress bar and auto-progress + if (existingCard.Rarity == AppleHills.Data.CardSystem.CardRarity.Legendary) + { + Debug.Log($"[BoosterOpeningPage] Card '{cardData.Name}' is LEGENDARY - auto-progressing!"); + // Add to inventory immediately and move to next card + Data.CardSystem.CardSystemManager.Instance.AddCardToInventoryDelayed(cardData); + _cardsCompletedInteraction++; + _revealedCardCount++; // This was already incremented earlier, but we need to track completion + EnableUnrevealedCards(); + return; // Skip showing the card enlarged + } + + int ownedCount = existingCard.CopiesOwned; + Debug.Log($"[BoosterOpeningPage] Card '{cardData.Name}' is a REPEAT! Owned: {ownedCount}"); + + // Check if this card will trigger an upgrade (ownedCount + 1 >= threshold) + bool willUpgrade = (ownedCount + 1) >= flippableCard.CardsToUpgrade && existingCard.Rarity < AppleHills.Data.CardSystem.CardRarity.Legendary; + + if (willUpgrade) + { + Debug.Log($"[BoosterOpeningPage] This card will trigger upgrade! ({ownedCount + 1}/{flippableCard.CardsToUpgrade})"); + // Show as repeat - progress bar will fill and auto-trigger upgrade + flippableCard.ShowAsRepeatWithUpgrade(ownedCount, existingCard); + } + else + { + // Normal repeat, no upgrade + flippableCard.ShowAsRepeat(ownedCount); + } + } + + // Set this card as the active one (only this card is clickable now) + SetActiveCard(flippableCard); + + // Subscribe to tap event to know when interaction is complete + flippableCard.OnCardTappedAfterReveal += (card) => OnCardCompletedInteraction(card, cardIndex); } /// - /// Wait until all cards are revealed + /// Handle when a card's interaction is complete (tapped after reveal) + /// + private void OnCardCompletedInteraction(FlippableCard card, int cardIndex) + { + Debug.Log($"[BoosterOpeningPage] Card {cardIndex} interaction complete!"); + + // Add card to inventory NOW (after player saw it) + Data.CardSystem.CardSystemManager.Instance.AddCardToInventoryDelayed(card.CardData); + + // Return card to normal size + card.ReturnToNormalSize(); + + // Increment completed interaction count + _cardsCompletedInteraction++; + + // Clear active card + _currentActiveCard = null; + + // Re-enable all unrevealed cards (they can be flipped now) + EnableUnrevealedCards(); + + Debug.Log($"[BoosterOpeningPage] Cards completed interaction: {_cardsCompletedInteraction}/{_currentCardData.Length}"); + } + + /// + /// Set which card is currently active (only this card can be clicked) + /// + private void SetActiveCard(FlippableCard activeCard) + { + _currentActiveCard = activeCard; + + // Disable all other cards + foreach (GameObject cardObj in _currentRevealedCards) + { + FlippableCard card = cardObj.GetComponent(); + if (card != null) + { + // Only the active card is clickable + card.SetClickable(card == activeCard); + } + } + + Debug.Log($"[BoosterOpeningPage] Set active card. Only one card is now clickable."); + } + + /// + /// Re-enable all unrevealed cards (allow them to be flipped) + /// + private void EnableUnrevealedCards() + { + foreach (GameObject cardObj in _currentRevealedCards) + { + FlippableCard card = cardObj.GetComponent(); + if (card != null && !card.IsFlipped) + { + card.SetClickable(true); + } + } + + Debug.Log($"[BoosterOpeningPage] Re-enabled unrevealed cards for flipping."); + } + + /// + /// Handle when a card is clicked while not active (jiggle the active card) + /// + private void OnCardClickedWhileInactive(FlippableCard inactiveCard) + { + Debug.Log($"[BoosterOpeningPage] Inactive card clicked, jiggling active card."); + + if (_currentActiveCard != null) + { + _currentActiveCard.Jiggle(); + } + } + + /// + /// Wait until all cards are revealed AND all interactions are complete /// private IEnumerator WaitForCardReveals() { + // Wait until all cards are flipped while (_revealedCardCount < _currentCardData.Length) { yield return null; } - // All cards revealed, wait a moment - yield return new WaitForSeconds(1f); + Debug.Log($"[BoosterOpeningPage] All cards revealed! Waiting for interactions..."); - // Clear cards - foreach (GameObject card in _currentRevealedCards) + // Wait until all cards have completed their new/repeat interaction + while (_cardsCompletedInteraction < _currentCardData.Length) { - if (card != null) + yield return null; + } + + Debug.Log($"[BoosterOpeningPage] All interactions complete! Animating cards to album..."); + + // All cards revealed and interacted with, wait a moment + yield return new WaitForSeconds(0.5f); + + // Animate cards to album icon (or center if no icon assigned) with staggered delays + Vector3 targetPosition = albumIcon != null ? albumIcon.position : Vector3.zero; + + int cardIndex = 0; + foreach (GameObject cardObj in _currentRevealedCards) + { + if (cardObj != null) { - // Animate out - Tween.LocalScale(card.transform, Vector3.zero, 0.3f, 0f, Tween.EaseInBack, - completeCallback: () => Destroy(card)); + // Stagger each card with 0.5s delay + float delay = cardIndex * 0.5f; + + // Animate to album icon position, then destroy + Tween.Position(cardObj.transform, targetPosition, 0.5f, delay, Tween.EaseInBack); + Tween.LocalScale(cardObj.transform, Vector3.zero, 0.5f, delay, Tween.EaseInBack, + completeCallback: () => Destroy(cardObj)); + + cardIndex++; } } diff --git a/Assets/Scripts/UI/CardSystem/FlippableCard.cs b/Assets/Scripts/UI/CardSystem/FlippableCard.cs index 3dd2aa87..3f18cd12 100644 --- a/Assets/Scripts/UI/CardSystem/FlippableCard.cs +++ b/Assets/Scripts/UI/CardSystem/FlippableCard.cs @@ -29,6 +29,14 @@ namespace UI.CardSystem [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; @@ -36,12 +44,20 @@ namespace UI.CardSystem 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() { @@ -51,11 +67,29 @@ namespace UI.CardSystem cardDisplay = cardFrontObject.GetComponent(); } - // Start with back showing, front hidden + // 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() @@ -98,40 +132,53 @@ namespace UI.CardSystem _isFlipping = true; + // Fire flip started event IMMEDIATELY (before animations) + OnFlipStarted?.Invoke(this); + // Stop idle hover StopIdleHover(); - // Flip animation: rotate Y 0 -> 90 (hide back) -> 180 (show front) - Transform cardTransform = transform; + // 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: Flip to 90 degrees (edge view, hide back) - Tween.LocalRotation(cardTransform, Quaternion.Euler(0, 90, 0), flipDuration * 0.5f, 0f, Tween.EaseInOut, - completeCallback: () => - { - // Switch visuals at the edge - if (cardBackObject != null) - cardBackObject.SetActive(false); - if (cardFrontObject != null) - cardFrontObject.SetActive(true); - - // Phase 2: Flip from 90 to 180 (show front) - Tween.LocalRotation(cardTransform, Quaternion.Euler(0, 180, 0), flipDuration * 0.5f, 0f, Tween.EaseInOut, - completeCallback: () => - { - _isFlipped = true; - _isFlipping = false; - - // Fire revealed event - OnCardRevealed?.Invoke(this, _cardData); - }); - }); + // 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 = cardTransform.localScale; - Tween.LocalScale(cardTransform, originalScale * flipScalePunch, flipDuration * 0.5f, 0f, Tween.EaseOutBack, + Vector3 originalScale = transform.localScale; + Tween.LocalScale(transform, originalScale * flipScalePunch, flipDuration * 0.5f, 0f, Tween.EaseOutBack, completeCallback: () => { - Tween.LocalScale(cardTransform, originalScale, flipDuration * 0.5f, 0f, Tween.EaseInBack); + Tween.LocalScale(transform, originalScale, flipDuration * 0.5f, 0f, Tween.EaseInBack); }); } @@ -207,6 +254,21 @@ namespace UI.CardSystem public void OnPointerClick(PointerEventData eventData) { + // If not clickable, notify and return + if (!_isClickable) + { + OnClickedWhileInactive?.Invoke(this); + return; + } + + // If waiting for tap after reveal, handle that + if (_isWaitingForTap) + { + OnCardTappedAfterReveal?.Invoke(this); + _isWaitingForTap = false; + return; + } + if (_isFlipped || _isFlipping) return; @@ -216,6 +278,352 @@ namespace UI.CardSystem #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); + }); + }); + } + + #endregion + private void OnDestroy() { StopIdleHover();