Semi-working rarity upgrades

This commit is contained in:
Michal Pikulski
2025-11-06 23:06:41 +01:00
parent d23c000347
commit a705b3a829
7 changed files with 969 additions and 70 deletions

View File

@@ -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
{
/// <summary>
/// Live preview window for the Card System. Shows real-time collection status.
/// </summary>
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<CardRarity, List<CardData>> cardsByRarity = new Dictionary<CardRarity, List<CardData>>();
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<CardSystemLivePreview>("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<CardData> 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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8c45a4455c1440069cd9c1ee815f32e0
timeCreated: 1762464117

View File

@@ -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

View File

@@ -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<string, CardData> collectedCards = new Dictionary<string, CardData>();
/// <summary>
/// Generate a unique key for a card based on definition ID and rarity
/// </summary>
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
}
/// <summary>
/// Get a specific card from the collection by definition ID
/// Get a specific card from the collection by definition ID and rarity
/// </summary>
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;
}
/// <summary>
/// Check if the player has a specific card
/// Check if the player has a specific card at a specific rarity
/// </summary>
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);
}
/// <summary>

View File

@@ -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
/// <summary>
/// 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
/// </summary>
public List<CardData> OpenBoosterPack()
{
@@ -165,11 +162,9 @@ namespace Data.CardSystem
// Draw 3 cards based on rarity distribution
List<CardData> 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;
}
/// <summary>
/// Check if a card is new to the player's collection at the specified rarity
/// </summary>
/// <param name="cardData">The card to check</param>
/// <param name="existingCard">Out parameter - the existing card if found, null otherwise</param>
/// <returns>True if this is a new card at this rarity, false if already owned</returns>
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;
}
/// <summary>
/// Adds a card to the player's inventory after reveal (delayed add)
/// Public wrapper for AddCardToInventory to support delayed inventory updates
/// </summary>
public void AddCardToInventoryDelayed(CardData card)
{
AddCardToInventory(card);
}
/// <summary>
/// Adds a card to the player's inventory, handles duplicates
/// </summary>
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
}
/// <summary>
/// Returns whether a specific card definition has been collected
/// Returns whether a specific card definition has been collected (at any rarity)
/// </summary>
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;
}
/// <summary>

View File

@@ -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<GameObject> _currentRevealedCards = new List<GameObject>();
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
}
}
/// <summary>
/// Handle when a card flip starts (disable all other cards IMMEDIATELY)
/// </summary>
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<FlippableCard>();
if (card != null)
{
card.SetClickable(false);
}
}
}
/// <summary>
/// Handle card reveal (when flipped)
/// </summary>
@@ -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<FlippableCard>();
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);
}
/// <summary>
/// Wait until all cards are revealed
/// Handle when a card's interaction is complete (tapped after reveal)
/// </summary>
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}");
}
/// <summary>
/// Set which card is currently active (only this card can be clicked)
/// </summary>
private void SetActiveCard(FlippableCard activeCard)
{
_currentActiveCard = activeCard;
// Disable all other cards
foreach (GameObject cardObj in _currentRevealedCards)
{
FlippableCard card = cardObj.GetComponent<FlippableCard>();
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.");
}
/// <summary>
/// Re-enable all unrevealed cards (allow them to be flipped)
/// </summary>
private void EnableUnrevealedCards()
{
foreach (GameObject cardObj in _currentRevealedCards)
{
FlippableCard card = cardObj.GetComponent<FlippableCard>();
if (card != null && !card.IsFlipped)
{
card.SetClickable(true);
}
}
Debug.Log($"[BoosterOpeningPage] Re-enabled unrevealed cards for flipping.");
}
/// <summary>
/// Handle when a card is clicked while not active (jiggle the active card)
/// </summary>
private void OnCardClickedWhileInactive(FlippableCard inactiveCard)
{
Debug.Log($"[BoosterOpeningPage] Inactive card clicked, jiggling active card.");
if (_currentActiveCard != null)
{
_currentActiveCard.Jiggle();
}
}
/// <summary>
/// Wait until all cards are revealed AND all interactions are complete
/// </summary>
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++;
}
}

View File

@@ -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<FlippableCard, CardData> OnCardRevealed;
public event Action<FlippableCard> OnCardTappedAfterReveal;
public event Action<FlippableCard> OnClickedWhileInactive; // Fired when clicked but not clickable
public event Action<FlippableCard> 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<CardDisplay>();
}
// 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
/// <summary>
/// Show this card as a new card (enlarge, show "NEW CARD" text, wait for tap)
/// </summary>
public void ShowAsNew()
{
_isNew = true;
_isWaitingForTap = true;
// Show new card text
if (newCardText != null)
newCardText.SetActive(true);
// Enlarge the card
EnlargeCard();
}
/// <summary>
/// Show this card as a repeat that will trigger an upgrade (enlarge, show progress, auto-transition to upgrade)
/// </summary>
/// <param name="ownedCount">Number of copies owned BEFORE this one</param>
/// <param name="lowerRarityCard">The existing card data at lower rarity (for upgrade reference)</param>
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);
});
}
/// <summary>
/// Trigger the upgrade transition (called after progress bar fills)
/// </summary>
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);
});
}
}
/// <summary>
/// Show this card as a repeat (enlarge, show progress bar, wait for tap)
/// </summary>
/// <param name="ownedCount">Number of copies owned BEFORE this one</param>
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
});
}
/// <summary>
/// Show this card as upgraded (hide progress bar, show as new with upgraded rarity)
/// </summary>
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
}
/// <summary>
/// Show this card as upgraded with progress bar (already have copies at higher rarity)
/// </summary>
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");
}
/// <summary>
/// Transition to "NEW CARD" view after upgrade progress completes
/// </summary>
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");
}
/// <summary>
/// Enlarge the card
/// </summary>
private void EnlargeCard()
{
Tween.LocalScale(transform, Vector3.one * enlargedScale, 0.3f, 0f, Tween.EaseOutBack);
}
/// <summary>
/// Return card to normal size
/// </summary>
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
});
}
/// <summary>
/// Show progress bar with owned count, then blink the new element
/// </summary>
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<UnityEngine.UI.Image>(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();
}
}
/// <summary>
/// Blink a progress element (enable/disable rapidly)
/// </summary>
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();
}
/// <summary>
/// Enable or disable clickability of this card
/// </summary>
public void SetClickable(bool clickable)
{
_isClickable = clickable;
}
/// <summary>
/// Jiggle the card (shake animation)
/// </summary>
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();