Files
AppleHillsProduction/docs/card_system_guide.md
tschesky 235fa04eba Merge a card refresh (#59)
- **Refactored Card Placement Flow**
  - Separated card presentation from orchestration logic
  - Extracted `CornerCardManager` for pending card lifecycle (spawn, shuffle, rebuild)
  - Extracted `AlbumNavigationService` for book page navigation and zone mapping
  - Extracted `CardEnlargeController` for backdrop management and card reparenting
  - Implemented controller pattern (non-MonoBehaviour) for complex logic
  - Cards now unparent from slots before rebuild to prevent premature destruction

- **Improved Corner Card Display**
  - Fixed cards spawning on top of each other during rebuild
  - Implemented shuffle-to-front logic (remaining cards occupy slots 0→1→2)
  - Added smart card selection (prioritizes cards matching current album page)
  - Pending cards now removed from queue immediately on drag start
  - Corner cards rebuild after each placement with proper slot reassignment

- **Enhanced Album Card Viewing**
  - Added dramatic scale increase when viewing cards from album slots
  - Implemented shrink animation when dismissing enlarged cards
  - Cards transition: `PlacedInSlotState` → `AlbumEnlargedState` → `PlacedInSlotState`
  - Backdrop shows/hides with card enlarge/shrink cycle
  - Cards reparent to enlarged container while viewing, return to slot after

- **State Machine Improvements**
  - Added `CardStateNames` constants for type-safe state transitions
  - Implemented `ICardClickHandler` and `ICardStateDragHandler` interfaces
  - State transitions now use cached property indices
  - `BoosterCardContext` separated from `CardContext` for single responsibility

- **Code Cleanup**
  - Identified unused `SlotContainerHelper.cs` (superseded by `CornerCardManager`)
  - Identified unused `BoosterPackDraggable.canOpenOnDrop` field
  - Identified unused `AlbumViewPage._previousInputMode` (hardcoded value)
  - Identified unused `Card.OnPlacedInAlbumSlot` event (no subscribers)

Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com>
Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com>
Reviewed-on: #59
2025-11-18 08:40:59 +00:00

19 KiB

Card System Guide

Complete guide to the Apple Hills card collecting and album system.

Table of Contents


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

// From any script
if (UIPageController.Instance != null)
{
    UIPageController.Instance.PushPage(albumViewPage);
}

Granting Booster Packs

// 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)

// Returns list of cards in the pack
List<CardData> cards = CardSystemManager.Instance.OpenBoosterPack();

// The cards are automatically added to "pending" state
// Player must place them in album manually

Checking Player's Collection

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<CardData> 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:

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.

// 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.

// 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.

// 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
// Accessing card components
var card = GetComponent<Card>();
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

GameObject cardObj = Instantiate(cardPrefab, containerTransform);
var card = cardObj.GetComponent<Card>();
var context = cardObj.GetComponent<CardContext>();

// 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

GameObject cardObj = Instantiate(cardPrefab, slotTransform);
var card = cardObj.GetComponent<Card>();

// 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

GameObject cardObj = Instantiate(cardPrefab, slotTransform);
var card = cardObj.GetComponent<Card>();

// 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

// Get card component
var card = GetComponent<Card>();

// 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<CardAlbumEnlargedState>(
    CardStateNames.AlbumEnlarged
);
if (enlargedState != null)
{
    // Access state-specific methods/properties
}

Subscribing to Card Events

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

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/

    using Core.SaveLoad;
    using UI.CardSystem.StateMachine;
    
    public class MyNewCardState : AppleState, ICardClickHandler
    {
        private CardContext _context;
    
        public override void EnterState()
        {
            _context = GetComponentInParent<CardContext>();
            // Setup animations, visuals, etc.
        }
    
        public void OnCardClicked(CardContext context)
        {
            // Handle click behavior
        }
    
        public override void ExitState()
        {
            // Cleanup
        }
    }
    
  2. Add State Name to CardStateNames.cs

    public const string MyNewState = "MyNewCardState";
    
  3. Add GameObject as child of Card prefab's AppleMachine with your state component attached

  4. Transition to State

    card.ChangeState(CardStateNames.MyNewState);
    

Creating a Custom Card Visual Effect

public class MyCardEffect : MonoBehaviour
{
    private CardContext _context;
    
    void Start()
    {
        _context = GetComponentInParent<CardContext>();
        
        // 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

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:

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:

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:

// 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:

var enlargedState = card.GetStateComponent<CardAlbumEnlargedState>(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:

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:

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:


Last Updated: November 18, 2025
Version: 1.0
Contributors: Development Team