# Card System Guide Complete guide to the Apple Hills card collecting and album system. ## Table of Contents - [Overview](#overview) - [Quick Start](#quick-start) - [System Architecture](#system-architecture) - [Card Flows](#card-flows) - [Key Components](#key-components) - [Working with Cards in Code](#working-with-cards-in-code) - [Extending the System](#extending-the-system) - [Troubleshooting](#troubleshooting) --- ## Overview The Card System manages the player's collectible card journey from booster pack opening to album placement. It consists of: - **Booster Opening Flow**: Interactive pack opening with card reveals - **Album Placement Flow**: Drag-and-drop cards into album slots - **State Machine**: Each card progresses through well-defined states - **Album Navigation**: Book-based album with zone tabs - **Notification System**: Visual feedback for new boosters/pending cards ### Core Concepts **CardData**: Runtime data for a single card (definition ID, rarity, zone, image) **CardDefinition**: ScriptableObject template defining card properties **Card States**: Cards transition through states (Idle → Revealed → Dragging → Placed) **Pending Cards**: Cards waiting to be placed in the album (shown in bottom-right corner) --- ## Quick Start ### Opening the Album ```csharp // From any script if (UIPageController.Instance != null) { UIPageController.Instance.PushPage(albumViewPage); } ``` ### Granting Booster Packs ```csharp // Simple API CardSystemManager.Instance.AddBoosterPack(1); // Visual grant with animation (from minigames) MinigameBoosterGiver.Instance.GiveBooster(() => { Debug.Log("Booster granted and animation complete!"); }); ``` ### Opening a Booster Pack (Programmatic) ```csharp // Returns list of cards in the pack List cards = CardSystemManager.Instance.OpenBoosterPack(); // The cards are automatically added to "pending" state // Player must place them in album manually ``` ### Checking Player's Collection ```csharp var inventory = CardSystemManager.Instance.GetCardInventory(); // Check if player owns a specific card CardData ownedCard = inventory.GetCard("CardDefID", CardRarity.Legendary); // Get all cards in a zone List townCards = inventory.GetCardsByZone(CardZone.Town); // Check completion percentage float completion = inventory.GetCompletionPercentage(); ``` --- ## System Architecture ### Component Hierarchy ``` CardSystemManager (Singleton) ├── Inventory Management ├── Booster Pack Logic └── Pending Card Queue AlbumViewPage (UI Page) ├── CornerCardManager (Non-Component) │ └── Manages 3 pending cards in corner ├── AlbumNavigationService (Non-Component) │ └── Book page flipping & zone navigation └── CardEnlargeController (Non-Component) └── Backdrop & enlarge/shrink animations BoosterOpeningPage (UI Page) └── Manages pack opening flow & card reveals Card (MonoBehaviour) ├── CardContext (shared state) ├── CardAnimator (animations) ├── CardDisplay (visuals) └── StateMachine (10 possible states) ``` ### Non-Component Controllers The system uses **Controller** pattern for complex logic without Unity lifecycle overhead: - **CornerCardManager**: Spawns/despawns pending cards, smart selection, shuffle logic - **AlbumNavigationService**: Book page navigation, zone mapping - **CardEnlargeController**: Backdrop visibility, card reparenting for enlarge view - **ProgressBarController**: Booster opening progress visualization These are instantiated lazily via C# properties: ```csharp private CornerCardManager _cornerCardManager; private CornerCardManager CornerCards => _cornerCardManager ??= new CornerCardManager(...); ``` --- ## Card Flows ### 1. Booster Opening Flow ``` Player opens booster pack ↓ BoosterOpeningPage spawns cards face-down ↓ Player clicks a card (IdleState → EnlargedNewState/EnlargedRepeatState) ↓ Card flips & enlarges, shows NEW or REPEAT badge ↓ Player clicks to dismiss (→ RevealedState) ↓ Repeat until all cards revealed ↓ Cards fly to album icon, added to pending queue ↓ Page auto-closes (or waits for player) ``` **Key States:** - **IdleState**: Face-down, awaiting click - **EnlargedNewState**: NEW badge (first time collection) - **EnlargedRepeatState**: REPEAT badge (duplicate) - **EnlargedLegendaryRepeatState**: Special legendary repeat state - **RevealedState**: Face-up, dismissed from center ### 2. Album Placement Flow ``` AlbumViewPage opens ↓ CornerCardManager spawns up to 3 pending cards (face-down) ↓ Player drags card (PendingFaceDownState → DraggingRevealedState) ↓ Card data assigned, flips to reveal, book navigates to correct zone page ↓ Player drops card on matching AlbumCardSlot ↓ Card placed (→ PlacedInSlotState), registered with AlbumViewPage ↓ CornerCardManager rebuilds: shuffles remaining cards, spawns new if available ``` **Key States:** - **PendingFaceDownState**: Face-down in corner, no data assigned yet - **DraggingRevealedState**: Data assigned, flipped, dragging to slot - **PlacedInSlotState**: Locked in album slot - **AlbumEnlargedState**: Clicked from slot, enlarged for viewing ### 3. Album Card Viewing ``` Player clicks placed card in album slot ↓ Card enlarges (PlacedInSlotState → AlbumEnlargedState) ↓ Backdrop shown, card reparented to enlarged container ↓ Card animates to center with dramatic scale increase ↓ Player clicks card or backdrop to dismiss ↓ Card shrinks & animates back to slot (→ PlacedInSlotState) ↓ Backdrop hidden, card reparented back to slot ``` --- ## Key Components ### CardSystemManager **Singleton** managing all card data and inventory. ```csharp // Access CardSystemManager.Instance // Key Methods .AddBoosterPack(int count) // Grant booster packs .OpenBoosterPack() // Open a pack, returns CardData[] .GetPendingRevealCards() // Get cards waiting for album placement .GetCardInventory() // Access player's collection .AddCardToInventory(CardData) // Add card to collection .RemoveFromPending(CardData) // Remove from pending queue // Events .OnBoosterCountChanged(int newCount) .OnPendingCardAdded(CardData) .OnPendingCardRemoved(CardData) ``` ### AlbumViewPage **UI Page** for viewing and managing the album. ```csharp // Setup in scene: // - Assign book reference (BookPro component) // - Assign zone tab container // - Assign card prefab for spawning // - Assign backdrop & enlarged container for card viewing // - Link to BoosterOpeningPage // Query Methods (used by Card states) public CardData GetCardForPendingSlot() // Smart card selection public AlbumCardSlot GetTargetSlotForCard(CardData) // Find destination slot public void NavigateToCardPage(CardData, Action) // Flip to correct page public void NotifyCardPlaced(Card) // Cleanup after placement // Public Properties public bool IsPageFlipping // For state timing checks ``` ### BoosterOpeningPage **UI Page** for opening booster packs. ```csharp // Setup in scene: // - Assign booster pack prefab // - Assign corner slots for waiting boosters (max 3) // - Assign center slot for opening // - Assign card display container // - Assign card prefab // - Assign album icon (dismiss button & tween target) // Call before showing: .SetAvailableBoosterCount(int count) // How many boosters player has // Flow automatically managed by page ``` ### Card States Each card uses **AppleMachine** state machine with these states: | State | Purpose | Entry Trigger | |-------|---------|---------------| | **IdleState** | Face-down in booster opening | Card spawned for booster reveal | | **EnlargedNewState** | Enlarged with NEW badge | Clicked first-time card | | **EnlargedRepeatState** | Enlarged with REPEAT badge | Clicked duplicate | | **EnlargedLegendaryRepeatState** | Legendary repeat variant | Clicked legendary duplicate | | **RevealedState** | Face-up, dismissed | Dismissed from enlarged state | | **PendingFaceDownState** | Face-down in corner | Spawned in album corner | | **DraggingRevealedState** | Face-up while dragging | Dragged from corner | | **PlacedInSlotState** | Locked in album slot | Dropped on correct slot | | **AlbumEnlargedState** | Enlarged from album | Clicked while in slot | | **DraggingState** | Generic drag state | *(unused in current flow)* | ### Card Context & Components Every card has: - **CardContext**: Shared state, component references, events - **CardAnimator**: Centralized animation methods - **CardDisplay**: Visual rendering (image, frame, overlay, rarity/zone styling) - **AppleMachine**: State machine controller ```csharp // Accessing card components var card = GetComponent(); card.Context.CardData // CardData card.Context.Animator // CardAnimator card.Context.CardDisplay // CardDisplay card.Context.StateMachine // AppleMachine card.Context.AlbumViewPage // Injected page reference ``` --- ## Working with Cards in Code ### Spawning a Card for Booster Opening ```csharp GameObject cardObj = Instantiate(cardPrefab, containerTransform); var card = cardObj.GetComponent(); var context = cardObj.GetComponent(); // Setup card data context.SetupCard(cardData); // Start in IdleState for booster reveal card.SetupForBoosterReveal(cardData, isNew); // isNew unused, states query inventory // Subscribe to reveal complete event context.BoosterContext.OnRevealFlowComplete += () => { Debug.Log("Card reveal finished!"); }; ``` ### Spawning a Card for Album Corner ```csharp GameObject cardObj = Instantiate(cardPrefab, slotTransform); var card = cardObj.GetComponent(); // Assign to slot FIRST card.AssignToSlot(slot, animateMove: false); // Inject AlbumViewPage dependency card.Context.SetAlbumViewPage(albumViewPage); // Setup for pending state (no data yet) card.SetupForAlbumPending(); // Starts in PendingFaceDownState ``` ### Spawning a Card Already in Album Slot ```csharp GameObject cardObj = Instantiate(cardPrefab, slotTransform); var card = cardObj.GetComponent(); // Setup for album slot (already owned) card.SetupForAlbumSlot(cardData, albumCardSlot); // Starts in PlacedInSlotState // Register for enlarge/shrink functionality albumViewPage.RegisterCardInAlbum(card); ``` ### Transitioning Card States Manually ```csharp // Get card component var card = GetComponent(); // Change state card.ChangeState(CardStateNames.Revealed); // Check current state string currentState = card.GetCurrentStateName(); if (currentState == CardStateNames.PlacedInSlot) { Debug.Log("Card is placed in album!"); } // Get specific state component var enlargedState = card.GetStateComponent( CardStateNames.AlbumEnlarged ); if (enlargedState != null) { // Access state-specific methods/properties } ``` ### Subscribing to Card Events ```csharp var context = card.Context; // Drag events context.OnDragStarted += (ctx) => Debug.Log("Drag started!"); context.OnDragEnded += (ctx) => Debug.Log("Drag ended!"); // Click events (routed through CardDisplay) context.CardDisplay.OnCardClicked += (display) => Debug.Log("Card clicked!"); // Booster reveal events context.BoosterContext.OnRevealFlowComplete += () => { Debug.Log("Reveal complete!"); CardSystemManager.Instance.AddCardToInventory(context.CardData); }; // State machine events (if needed) context.StateMachine.OnStateChange += (newState) => Debug.Log($"State: {newState.name}"); ``` ### Custom Animations ```csharp var animator = card.Animator; // Built-in animations animator.PopIn(duration: 0.5f, onComplete: () => Debug.Log("Popped in!")); animator.PopOut(duration: 0.3f); animator.PlayEnlarge(targetScale: 2.5f); animator.PlayShrink(targetScale: Vector3.one); // Combine animations animator.AnimateLocalPosition(Vector3.zero, duration: 0.5f); animator.AnimateScale(Vector3.one * 1.5f, duration: 0.3f); // Flip animation Transform cardBack = card.transform.Find("CardBack"); Transform cardFront = card.transform.Find("CardDisplay"); animator.PlayFlip(cardBack, cardFront, duration: 0.6f, onComplete: () => { Debug.Log("Flip complete!"); }); animator.PlayFlipScalePunch(punchScale: 1.1f); // Hover effects Vector2 originalPos = animator.GetAnchoredPosition(); animator.HoverEnter(liftAmount: 20f, scaleMultiplier: 1.05f); // ... later animator.HoverExit(originalPos); ``` --- ## Extending the System ### Adding a New Card State 1. **Create State Script** in `StateMachine/States/` ```csharp using Core.SaveLoad; using UI.CardSystem.StateMachine; public class MyNewCardState : AppleState, ICardClickHandler { private CardContext _context; public override void EnterState() { _context = GetComponentInParent(); // Setup animations, visuals, etc. } public void OnCardClicked(CardContext context) { // Handle click behavior } public override void ExitState() { // Cleanup } } ``` 2. **Add State Name** to `CardStateNames.cs` ```csharp public const string MyNewState = "MyNewCardState"; ``` 3. **Add GameObject** as child of Card prefab's AppleMachine with your state component attached 4. **Transition to State** ```csharp card.ChangeState(CardStateNames.MyNewState); ``` ### Creating a Custom Card Visual Effect ```csharp public class MyCardEffect : MonoBehaviour { private CardContext _context; void Start() { _context = GetComponentInParent(); // Subscribe to state changes _context.StateMachine.OnStateChange += OnStateChanged; } void OnStateChanged(AppleState newState) { if (newState.name == CardStateNames.EnlargedNew) { // Play custom effect PlaySparkleEffect(); } } void PlaySparkleEffect() { // Your custom effect logic } } ``` ### Adding Custom Card Slots ```csharp public class MyCustomCardSlot : AlbumCardSlot { protected override void OnCardPlaced(Card card) { base.OnCardPlaced(card); // Custom logic when card placed PlaySpecialEffect(); } } ``` --- ## Troubleshooting ### Cards Not Showing in Corner **Check:** 1. AlbumViewPage has `cardPrefab` assigned 2. AlbumViewPage has `bottomRightSlots` assigned (SlotContainer with 3 slots) 3. CardSystemManager has pending cards: `GetPendingRevealCards().Count > 0` 4. Page is in album proper (not menu page): Check with `IsInAlbumProper()` **Debug:** ```csharp Debug.Log($"Pending cards: {CardSystemManager.Instance.GetPendingRevealCards().Count}"); Debug.Log($"Is in album: {albumViewPage.IsInAlbumProper()}"); ``` ### Cards Spawning on Top of Each Other **Cause:** Corner slots not properly configured with `SlotIndex` property **Fix:** - Ensure each slot has unique `SlotIndex` (0, 1, 2) - Verify in inspector: Select each slot → Check `SlotIndex` field ### Card Won't Flip **Check:** 1. Card has `CardBack` child GameObject 2. Card has `CardDisplay` child GameObject 3. Both have proper hierarchy: `Card → CardBack`, `Card → CardDisplay` 4. State transitions are correct **Debug:** ```csharp var cardBack = card.transform.Find("CardBack"); var cardDisplay = card.transform.Find("CardDisplay"); Debug.Log($"Back found: {cardBack != null}, Display found: {cardDisplay != null}"); Debug.Log($"Current state: {card.GetCurrentStateName()}"); ``` ### Book Won't Flip to Correct Page **Check:** 1. BookTabButton components configured with correct `zone` and `targetPage` 2. AlbumViewPage has `tabContainer` assigned 3. BookPro component reference assigned on AlbumViewPage **Debug:** ```csharp // In AlbumViewPage foreach (var tab in _zoneTabs) { Debug.Log($"Tab: {tab.Zone} → Page {tab.TargetPage}"); } ``` ### Cards Not Enlarging When Clicked in Album **Check:** 1. Card is in `PlacedInSlotState` 2. AlbumViewPage has `cardEnlargedBackdrop` and `cardEnlargedContainer` assigned 3. Card was registered: `albumViewPage.RegisterCardInAlbum(card)` **Debug:** ```csharp var enlargedState = card.GetStateComponent(CardStateNames.AlbumEnlarged); Debug.Log($"Enlarged state found: {enlargedState != null}"); Debug.Log($"Current state: {card.GetCurrentStateName()}"); ``` ### Memory Leaks / Cards Not Destroying **Cause:** Event subscriptions not cleaned up **Fix Pattern:** ```csharp void OnEnable() { card.Context.OnDragStarted += HandleDrag; } void OnDisable() { if (card != null && card.Context != null) { card.Context.OnDragStarted -= HandleDrag; } } ``` ### Booster Packs Not Appearing **Check:** 1. BoosterOpeningPage has `boosterPackPrefab` assigned 2. BoosterOpeningPage has `bottomRightSlots` assigned 3. Called `SetAvailableBoosterCount()` before showing page **Debug:** ```csharp Debug.Log($"Booster count: {CardSystemManager.Instance.GetBoosterPackCount()}"); Debug.Log($"Booster prefab: {boosterOpeningPage.boosterPackPrefab != null}"); ``` --- ## State Diagram ``` BOOSTER OPENING FLOW: IdleState ──click──> EnlargedNewState ──click──> RevealedState └──> EnlargedRepeatState ──┘ └──> EnlargedLegendaryRepeatState ──┘ ALBUM PLACEMENT FLOW: PendingFaceDownState ──drag──> DraggingRevealedState ──drop on slot──> PlacedInSlotState │ click │ ↓ AlbumEnlargedState │ click │ ↓ PlacedInSlotState ``` --- ## Additional Resources - **Code Location**: `Assets/Scripts/UI/CardSystem/` - **Card Prefabs**: `Assets/Prefabs/UI/Cards/` - **Card Definitions**: `Assets/Data/CardSystem/Definitions/` - **Album Scene**: `Assets/Scenes/Album.unity` **Related Systems:** - [UIPageController Documentation](ui_page_navigation.md) - UI page stack navigation - [DragAndDrop System](../Scripts/UI/DragAndDrop/Core/) - Base drag/drop framework - [Settings System](settings_readme.md) - Card system configuration --- **Last Updated:** November 18, 2025 **Version:** 1.0 **Contributors:** Development Team