Refactor, cleanup code and add documentaiton

This commit is contained in:
Michal Pikulski
2025-11-18 09:29:59 +01:00
parent 034654c308
commit cf13fb78b5
100 changed files with 689 additions and 537 deletions

View File

@@ -0,0 +1,28 @@
using System.Collections.Generic;
using AppleHills.Data.CardSystem;
namespace Data.CardSystem
{
/// <summary>
/// Serializable snapshot of the card collection state for save/load operations.
/// </summary>
[System.Serializable]
public class CardCollectionState
{
public int boosterPackCount;
public List<SavedCardEntry> cards = new List<SavedCardEntry>();
public List<SavedCardEntry> pendingRevealCards = new List<SavedCardEntry>();
public List<string> placedInAlbumCardIds = new List<string>();
}
/// <summary>
/// Serializable representation of a single card's saved data.
/// </summary>
[System.Serializable]
public class SavedCardEntry
{
public string definitionId;
public CardRarity rarity;
public int copiesOwned;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e552abbd5bd74192840939e499372ff2
timeCreated: 1761830599

View File

@@ -0,0 +1,106 @@
using System;
using UnityEngine;
namespace AppleHills.Data.CardSystem
{
[Serializable]
public class CardData
{
// Core data (serialized)
public string Id; // Auto-generated unique ID (GUID)
public string DefinitionId; // ID of the card definition this instance was created from
public CardRarity Rarity; // Current rarity (may be upgraded from original)
public int CopiesOwned; // Number of copies the player has (for stacking)
// Reference back to the definition (not serialized)
[NonSerialized]
private CardDefinition _definition;
// Properties that reference definition data
public string Name => _definition?.Name;
public string Description => _definition?.Description;
public CardZone Zone => _definition?.Zone ?? CardZone.AppleHills;
public int CollectionIndex => _definition?.CollectionIndex ?? 0;
public Sprite CardImage => _definition?.CardImage;
// Default constructor
public CardData()
{
Id = Guid.NewGuid().ToString();
CopiesOwned = 0;
}
// Constructor from definition
public CardData(CardDefinition definition)
{
Id = Guid.NewGuid().ToString();
DefinitionId = definition.Id;
Rarity = definition.Rarity;
CopiesOwned = 1;
_definition = definition;
}
// Copy constructor
public CardData(CardData other)
{
Id = other.Id;
DefinitionId = other.DefinitionId;
Rarity = other.Rarity;
CopiesOwned = other.CopiesOwned;
_definition = other._definition;
}
// Method to link this card data to its definition
public void SetDefinition(CardDefinition definition)
{
if (definition != null)
{
_definition = definition;
DefinitionId = definition.Id;
}
}
// Method to upgrade rarity when enough copies are collected
public bool TryUpgradeRarity()
{
// Simple implementation - each rarity needs twice as many copies to upgrade
int requiredCopies = (int)Rarity * 2 + 1;
if (CopiesOwned >= requiredCopies && Rarity < CardRarity.Legendary)
{
Rarity += 1;
CopiesOwned -= requiredCopies;
return true;
}
return false;
}
// ToString method for debugging
public override string ToString()
{
return $"CardData [ID: {Id}, Name: {Name}, Rarity: {Rarity}, Zone: {Zone}, " +
$"DefinitionID: {DefinitionId}, Copies: {CopiesOwned}, " +
$"Has Definition: {_definition != null}, Has Image: {CardImage != null}]";
}
}
// Enums for card attributes
public enum CardRarity
{
Normal = 0,
Rare = 1,
Legendary = 2
}
public enum CardZone
{
NotApplicable,
AppleHills,
Quarry,
CementFactory,
CentralStreet,
Valentine,
Dump
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 812f681e555841c584d5791cb66278de
timeCreated: 1759923654

View File

@@ -0,0 +1,59 @@
using System;
using UnityEngine;
namespace AppleHills.Data.CardSystem
{
/// <summary>
/// Scriptable object defining a collectible card's properties.
/// Used as a template for generating CardData instances.
/// </summary>
[CreateAssetMenu(fileName = "New Card", menuName = "AppleHills/Card System/Card Definition")]
public class CardDefinition : ScriptableObject
{
[Header("Identification")]
[Tooltip("Unique identifier for this card definition")]
public string Id = Guid.NewGuid().ToString();
[Header("Basic Info")]
public string Name;
[Tooltip("Use a custom file name instead of the card name")]
public bool UseCustomFileName = false;
[Tooltip("Custom file name (only used if UseCustomFileName is true)")]
public string CustomFileName = "";
[TextArea(3, 5)]
public string Description;
public CardRarity Rarity;
public CardZone Zone;
[Header("Visual Elements")]
public Sprite CardImage; // The actual card image
[Header("Collection Info")]
public int CollectionIndex; // Position in the album
/// <summary>
/// Creates a new CardData instance from this definition
/// </summary>
public CardData CreateCardData()
{
return new CardData(this);
}
public override bool Equals(object obj)
{
if (obj is CardDefinition other)
{
return string.Equals(Id, other.Id, StringComparison.Ordinal);
}
return false;
}
public override int GetHashCode()
{
return Id != null ? Id.GetHashCode() : 0;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2a80cc88c9884512b8b633110d838780
timeCreated: 1759923702

View File

@@ -0,0 +1,235 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace AppleHills.Data.CardSystem
{
/// <summary>
/// Manages the player's collection of cards and booster packs
/// </summary>
[Serializable]
public class CardInventory
{
// 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;
// Additional lookup dictionaries (not serialized)
[NonSerialized] private Dictionary<CardZone, List<CardData>> cardsByZone = new Dictionary<CardZone, List<CardData>>();
[NonSerialized] private Dictionary<CardRarity, List<CardData>> cardsByRarity = new Dictionary<CardRarity, List<CardData>>();
// Properties with public getters
public Dictionary<string, CardData> CollectedCards => collectedCards;
public int BoosterPackCount
{
get => boosterPackCount;
set => boosterPackCount = value;
}
// Constructor initializes empty dictionaries
public CardInventory()
{
// Initialize dictionaries for all enum values so we never have to check for null
foreach (CardZone zone in Enum.GetValues(typeof(CardZone)))
{
cardsByZone[zone] = new List<CardData>();
}
foreach (CardRarity rarity in Enum.GetValues(typeof(CardRarity)))
{
cardsByRarity[rarity] = new List<CardData>();
}
}
/// <summary>
/// Get all cards in the player's collection as a list
/// </summary>
public List<CardData> GetAllCards()
{
return collectedCards.Values.ToList();
}
/// <summary>
/// Clears all cards from the inventory
/// Primarily used for testing
/// </summary>
public void ClearAllCards()
{
collectedCards.Clear();
// Clear lookup dictionaries
foreach (var zone in cardsByZone.Keys)
{
cardsByZone[zone].Clear();
}
foreach (var rarity in cardsByRarity.Keys)
{
cardsByRarity[rarity].Clear();
}
}
/// <summary>
/// Get cards filtered by zone
/// </summary>
public List<CardData> GetCardsByZone(CardZone zone)
{
return new List<CardData>(cardsByZone[zone]);
}
/// <summary>
/// Get cards filtered by rarity
/// </summary>
public List<CardData> GetCardsByRarity(CardRarity rarity)
{
return new List<CardData>(cardsByRarity[rarity]);
}
/// <summary>
/// Add a card to the inventory (or increase the copies if already owned)
/// </summary>
public void AddCard(CardData card)
{
if (card == null) return;
string key = GetCardKey(card.DefinitionId, card.Rarity);
if (collectedCards.TryGetValue(key, out CardData existingCard))
{
// Increase copies of existing card
existingCard.CopiesOwned++;
}
else
{
// Add new card to collection
var newCard = new CardData(card);
collectedCards[key] = newCard;
// Add to lookup dictionaries
cardsByZone[newCard.Zone].Add(newCard);
cardsByRarity[newCard.Rarity].Add(newCard);
}
}
/// <summary>
/// Update card zone and rarity indexes when a card changes
/// </summary>
public void UpdateCardProperties(CardData card, CardZone oldZone, CardRarity oldRarity)
{
// Only needed if the card's zone or rarity actually changed
if (oldZone != card.Zone)
{
cardsByZone[oldZone].Remove(card);
cardsByZone[card.Zone].Add(card);
}
if (oldRarity != card.Rarity)
{
cardsByRarity[oldRarity].Remove(card);
cardsByRarity[card.Rarity].Add(card);
}
}
/// <summary>
/// Get a specific card from the collection by definition ID and rarity
/// </summary>
public CardData GetCard(string definitionId, CardRarity rarity)
{
string key = GetCardKey(definitionId, rarity);
return collectedCards.TryGetValue(key, out CardData card) ? card : null;
}
/// <summary>
/// Check if the player has a specific card at a specific rarity
/// </summary>
public bool HasCard(string definitionId, CardRarity rarity)
{
string key = GetCardKey(definitionId, rarity);
return collectedCards.ContainsKey(key);
}
/// <summary>
/// Get total number of unique cards in collection
/// </summary>
public int GetUniqueCardCount()
{
return collectedCards.Count;
}
/// <summary>
/// Get total number of cards including copies
/// </summary>
public int GetTotalCardCount()
{
return collectedCards.Values.Sum(card => card.CopiesOwned);
}
/// <summary>
/// Get number of cards in a specific zone
/// </summary>
public int GetZoneCardCount(CardZone zone)
{
return cardsByZone[zone].Count;
}
/// <summary>
/// Get number of cards of a specific rarity
/// </summary>
public int GetRarityCardCount(CardRarity rarity)
{
return cardsByRarity[rarity].Count;
}
/// <summary>
/// Get cards sorted by collection index (for album view)
/// </summary>
public List<CardData> GetCardsSortedByIndex()
{
return collectedCards.Values
.OrderBy(card => card.CollectionIndex)
.ToList();
}
/// <summary>
/// Check if there's a complete collection for a specific zone
/// </summary>
public bool IsZoneCollectionComplete(CardZone zone, List<CardDefinition> allAvailableCards)
{
int availableInZone = allAvailableCards.Count(card => card.Zone == zone);
int collectedInZone = cardsByZone[zone].Count;
return availableInZone > 0 && collectedInZone >= availableInZone;
}
/// <summary>
/// Adds booster packs to the inventory
/// </summary>
public void AddBoosterPacks(int count)
{
boosterPackCount += count;
}
/// <summary>
/// Use a single booster pack (returns true if successful)
/// </summary>
public bool UseBoosterPack()
{
if (boosterPackCount <= 0) return false;
boosterPackCount--;
return true;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f5b1aa91590d48a1a4c426f3cd4aa103
timeCreated: 1760445622

View File

@@ -0,0 +1,790 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AppleHills.Data.CardSystem;
using Core;
using Core.Lifecycle;
using UnityEngine;
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();
// Album system - cards waiting to be placed in album
private List<CardData> _pendingRevealCards = new List<CardData>();
private HashSet<string> _placedInAlbumCardIds = new HashSet<string>();
// Dictionary to quickly look up card definitions by ID
private Dictionary<string, CardDefinition> _definitionLookup;
private bool _lookupInitialized = false;
// Event callbacks using System.Action
public event Action<List<CardData>> OnBoosterOpened;
public event Action<CardData> OnCardCollected;
public event Action<int> OnBoosterCountChanged;
public event Action<CardData> OnPendingCardAdded;
public event Action<CardData> OnPendingCardRemoved;
public event Action<CardData> OnCardPlacedInAlbum;
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");
}
/// <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()
{
if (_definitionLookup == null)
{
_definitionLookup = new Dictionary<string, CardDefinition>();
}
_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;
}
/// <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
/// NOTE: Cards are NOT added to inventory immediately - they're added after the reveal interaction
/// </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);
// 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;
}
/// <summary>
/// Check if a card is new to the player's collection at the specified rarity
/// Checks both owned inventory and pending reveal queue
/// </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 or pending</returns>
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
}
/// <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
/// Checks both inventory and pending lists to find existing cards
/// </summary>
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.");
}
/// <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()
{
// 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
}
/// <summary>
/// Returns all cards from the player's collection (both owned and pending)
/// </summary>
public List<CardData> GetAllCollectedCards()
{
List<CardData> allCards = new List<CardData>(playerInventory.GetAllCards());
allCards.AddRange(_pendingRevealCards);
return allCards;
}
/// <summary>
/// Returns only owned/collected cards (excludes pending reveal cards)
/// </summary>
public List<CardData> GetCollectionOnly()
{
return new List<CardData>(playerInventory.GetAllCards());
}
/// <summary>
/// Returns cards from a specific zone (both owned and pending)
/// </summary>
public List<CardData> GetCardsByZone(CardZone zone)
{
List<CardData> zoneCards = new List<CardData>(playerInventory.GetCardsByZone(zone));
zoneCards.AddRange(_pendingRevealCards.Where(c => c.Zone == zone));
return zoneCards;
}
/// <summary>
/// Returns cards of a specific rarity (both owned and pending)
/// </summary>
public List<CardData> GetCardsByRarity(CardRarity rarity)
{
List<CardData> rarityCards = new List<CardData>(playerInventory.GetCardsByRarity(rarity));
rarityCards.AddRange(_pendingRevealCards.Where(c => c.Rarity == rarity));
return rarityCards;
}
/// <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 (at any rarity, in inventory or pending)
/// </summary>
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;
}
/// <summary>
/// Gets total unique card count (both owned and pending)
/// </summary>
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;
}
/// <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 (both owned and pending)
/// </summary>
public int GetCardCountByRarity(CardRarity rarity)
{
int inventoryCount = playerInventory.GetCardsByRarity(rarity).Count;
int pendingCount = _pendingRevealCards.Count(c => c.Rarity == rarity);
return inventoryCount + pendingCount;
}
/// <summary>
/// Returns the count of cards by zone (both owned and pending)
/// </summary>
public int GetCardCountByZone(CardZone zone)
{
int inventoryCount = playerInventory.GetCardsByZone(zone).Count;
int pendingCount = _pendingRevealCards.Count(c => c.Zone == zone);
return inventoryCount + pendingCount;
}
/// <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;
}
#region Album System
/// <summary>
/// Returns all pending reveal cards (cards waiting to be placed in album)
/// </summary>
public List<CardData> GetPendingRevealCards()
{
return _pendingRevealCards;
}
/// <summary>
/// Remove a card from the pending reveal list and fire event.
/// Called when a card starts being dragged to album slot.
/// </summary>
public bool RemoveFromPending(CardData card)
{
if (card == null) return false;
bool removed = _pendingRevealCards.Remove(card);
if (removed)
{
OnPendingCardRemoved?.Invoke(card);
Logging.Debug($"[CardSystemManager] Removed '{card.Name}' from pending reveal cards");
}
return removed;
}
/// <summary>
/// Get a card by definition ID and rarity from either inventory or pending
/// Returns the actual card reference so changes persist
/// </summary>
/// <param name="definitionId">Card definition ID</param>
/// <param name="rarity">Card rarity</param>
/// <param name="isFromPending">Out parameter - true if card is from pending, false if from inventory</param>
/// <returns>The card data if found, null otherwise</returns>
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;
}
/// <summary>
/// Get a card by definition ID and rarity from either inventory or pending (simplified overload)
/// </summary>
public CardData GetCard(string definitionId, CardRarity rarity)
{
return GetCard(definitionId, rarity, out _);
}
/// <summary>
/// Update a card's data in whichever list it's in (inventory or pending)
/// Useful for incrementing CopiesOwned, upgrading rarity, etc.
/// </summary>
/// <param name="definitionId">Card definition ID</param>
/// <param name="rarity">Card rarity</param>
/// <param name="updateAction">Action to perform on the card</param>
/// <returns>True if card was found and updated, false otherwise</returns>
public bool UpdateCard(string definitionId, CardRarity rarity, System.Action<CardData> 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;
}
/// <summary>
/// Marks a card as placed in the album
/// Adds card to owned inventory and tracks as placed
/// Note: Card may have already been removed from pending list during drag
/// </summary>
public void MarkCardAsPlaced(CardData card)
{
if (card == null)
{
Logging.Warning("[CardSystemManager] Attempted to place null card");
return;
}
// Try to remove from pending (may already be removed during drag)
bool wasInPending = _pendingRevealCards.Remove(card);
// Add to owned inventory (regardless of whether it was in pending)
playerInventory.AddCard(card);
// Track as placed
_placedInAlbumCardIds.Add(card.Id);
// Fire events
OnCardPlacedInAlbum?.Invoke(card);
OnCardCollected?.Invoke(card);
if (wasInPending)
{
Logging.Debug($"[CardSystemManager] Card '{card.Name}' removed from pending and added to inventory");
}
else
{
Logging.Debug($"[CardSystemManager] Card '{card.Name}' added to inventory (was already removed from pending)");
}
}
/// <summary>
/// Checks if a card has been placed in the album
/// </summary>
public bool IsCardPlacedInAlbum(string cardId)
{
return _placedInAlbumCardIds.Contains(cardId);
}
/// <summary>
/// Gets count of cards waiting to be revealed
/// </summary>
public int GetPendingRevealCount()
{
return _pendingRevealCards.Count;
}
#endregion
/// <summary>
/// Export current card collection to a serializable snapshot
/// </summary>
public CardCollectionState ExportCardCollectionState()
{
var state = new CardCollectionState
{
boosterPackCount = playerInventory.BoosterPackCount,
cards = new List<SavedCardEntry>(),
pendingRevealCards = new List<SavedCardEntry>(),
placedInAlbumCardIds = new List<string>(_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;
}
/// <summary>
/// Apply a previously saved snapshot to the runtime inventory
/// </summary>
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);
}
}
}
/// <summary>
/// Clears all card collection data - inventory, pending cards, boosters, and placement tracking
/// Used for dev reset functionality
/// </summary>
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<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
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8d80347e4bd04c87be23a9399860783d
timeCreated: 1759923691

