487 lines
18 KiB
C#
487 lines
18 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// Manages the player's card collection, booster packs, and related operations.
|
|
/// Uses a singleton pattern for global access.
|
|
/// </summary>
|
|
public class CardSystemManager : MonoBehaviour
|
|
{
|
|
private static CardSystemManager _instance;
|
|
private static bool _isQuitting = false;
|
|
public static CardSystemManager Instance => _instance;
|
|
|
|
[Header("Card Collection")]
|
|
[SerializeField] private List<CardDefinition> availableCards = new List<CardDefinition>();
|
|
|
|
// Runtime data - will be serialized for save/load
|
|
[SerializeField] private CardInventory playerInventory = new CardInventory();
|
|
|
|
// Dictionary to quickly look up card definitions by ID
|
|
private Dictionary<string, CardDefinition> _definitionLookup = new Dictionary<string, CardDefinition>();
|
|
|
|
// Event callbacks using System.Action
|
|
public event Action<List<CardData>> OnBoosterOpened;
|
|
public event Action<CardData> OnCardCollected;
|
|
public event Action<CardData> OnCardRarityUpgraded;
|
|
public event Action<int> OnBoosterCountChanged;
|
|
|
|
// Keep a reference to unsubscribe safely
|
|
private Action<string> _onSaveDataLoadedHandler;
|
|
|
|
private void Awake()
|
|
{
|
|
_instance = this;
|
|
|
|
// Register for post-boot initialization
|
|
BootCompletionService.RegisterInitAction(InitializePostBoot);
|
|
}
|
|
|
|
private void InitializePostBoot()
|
|
{
|
|
// Load card definitions from Addressables
|
|
LoadCardDefinitionsFromAddressables();
|
|
|
|
Logging.Debug("[CardSystemManager] Post-boot initialization complete");
|
|
}
|
|
|
|
private void OnApplicationQuit()
|
|
{
|
|
_isQuitting = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads all card definitions from Addressables using the "BlokkemonCard" label
|
|
/// </summary>
|
|
private async void LoadCardDefinitionsFromAddressables()
|
|
{
|
|
availableCards = new List<CardDefinition>();
|
|
// 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<string>();
|
|
foreach (var loc in locations)
|
|
{
|
|
var cardHandle = UnityEngine.AddressableAssets.Addressables.LoadAssetAsync<CardDefinition>(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();
|
|
|
|
// Apply saved state when save data is available
|
|
if (SaveLoadManager.Instance != null)
|
|
{
|
|
if (SaveLoadManager.Instance.IsSaveDataLoaded)
|
|
{
|
|
ApplySavedCardStateIfAvailable();
|
|
}
|
|
else
|
|
{
|
|
SaveLoadManager.Instance.OnLoadCompleted += OnSaveDataLoadedHandler;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
if (SaveLoadManager.Instance != null)
|
|
{
|
|
SaveLoadManager.Instance.OnLoadCompleted -= OnSaveDataLoadedHandler;
|
|
}
|
|
}
|
|
|
|
// Apply saved state if present in the SaveLoadManager
|
|
private void ApplySavedCardStateIfAvailable()
|
|
{
|
|
var data = SaveLoadManager.Instance?.currentSaveData;
|
|
if (data?.cardCollection != null)
|
|
{
|
|
ApplyCardCollectionState(data.cardCollection);
|
|
Logging.Debug("[CardSystemManager] Applied saved card collection state after loading definitions");
|
|
}
|
|
}
|
|
|
|
// Event handler for when save data load completes
|
|
private void OnSaveDataLoadedHandler(string slot)
|
|
{
|
|
ApplySavedCardStateIfAvailable();
|
|
if (SaveLoadManager.Instance != null)
|
|
{
|
|
SaveLoadManager.Instance.OnLoadCompleted -= OnSaveDataLoadedHandler;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds a lookup dictionary for quick access to card definitions by ID
|
|
/// </summary>
|
|
private void BuildDefinitionLookup()
|
|
{
|
|
_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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a booster pack to the player's inventory
|
|
/// </summary>
|
|
public void AddBoosterPack(int count = 1)
|
|
{
|
|
playerInventory.BoosterPackCount += count;
|
|
OnBoosterCountChanged?.Invoke(playerInventory.BoosterPackCount);
|
|
Logging.Debug($"[CardSystemManager] Added {count} booster pack(s). Total: {playerInventory.BoosterPackCount}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Opens a booster pack and returns the newly obtained cards
|
|
/// </summary>
|
|
public List<CardData> OpenBoosterPack()
|
|
{
|
|
if (playerInventory.BoosterPackCount <= 0)
|
|
{
|
|
Logging.Warning("[CardSystemManager] Attempted to open a booster pack, but none are available.");
|
|
return new List<CardData>();
|
|
}
|
|
|
|
playerInventory.BoosterPackCount--;
|
|
OnBoosterCountChanged?.Invoke(playerInventory.BoosterPackCount);
|
|
|
|
// Draw 3 cards based on rarity distribution
|
|
List<CardData> drawnCards = DrawRandomCards(3);
|
|
|
|
// Add cards to the inventory
|
|
foreach (var card in drawnCards)
|
|
{
|
|
AddCardToInventory(card);
|
|
}
|
|
|
|
// Notify listeners
|
|
OnBoosterOpened?.Invoke(drawnCards);
|
|
|
|
Logging.Debug($"[CardSystemManager] Opened a booster pack and obtained {drawnCards.Count} cards. Remaining boosters: {playerInventory.BoosterPackCount}");
|
|
return drawnCards;
|
|
}
|
|
|
|
/// <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))
|
|
{
|
|
CardData existingCard = playerInventory.GetCard(card.DefinitionId);
|
|
existingCard.CopiesOwned++;
|
|
|
|
// Check if the card can be upgraded
|
|
if (existingCard.TryUpgradeRarity())
|
|
{
|
|
OnCardRarityUpgraded?.Invoke(existingCard);
|
|
}
|
|
|
|
Logging.Debug($"[CardSystemManager] Added duplicate card '{card.Name}'. Now have {existingCard.CopiesOwned} copies.");
|
|
}
|
|
else
|
|
{
|
|
// Add new card
|
|
playerInventory.AddCard(card);
|
|
OnCardCollected?.Invoke(card);
|
|
|
|
Logging.Debug($"[CardSystemManager] Added new card '{card.Name}' to collection.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draws random cards based on rarity distribution
|
|
/// </summary>
|
|
private List<CardData> DrawRandomCards(int count)
|
|
{
|
|
List<CardData> result = new List<CardData>();
|
|
|
|
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<CardDefinition> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines a random card rarity with appropriate weighting
|
|
/// </summary>
|
|
private CardRarity DetermineRandomRarity()
|
|
{
|
|
// Simple weighted random - can be adjusted for better distribution
|
|
float rand = UnityEngine.Random.value;
|
|
|
|
if (rand < 0.6f) return CardRarity.Common;
|
|
if (rand < 0.85f) return CardRarity.Uncommon;
|
|
if (rand < 0.95f) return CardRarity.Rare;
|
|
if (rand < 0.99f) return CardRarity.Epic;
|
|
return CardRarity.Legendary;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns all cards from the player's collection
|
|
/// </summary>
|
|
public List<CardData> GetAllCollectedCards()
|
|
{
|
|
return playerInventory.GetAllCards();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns cards from a specific zone
|
|
/// </summary>
|
|
public List<CardData> GetCardsByZone(CardZone zone)
|
|
{
|
|
return playerInventory.GetCardsByZone(zone);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns cards of a specific rarity
|
|
/// </summary>
|
|
public List<CardData> GetCardsByRarity(CardRarity rarity)
|
|
{
|
|
return playerInventory.GetCardsByRarity(rarity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the number of booster packs the player has
|
|
/// </summary>
|
|
public int GetBoosterPackCount()
|
|
{
|
|
return playerInventory.BoosterPackCount;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns whether a specific card definition has been collected
|
|
/// </summary>
|
|
public bool IsCardCollected(string definitionId)
|
|
{
|
|
return playerInventory.HasCard(definitionId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets total unique card count
|
|
/// </summary>
|
|
public int GetUniqueCardCount()
|
|
{
|
|
return playerInventory.GetUniqueCardCount();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets completion percentage for a specific zone (0-100)
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns all available card definitions in the system
|
|
/// </summary>
|
|
public List<CardDefinition> GetAllCardDefinitions()
|
|
{
|
|
return new List<CardDefinition>(availableCards);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns direct access to the player's card inventory
|
|
/// For advanced operations and testing
|
|
/// </summary>
|
|
public CardInventory GetCardInventory()
|
|
{
|
|
return playerInventory;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns cards filtered by both zone and rarity
|
|
/// </summary>
|
|
public List<CardData> GetCardsByZoneAndRarity(CardZone zone, CardRarity rarity)
|
|
{
|
|
List<CardData> zoneCards = GetCardsByZone(zone);
|
|
return zoneCards.FindAll(c => c.Rarity == rarity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the count of cards by rarity
|
|
/// </summary>
|
|
public int GetCardCountByRarity(CardRarity rarity)
|
|
{
|
|
return playerInventory.GetCardsByRarity(rarity).Count;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the count of cards by zone
|
|
/// </summary>
|
|
public int GetCardCountByZone(CardZone zone)
|
|
{
|
|
return playerInventory.GetCardsByZone(zone).Count;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the total number of card definitions available in the system
|
|
/// </summary>
|
|
public int GetTotalCardDefinitionsCount()
|
|
{
|
|
return availableCards.Count;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the total collection completion percentage (0-100)
|
|
/// </summary>
|
|
public float GetTotalCompletionPercentage()
|
|
{
|
|
if (availableCards.Count == 0) return 0;
|
|
return (float)GetUniqueCardCount() / availableCards.Count * 100f;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets total completion percentage for a specific rarity (0-100)
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Export current card collection to a serializable snapshot
|
|
/// </summary>
|
|
public CardCollectionState ExportCardCollectionState()
|
|
{
|
|
var state = new CardCollectionState
|
|
{
|
|
boosterPackCount = playerInventory.BoosterPackCount,
|
|
cards = new List<SavedCardEntry>()
|
|
};
|
|
|
|
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
|
|
});
|
|
}
|
|
return state;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Apply a previously saved snapshot to the runtime inventory
|
|
/// </summary>
|
|
public void ApplyCardCollectionState(CardCollectionState state)
|
|
{
|
|
if (state == null) return;
|
|
|
|
playerInventory.ClearAllCards();
|
|
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}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|