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();