View File

@@ -0,0 +1,206 @@
using System;
using System.Collections.Generic;
using Core;
using UnityEngine;
namespace AppleHills.Data.CardSystem
{
/// <summary>
/// ScriptableObject containing visual configuration for card display
/// Maps card rarities to frames/overlays and zones to backgrounds/shapes
/// </summary>
[CreateAssetMenu(fileName = "CardVisualConfig", menuName = "AppleHills/Card System/Visual Config")]
public class CardVisualConfig : ScriptableObject
{
[Serializable]
public class RarityVisualMapping
{
public CardRarity rarity;
[Tooltip("Frame image for this rarity")]
public Sprite frame;
[Tooltip("Overlay image for this rarity (can be null)")]
public Sprite overlay;
}
[Serializable]
public class ZoneVisualMapping
{
public CardZone zone;
[Tooltip("Background image for this zone")]
public Sprite background;
[Tooltip("Shape sprite for Normal rarity cards in this zone")]
public Sprite shapeNormal;
[Tooltip("Shape sprite for Rare rarity cards in this zone")]
public Sprite shapeRare;
}
[Header("Rarity Configuration")]
[Tooltip("Visual mappings for different card rarities (frames and overlays)")]
[SerializeField] private List<RarityVisualMapping> rarityVisuals = new List<RarityVisualMapping>();
[Header("Zone Configuration")]
[Tooltip("Visual mappings for different card zones (backgrounds and shapes)")]
[SerializeField] private List<ZoneVisualMapping> zoneVisuals = new List<ZoneVisualMapping>();
[Header("Legendary Override")]
[Tooltip("Background used for all Legendary cards, regardless of zone")]
[SerializeField] private Sprite legendaryBackground;
private Dictionary<CardRarity, RarityVisualMapping> _rarityLookup;
private Dictionary<CardZone, ZoneVisualMapping> _zoneLookup;
/// <summary>
/// Initialize the lookup dictionaries when the asset is loaded
/// </summary>
private void OnEnable()
{
InitializeLookups();
}
/// <summary>
/// Builds the lookup dictionaries from the serialized lists
/// </summary>
private void InitializeLookups()
{
// Build rarity visual lookup
_rarityLookup = new Dictionary<CardRarity, RarityVisualMapping>();
foreach (var mapping in rarityVisuals)
{
_rarityLookup[mapping.rarity] = mapping;
}
// Build zone visual lookup
_zoneLookup = new Dictionary<CardZone, ZoneVisualMapping>();
foreach (var mapping in zoneVisuals)
{
_zoneLookup[mapping.zone] = mapping;
}
}
/// <summary>
/// Get the frame sprite for a specific card rarity
/// </summary>
public Sprite GetRarityFrame(CardRarity rarity)
{
if (_rarityLookup == null) InitializeLookups();
if (_rarityLookup.TryGetValue(rarity, out RarityVisualMapping mapping))
{
return mapping.frame;
}
Logging.Warning($"[CardVisualConfig] No frame mapping found for rarity {rarity}");
return null;
}
/// <summary>
/// Get the overlay sprite for a specific card rarity (can be null)
/// </summary>
public Sprite GetRarityOverlay(CardRarity rarity)
{
if (_rarityLookup == null) InitializeLookups();
if (_rarityLookup.TryGetValue(rarity, out RarityVisualMapping mapping))
{
return mapping.overlay;
}
return null;
}
/// <summary>
/// Get the background sprite for a card based on zone and rarity
/// Legendary cards always use the legendary background override
/// </summary>
public Sprite GetBackground(CardZone zone, CardRarity rarity)
{
if (_zoneLookup == null) InitializeLookups();
// Legendary cards use special background
if (rarity == CardRarity.Legendary)
{
return legendaryBackground;
}
// Normal and Rare cards use zone background
if (_zoneLookup.TryGetValue(zone, out ZoneVisualMapping mapping))
{
return mapping.background;
}
Logging.Warning($"[CardVisualConfig] No background mapping found for zone {zone}");
return null;
}
/// <summary>
/// Get the shape sprite for a card based on zone and rarity
/// Legendary cards don't display shapes (returns null)
/// </summary>
public Sprite GetZoneShape(CardZone zone, CardRarity rarity)
{
if (_zoneLookup == null) InitializeLookups();
// Legendary cards don't have shapes
if (rarity == CardRarity.Legendary)
{
return null;
}
if (_zoneLookup.TryGetValue(zone, out ZoneVisualMapping mapping))
{
// Return shape based on rarity
return rarity == CardRarity.Rare ? mapping.shapeRare : mapping.shapeNormal;
}
Logging.Warning($"[CardVisualConfig] No shape mapping found for zone {zone}");
return null;
}
#if UNITY_EDITOR
/// <summary>
/// Editor-only utility to initialize the config with default structure
/// </summary>
public void InitializeDefaults()
{
// Clear existing mappings
rarityVisuals.Clear();
zoneVisuals.Clear();
// Add entries for all rarities
foreach (CardRarity rarity in Enum.GetValues(typeof(CardRarity)))
{
rarityVisuals.Add(new RarityVisualMapping { rarity = rarity });
}
// Add entries for all zones
foreach (CardZone zone in Enum.GetValues(typeof(CardZone)))
{
zoneVisuals.Add(new ZoneVisualMapping { zone = zone });
}
// Initialize the lookups
InitializeLookups();
}
#endif
}
#if UNITY_EDITOR
[UnityEditor.CustomEditor(typeof(CardVisualConfig))]
public class CardVisualConfigEditor : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
CardVisualConfig config = (CardVisualConfig)target;
UnityEditor.EditorGUILayout.Space();
if (GUILayout.Button("Initialize Default Structure"))
{
config.InitializeDefaults();
UnityEditor.EditorUtility.SetDirty(config);
}
}
}
#endif
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a82f88f485b4410e9eb7c383b44557cf
timeCreated: 1759931508