using System; using System.Collections.Generic; using System.Linq; using AppleHills.Data.CardSystem; using Core; using Core.Lifecycle; using UnityEngine; namespace Data.CardSystem { /// /// Manages the player's card collection, booster packs, and related operations. /// Manages the card collection system for the game. /// Handles unlocking cards, tracking collections, and integrating with the save/load system. /// public class CardSystemManager : ManagedBehaviour { private static CardSystemManager _instance; public static CardSystemManager Instance => _instance; // Save system configuration public override bool AutoRegisterForSave => true; public override string SaveId => "CardSystemManager"; [Header("Card Collection")] [SerializeField] private List availableCards = new List(); // Runtime data - will be serialized for save/load [SerializeField] private CardInventory playerInventory = new CardInventory(); // Album system - cards waiting to be placed in album private List _pendingRevealCards = new List(); private HashSet _placedInAlbumCardIds = new HashSet(); // Dictionary to quickly look up card definitions by ID private Dictionary _definitionLookup; private bool _lookupInitialized = false; // Event callbacks using System.Action public event Action> OnBoosterOpened; public event Action OnCardCollected; public event Action OnCardRarityUpgraded; public event Action OnBoosterCountChanged; public event Action OnPendingCardAdded; public event Action OnCardPlacedInAlbum; public override int ManagedAwakePriority => 60; // Data systems internal override void OnManagedAwake() { // Set instance immediately (early initialization) _instance = this; // Load card definitions from Addressables, then register with save system LoadCardDefinitionsFromAddressables(); } internal override void OnManagedStart() { Logging.Debug("[CardSystemManager] Initialized"); } /// /// Loads all card definitions from Addressables using the "BlokkemonCard" label /// private async void LoadCardDefinitionsFromAddressables() { availableCards = new List(); // Load by label instead of group name for better flexibility var handle = UnityEngine.AddressableAssets.Addressables.LoadResourceLocationsAsync(new string[] { "BlokkemonCard" }, UnityEngine.AddressableAssets.Addressables.MergeMode.Union); await handle.Task; if (handle.Status == UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationStatus.Succeeded) { var locations = handle.Result; var loadedIds = new HashSet(); foreach (var loc in locations) { var cardHandle = UnityEngine.AddressableAssets.Addressables.LoadAssetAsync(loc); await cardHandle.Task; if (cardHandle.Status == UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationStatus.Succeeded) { var cardDef = cardHandle.Result; if (cardDef != null && !string.IsNullOrEmpty(cardDef.Id) && !loadedIds.Contains(cardDef.Id)) { availableCards.Add(cardDef); loadedIds.Add(cardDef.Id); } } } // Build lookup now that cards are loaded BuildDefinitionLookup(); Logging.Debug($"[CardSystemManager] Loaded {availableCards.Count} card definitions from Addressables"); } else { Logging.Warning("[CardSystemManager] Failed to load card definitions from Addressables"); } } /// /// Builds a lookup dictionary for quick access to card definitions by ID /// private void BuildDefinitionLookup() { if (_definitionLookup == null) { _definitionLookup = new Dictionary(); } _definitionLookup.Clear(); foreach (var cardDef in availableCards) { if (cardDef != null && !string.IsNullOrEmpty(cardDef.Id)) { _definitionLookup[cardDef.Id] = cardDef; } } // Link existing card data to their definitions foreach (var cardData in playerInventory.CollectedCards.Values) { if (!string.IsNullOrEmpty(cardData.DefinitionId) && _definitionLookup.TryGetValue(cardData.DefinitionId, out CardDefinition def)) { cardData.SetDefinition(def); } } _lookupInitialized = true; } /// /// Adds a booster pack to the player's inventory /// public void AddBoosterPack(int count = 1) { playerInventory.BoosterPackCount += count; OnBoosterCountChanged?.Invoke(playerInventory.BoosterPackCount); Logging.Debug($"[CardSystemManager] Added {count} booster pack(s). Total: {playerInventory.BoosterPackCount}"); } /// /// 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() { if (playerInventory.BoosterPackCount <= 0) { Logging.Warning("[CardSystemManager] Attempted to open a booster pack, but none are available."); return new List(); } playerInventory.BoosterPackCount--; OnBoosterCountChanged?.Invoke(playerInventory.BoosterPackCount); // Draw 3 cards based on rarity distribution List drawnCards = DrawRandomCards(3); // 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); 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 /// Checks both owned inventory and pending reveal queue /// /// 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 or pending public bool IsCardNew(CardData cardData, out CardData existingCard) { // First check inventory (cards already placed in album) if (playerInventory.HasCard(cardData.DefinitionId, cardData.Rarity)) { existingCard = playerInventory.GetCard(cardData.DefinitionId, cardData.Rarity); return false; } // Then check pending reveal queue (cards waiting to be placed) CardData pendingCard = _pendingRevealCards.FirstOrDefault(c => c.DefinitionId == cardData.DefinitionId && c.Rarity == cardData.Rarity); if (pendingCard != null) { // Return the actual pending card with its real CopiesOwned count // Pending status is just about placement location, not copy count existingCard = pendingCard; return false; // Not new - already in pending queue } existingCard = null; return true; // Truly new - not in inventory or pending } /// /// 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 /// Checks both inventory and pending lists to find existing cards /// private void AddCardToInventory(CardData card) { // Guard: Ensure card has at least 1 copy if (card.CopiesOwned <= 0) { card.CopiesOwned = 1; Logging.Warning($"[CardSystemManager] Card '{card.Name}' had {card.CopiesOwned} copies, setting to 1"); } // First check inventory (cards already placed in album) if (playerInventory.HasCard(card.DefinitionId, card.Rarity)) { CardData existingCard = playerInventory.GetCard(card.DefinitionId, card.Rarity); existingCard.CopiesOwned++; Logging.Debug($"[CardSystemManager] Added duplicate card '{card.Name}' ({card.Rarity}) to INVENTORY. Now have {existingCard.CopiesOwned} copies."); return; } // Then check pending reveal queue CardData pendingCard = _pendingRevealCards.FirstOrDefault(c => c.DefinitionId == card.DefinitionId && c.Rarity == card.Rarity); if (pendingCard != null) { // Card already in pending - increment its copy count pendingCard.CopiesOwned++; Logging.Debug($"[CardSystemManager] Added duplicate card '{card.Name}' ({card.Rarity}) to PENDING. Now have {pendingCard.CopiesOwned} copies pending."); return; } // This is a NEW card (never owned at this rarity before) // Add to pending reveal list instead of inventory _pendingRevealCards.Add(card); OnPendingCardAdded?.Invoke(card); Logging.Debug($"[CardSystemManager] Added new card '{card.Name}' ({card.Rarity}) to pending reveal queue."); } /// /// Draws random cards based on rarity distribution /// private List DrawRandomCards(int count) { List result = new List(); if (availableCards.Count == 0) { Debug.LogError("[CardSystemManager] No available cards defined!"); return result; } // Simple weighted random selection based on rarity for (int i = 0; i < count; i++) { // Determine card rarity first CardRarity rarity = DetermineRandomRarity(); // Filter cards by the selected rarity List cardsOfRarity = availableCards.FindAll(c => c.Rarity == rarity); if (cardsOfRarity.Count > 0) { // Select a random card of this rarity int randomIndex = UnityEngine.Random.Range(0, cardsOfRarity.Count); CardDefinition selectedDef = cardsOfRarity[randomIndex]; // Create card data from definition CardData newCard = selectedDef.CreateCardData(); result.Add(newCard); } else { // Fallback if no cards of the selected rarity Logging.Warning($"[CardSystemManager] No cards of rarity {rarity} available, selecting a random card instead."); int randomIndex = UnityEngine.Random.Range(0, availableCards.Count); CardDefinition randomDef = availableCards[randomIndex]; CardData newCard = randomDef.CreateCardData(); result.Add(newCard); } } return result; } /// /// Determines a random card rarity with appropriate weighting /// private CardRarity DetermineRandomRarity() { // Weighted random for 3 rarities float rand = UnityEngine.Random.value; if (rand < 0.70f) return CardRarity.Normal; // 70% chance if (rand < 0.95f) return CardRarity.Rare; // 25% chance return CardRarity.Legendary; // 5% chance } /// /// Returns all cards from the player's collection (both owned and pending) /// public List GetAllCollectedCards() { List allCards = new List(playerInventory.GetAllCards()); allCards.AddRange(_pendingRevealCards); return allCards; } /// /// Returns cards from a specific zone (both owned and pending) /// public List GetCardsByZone(CardZone zone) { List zoneCards = new List(playerInventory.GetCardsByZone(zone)); zoneCards.AddRange(_pendingRevealCards.Where(c => c.Zone == zone)); return zoneCards; } /// /// Returns cards of a specific rarity (both owned and pending) /// public List GetCardsByRarity(CardRarity rarity) { List rarityCards = new List(playerInventory.GetCardsByRarity(rarity)); rarityCards.AddRange(_pendingRevealCards.Where(c => c.Rarity == rarity)); return rarityCards; } /// /// Returns the number of booster packs the player has /// public int GetBoosterPackCount() { return playerInventory.BoosterPackCount; } /// /// Returns whether a specific card definition has been collected (at any rarity, in inventory or pending) /// public bool IsCardCollected(string definitionId) { // Check inventory at any rarity foreach (CardRarity rarity in System.Enum.GetValues(typeof(CardRarity))) { if (playerInventory.HasCard(definitionId, rarity)) return true; } // Check pending reveal queue if (_pendingRevealCards.Any(c => c.DefinitionId == definitionId)) return true; return false; } /// /// Gets total unique card count (both owned and pending) /// public int GetUniqueCardCount() { int inventoryCount = playerInventory.GetUniqueCardCount(); // Count unique cards in pending that aren't already in inventory int pendingUniqueCount = _pendingRevealCards .Select(c => new { c.DefinitionId, c.Rarity }) .Distinct() .Count(pc => !playerInventory.HasCard(pc.DefinitionId, pc.Rarity)); return inventoryCount + pendingUniqueCount; } /// /// Gets completion percentage for a specific zone (0-100) /// public float GetZoneCompletionPercentage(CardZone zone) { // Count available cards in this zone int totalInZone = availableCards.FindAll(c => c.Zone == zone).Count; if (totalInZone == 0) return 0; // Count collected cards in this zone int collectedInZone = playerInventory.GetCardsByZone(zone).Count; return (float)collectedInZone / totalInZone * 100f; } /// /// Returns all available card definitions in the system /// public List GetAllCardDefinitions() { return new List(availableCards); } /// /// Returns direct access to the player's card inventory /// For advanced operations and testing /// public CardInventory GetCardInventory() { return playerInventory; } /// /// Returns cards filtered by both zone and rarity /// public List GetCardsByZoneAndRarity(CardZone zone, CardRarity rarity) { List zoneCards = GetCardsByZone(zone); return zoneCards.FindAll(c => c.Rarity == rarity); } /// /// Returns the count of cards by rarity (both owned and pending) /// public int GetCardCountByRarity(CardRarity rarity) { int inventoryCount = playerInventory.GetCardsByRarity(rarity).Count; int pendingCount = _pendingRevealCards.Count(c => c.Rarity == rarity); return inventoryCount + pendingCount; } /// /// Returns the count of cards by zone (both owned and pending) /// public int GetCardCountByZone(CardZone zone) { int inventoryCount = playerInventory.GetCardsByZone(zone).Count; int pendingCount = _pendingRevealCards.Count(c => c.Zone == zone); return inventoryCount + pendingCount; } /// /// Gets the total number of card definitions available in the system /// public int GetTotalCardDefinitionsCount() { return availableCards.Count; } /// /// Gets the total collection completion percentage (0-100) /// public float GetTotalCompletionPercentage() { if (availableCards.Count == 0) return 0; return (float)GetUniqueCardCount() / availableCards.Count * 100f; } /// /// Gets total completion percentage for a specific rarity (0-100) /// public float GetRarityCompletionPercentage(CardRarity rarity) { // Count available cards of this rarity int totalOfRarity = availableCards.FindAll(c => c.Rarity == rarity).Count; if (totalOfRarity == 0) return 0; // Count collected cards of this rarity int collectedOfRarity = playerInventory.GetCardsByRarity(rarity).Count; return (float)collectedOfRarity / totalOfRarity * 100f; } #region Album System /// /// Returns all cards waiting to be placed in the album /// public List GetPendingRevealCards() { return new List(_pendingRevealCards); } /// /// Get a card by definition ID and rarity from either inventory or pending /// Returns the actual card reference so changes persist /// /// Card definition ID /// Card rarity /// Out parameter - true if card is from pending, false if from inventory /// The card data if found, null otherwise public CardData GetCard(string definitionId, CardRarity rarity, out bool isFromPending) { // Check inventory first if (playerInventory.HasCard(definitionId, rarity)) { isFromPending = false; return playerInventory.GetCard(definitionId, rarity); } // Check pending CardData pendingCard = _pendingRevealCards.FirstOrDefault(c => c.DefinitionId == definitionId && c.Rarity == rarity); if (pendingCard != null) { isFromPending = true; return pendingCard; } isFromPending = false; return null; } /// /// Get a card by definition ID and rarity from either inventory or pending (simplified overload) /// public CardData GetCard(string definitionId, CardRarity rarity) { return GetCard(definitionId, rarity, out _); } /// /// Update a card's data in whichever list it's in (inventory or pending) /// Useful for incrementing CopiesOwned, upgrading rarity, etc. /// /// Card definition ID /// Card rarity /// Action to perform on the card /// True if card was found and updated, false otherwise public bool UpdateCard(string definitionId, CardRarity rarity, System.Action updateAction) { CardData card = GetCard(definitionId, rarity, out bool isFromPending); if (card != null) { updateAction?.Invoke(card); Logging.Debug($"[CardSystemManager] Updated card '{card.Name}' in {(isFromPending ? "pending" : "inventory")}"); return true; } Logging.Warning($"[CardSystemManager] Could not find card with ID '{definitionId}' and rarity '{rarity}' to update"); return false; } /// /// Marks a card as placed in the album /// Moves it from pending reveal to owned inventory /// public void MarkCardAsPlaced(CardData card) { if (_pendingRevealCards.Remove(card)) { // Add to owned inventory playerInventory.AddCard(card); // Track as placed _placedInAlbumCardIds.Add(card.Id); OnCardPlacedInAlbum?.Invoke(card); OnCardCollected?.Invoke(card); Logging.Debug($"[CardSystemManager] Card '{card.Name}' placed in album and added to inventory."); } else { Logging.Warning($"[CardSystemManager] Attempted to place card '{card.Name}' but it wasn't in pending reveal list."); } } /// /// Checks if a card has been placed in the album /// public bool IsCardPlacedInAlbum(string cardId) { return _placedInAlbumCardIds.Contains(cardId); } /// /// Gets count of cards waiting to be revealed /// public int GetPendingRevealCount() { return _pendingRevealCards.Count; } #endregion /// /// Export current card collection to a serializable snapshot /// public CardCollectionState ExportCardCollectionState() { var state = new CardCollectionState { boosterPackCount = playerInventory.BoosterPackCount, cards = new List(), pendingRevealCards = new List(), placedInAlbumCardIds = new List(_placedInAlbumCardIds) }; foreach (var card in playerInventory.CollectedCards.Values) { if (string.IsNullOrEmpty(card.DefinitionId)) continue; state.cards.Add(new SavedCardEntry { definitionId = card.DefinitionId, rarity = card.Rarity, copiesOwned = card.CopiesOwned }); } foreach (var card in _pendingRevealCards) { if (string.IsNullOrEmpty(card.DefinitionId)) continue; state.pendingRevealCards.Add(new SavedCardEntry { definitionId = card.DefinitionId, rarity = card.Rarity, copiesOwned = card.CopiesOwned }); } return state; } /// /// Apply a previously saved snapshot to the runtime inventory /// public async void ApplyCardCollectionState(CardCollectionState state) { if (state == null) return; // Wait for lookup to be initialized before loading while (!_lookupInitialized) { await System.Threading.Tasks.Task.Yield(); } playerInventory.ClearAllCards(); _pendingRevealCards.Clear(); _placedInAlbumCardIds.Clear(); playerInventory.BoosterPackCount = state.boosterPackCount; OnBoosterCountChanged?.Invoke(playerInventory.BoosterPackCount); foreach (var entry in state.cards) { if (string.IsNullOrEmpty(entry.definitionId)) continue; if (_definitionLookup.TryGetValue(entry.definitionId, out var def)) { // Create from definition to ensure links, then overwrite runtime fields var cd = def.CreateCardData(); cd.Rarity = entry.rarity; cd.CopiesOwned = entry.copiesOwned; playerInventory.AddCard(cd); } else { Logging.Warning($"[CardSystemManager] Saved card definition not found: {entry.definitionId}"); } } // Restore pending reveal cards if (state.pendingRevealCards != null) { foreach (var entry in state.pendingRevealCards) { if (string.IsNullOrEmpty(entry.definitionId)) continue; if (_definitionLookup.TryGetValue(entry.definitionId, out var def)) { var cd = def.CreateCardData(); cd.Rarity = entry.rarity; cd.CopiesOwned = entry.copiesOwned; _pendingRevealCards.Add(cd); } } } // Restore placed in album tracking if (state.placedInAlbumCardIds != null) { foreach (var cardId in state.placedInAlbumCardIds) { _placedInAlbumCardIds.Add(cardId); } } } /// /// Clears all card collection data - inventory, pending cards, boosters, and placement tracking /// Used for dev reset functionality /// public void ClearAllCollectionData() { playerInventory.ClearAllCards(); playerInventory.BoosterPackCount = 0; _pendingRevealCards.Clear(); _placedInAlbumCardIds.Clear(); OnBoosterCountChanged?.Invoke(0); Logging.Debug("[CardSystemManager] Cleared all collection data (inventory, boosters, pending, placement tracking)"); } #region Save/Load Lifecycle Hooks internal override string OnGlobalSaveRequested() { var state = ExportCardCollectionState(); return JsonUtility.ToJson(state); } internal override void OnGlobalRestoreRequested(string serializedData) { if (string.IsNullOrEmpty(serializedData)) { Logging.Debug("[CardSystemManager] No saved state to restore, using defaults"); return; } try { var state = JsonUtility.FromJson(serializedData); if (state != null) { ApplyCardCollectionState(state); Logging.Debug("[CardSystemManager] Successfully restored card collection state"); } else { Logging.Warning("[CardSystemManager] Failed to deserialize card collection state"); } } catch (Exception ex) { Logging.Warning($"[CardSystemManager] Exception while restoring card collection state: {ex}"); } } #endregion } }