488 lines
18 KiB
C#
488 lines
18 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using AppleHills.Data.CardSystem;
|
||
using Core;
|
||
using Core.Lifecycle;
|
||
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.
|
||
/// Manages the card collection system for the game.
|
||
/// Handles unlocking cards, tracking collections, and integrating with the save/load system.
|
||
/// </summary>
|
||
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<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;
|
||
|
||
public override int ManagedAwakePriority => 60; // Data systems
|
||
|
||
private new void Awake()
|
||
{
|
||
base.Awake(); // CRITICAL: Register with LifecycleManager!
|
||
|
||
// Set instance immediately so it's available before OnManagedAwake() is called
|
||
_instance = this;
|
||
}
|
||
|
||
protected override void OnManagedAwake()
|
||
{
|
||
// Load card definitions from Addressables, then register with save system
|
||
LoadCardDefinitionsFromAddressables();
|
||
|
||
Logging.Debug("[CardSystemManager] Initialized");
|
||
}
|
||
|
||
/// <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();
|
||
|
||
Logging.Debug($"[CardSystemManager] Loaded {availableCards.Count} card definitions from Addressables");
|
||
}
|
||
else
|
||
{
|
||
Logging.Warning("[CardSystemManager] Failed to load card definitions from Addressables");
|
||
}
|
||
}
|
||
|
||
|
||
/// <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}");
|
||
}
|
||
}
|
||
}
|
||
|
||
#region Save/Load Lifecycle Hooks
|
||
|
||
protected override string OnGlobalSaveRequested()
|
||
{
|
||
var state = ExportCardCollectionState();
|
||
return JsonUtility.ToJson(state);
|
||
}
|
||
|
||
protected 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<CardCollectionState>(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
|
||
}
|
||
}
|