Files
AppleHillsProduction/docs/cards_wip/card_system_implementation_summary.md
2025-11-15 20:37:02 +01:00

14 KiB

# Card State Machine Implementation Summary

Architecture Overview

Isolated State Pattern using Pixelplacement StateMachine:

Card (RectTransform - primary animation target)
├─ CardDisplay (always visible - shows card front)
├─ CardContext (shared references + events)
├─ CardAnimator (reusable animations)
└─ CardStateMachine (AppleMachine)
    ├─ IdleState/
    │   └─ CardBackVisual ← State owns this
    ├─ FlippingState/
    │   └─ CardBackVisual ← State owns this
    ├─ RevealedState/
    │   ├─ NewCardIdleBadge ← State owns this
    │   └─ RepeatCardIdleBadge ← State owns this
    ├─ EnlargedNewState/
    │   └─ NewCardBadge ← State owns this
    ├─ EnlargedRepeatState/
    │   ├─ RepeatText ← State owns this
    │   └─ ProgressBarContainer/
    │       └─ ProgressBarUI (prefab with ProgressBarController)
    ├─ DraggingState/ (no child visuals)
    ├─ PlacedInSlotState/ (no child visuals)
    └─ AlbumEnlargedState/ (no child visuals)

State Flow Diagrams

Booster Opening Flow:

↓ [player clicks] 2. FlippingState (flip animation + scale punch)

  1. IdleState (card back in assigned slot, hover enabled) ↓ [player clicks - flip animation plays within IdleState] 3a. IF NEW CARD: ↓ [determine path based on card status]

2a. IF NEW CARD: 3b. IF LEGENDARY REPEAT: → [tap] → Fires OnCardDismissed → Shrink → RevealedState 3c. IF REPEAT (won't upgrade, e.g., 2/5): 2b. IF LEGENDARY REPEAT: → Skip enlarge → RevealedState (can't upgrade)

2c. IF REPEAT (won't upgrade, e.g., 2/5): → EnlargedRepeatState 3d. IF REPEAT (WILL upgrade, e.g., 5/5): → [tap] → Fires OnCardDismissed → Shrink → RevealedState

2d. IF REPEAT (WILL upgrade, e.g., 5/5): → EnlargedRepeatState → Show progress bar (5/5) + blink → AUTO-UPGRADE (no tap needed) → Fires OnUpgradeTriggered → Update inventory → Check if new/repeat at higher rarity: IF NEW at higher: → EnlargedNewState (higher rarity) 4. RevealedState (normal size, waiting) → [tap] → Fires OnCardDismissed → Shrink → RevealedState

  1. [When all cards complete] → Fires OnCardInteractionComplete → Waits for all 3 cards to finish

  2. [When all cards complete] → BoosterOpeningPage animates cards to album → Destroy


### **Album Placement Flow:**
  1. IdleState (card back in corner, hover enabled) ↓ [player clicks]
  2. FlippingState (reveals which card it is) → OnFlipComplete ↓
  3. RevealedState → OnCardInteractionComplete ↓ [player drags]
  4. DraggingState (scaled up during drag) ↓ [drop in slot]
  5. PlacedInSlotState (in album permanently) ↓ [player clicks]
  6. AlbumEnlargedState → Fires OnEnlargeRequested (page shows backdrop, reparents) ↓ [player taps] → Fires OnShrinkRequested (page prepares) → Shrink animation ↓
  7. PlacedInSlotState (back in slot)

## Key Design Decisions

### 1. State-Owned Visuals
- State-specific GameObjects (CardBackVisual, NewCardBadge, etc.) are **children of their state GameObject**
- When state activates → children activate automatically
- When state deactivates → children deactivate automatically
- **No manual visibility management needed!**

### 2. Transform Animation
- **Root Card.transform** is animated for position/scale (affects all children via Unity hierarchy)
- **State child visuals** can be animated independently (e.g., rotating CardBackVisual during flip)
- States decide WHAT to animate, CardAnimator provides HOW

### 3. Shared Resources via CardContext
```csharp
public class CardContext : MonoBehaviour
{
    // Component references
    public CardDisplay CardDisplay { get; }
    public CardAnimator Animator { get; }
    public AppleMachine StateMachine { get; }
    public Transform RootTransform { get; }
    
    // Card data
    public CardData CardData { get; }
    public bool IsNewCard { get; set; }
    public int RepeatCardCount { get; set; }
    public bool IsClickable { get; set; } // Prevents multi-flip in booster opening
    
    // Events for external coordination (BoosterOpeningPage)
    public event Action<CardContext> OnFlipComplete;
    public event Action<CardContext> OnCardDismissed;
    public event Action<CardContext> OnCardInteractionComplete;
    public event Action<CardContext> OnUpgradeTriggered;
    
    // Helper methods
    public void FireFlipComplete();
    public void FireCardDismissed();
    public void FireCardInteractionComplete();
    public void FireUpgradeTriggered();
}

4. State Transitions

States explicitly transition via _context.StateMachine.ChangeState("StateName")

Example:

// In CardFlippingState.OnFlipComplete():
if (_context.IsNewCard)
    _context.StateMachine.ChangeState("EnlargedNewState");
else if (_context.RepeatCardCount > 0)
    _context.StateMachine.ChangeState("EnlargedRepeatState");

5. Progress Bar Architecture

ProgressBarController Component:

  • Auto-detects child Image elements (5 images in GridLayout)
  • Fills from bottom to top (element[0] = bottom)
  • Blinks newest element with configurable timing
  • Callback when animation completes

Usage:

progressBar.ShowProgress(currentCount, maxCount, OnProgressComplete);

Files Created

Core Components:

  • StateMachine/CardContext.cs - Shared context + events
  • StateMachine/CardAnimator.cs - Reusable animation methods (enlarge, shrink, flip, idle hover, etc.)
  • ProgressBarController.cs - Progress bar UI controller with blink animation

Settings:

  • Core/Settings/CardSystemSettings.cs - ScriptableObject for all card animation timings
  • Core/Settings/ICardSystemSettings.cs - Interface for settings access

State Implementations:

  • States/CardIdleState.cs - Owns CardBackVisual, idle hover, click to flip (with click blocking)
  • States/CardFlippingState.cs - Owns CardBackVisual, flip animation, Legendary shortcut
  • States/CardRevealedState.cs - Owns NewCardIdleBadge + RepeatCardIdleBadge, fires OnCardInteractionComplete
  • States/CardEnlargedNewState.cs - Owns NewCardBadge, tap to shrink
  • States/CardEnlargedRepeatState.cs - Owns RepeatText + ProgressBarUI, auto-upgrade logic
  • States/CardDraggingState.cs - Drag handling for album placement
  • States/CardPlacedInSlotState.cs - In album slot, click to enlarge
  • States/CardAlbumEnlargedState.cs - Enlarged from album, tap to shrink

Prefab Assembly Instructions

Card Prefab Hierarchy:

Card (RectTransform)
├─ CardDisplay (existing prefab)
├─ CardContext (component)
    ├─ FlippingState (CardFlippingState component)
    │   └─ CardBackVisual (Image)
├─ CardAnimator (component)
└─ CardStateMachine (AppleMachine)
    ├─ IdleState (CardIdleState component)
    │   └─ CardBackVisual (Image)
    ├─ RevealedState (CardRevealedState component)
    │   ├─ NewCardIdleBadge (Image/Text - "NEW!")
    │   └─ RepeatCardIdleBadge (Image/Text - "REPEAT")
    │   └─ ProgressBarContainer (GameObject)
    │       └─ ProgressBarUI (prefab instance)
    │   └─ NewCardBadge (Image/Text - "NEW CARD")
    ├─ EnlargedRepeatState (CardEnlargedRepeatState component)
    │   ├─ RepeatText (Image/Text - "REPEAT CARD")
    │   └─ ProgressBarUI (ProgressBarController component + 5 Image children)
    ├─ DraggingState (CardDraggingState component)
ProgressBarUI
├─ GridLayoutGroup (1 column, 5 rows, "Lower Right" corner start)
    ├─ ProgressElement1 (Image)

### **ProgressBarUI Prefab:**

ProgressBarUI (GameObject with ProgressBarController component) └─ ProgressElement5 (Image) ├─ VerticalLayoutGroup (Reverse Arrangement enabled) └─ Children (5 Images, auto-detected): ├─ ProgressElement1 (Image) - First child = 1/5 ├─ ProgressElement2 (Image) ├─ ProgressElement3 (Image) ├─ ProgressElement4 (Image) └─ ProgressElement5 (Image) - Last child = 5/5


### **Component References to Assign:**
**CardContext:**
- cardDisplay → CardDisplay component
- cardAnimator → CardAnimator component

**CardIdleState:**
- cardBackVisual → CardBackVisual child GameObject

**CardFlippingState:**
- cardBackVisual → CardBackVisual child GameObject

**CardRevealedState:**
- progressBarContainer → ProgressBarContainer child GameObject
- progressBar → ProgressBarController component (on ProgressBarUI prefab)
- repeatCardIdleBadge → RepeatCardIdleBadge child GameObject

**CardEnlargedNewState:**
- newCardBadge → NewCardBadge child GameObject

**CardEnlargedRepeatState:**
- progressBar → ProgressBarController component (on ProgressBarUI child GameObject)
- repeatText → RepeatText child GameObject

## Integration with BoosterOpeningPage

```csharp
// When spawning cards:
Card card = Instantiate(cardPrefab);
CardContext context = card.GetComponent<CardContext>();

// Setup card data
context.SetupCard(cardData, isNew: isNewCard, repeatCount: ownedCount);

// All cards start clickable
context.IsClickable = true;

// Subscribe to events
context.OnFlipComplete += OnCardFlipComplete;
context.OnCardDismissed += OnCardDismissed;
context.OnCardInteractionComplete += OnCardInteractionComplete;
context.OnUpgradeTriggered += OnCardUpgraded;

// Start in IdleState
context.StateMachine.ChangeState("IdleState");

// When a card starts flipping, block all others
private void OnCardFlipComplete(CardContext flippingCard)
{
    // Disable all cards to prevent multi-flip
    foreach (CardContext card in _allCards)
    {
        card.IsClickable = false;
    }
}

// Track completion
private void OnCardInteractionComplete(CardContext card)
{
    _cardsCompletedInteraction++;
    
    if (_cardsCompletedInteraction == 3)
    {
        AnimateCardsToAlbum(); // All cards revealed, animate to album
    }
    else
    {
        // Re-enable unflipped cards
        foreach (CardContext c in _allCards)
        {
            if (c.StateMachine.CurrentState.name == "IdleState")
            {
                c.IsClickable = true;
            }
        }
    }
}

Benefits vs Old System

Aspect Old System New System
Components per card FlippableCard + AlbumCard + wrappers 1 Card + states
Animation code duplication ~200 lines across 5 files 0 (shared CardAnimator)
State tracking 12+ boolean flags (_isFlipped, _isFlipping, _isWaitingForTap, etc.) 1 active state name
Visual element management Manual SetActive() in 8+ places Automatic via state activation
Adding new behaviors Modify 3-4 components + events Add 1 new state GameObject
Prefab nesting FlippableCard → AlbumCard → CardDisplay (5 layers) Card → States (flat hierarchy)
Debugging state Check 12 booleans across files Look at active state name in inspector
Progress bar logic 50 lines in FlippableCard.ShowProgressBar() Isolated in ProgressBarController
Upgrade logic TriggerUpgradeTransition (80 lines in FlippableCard) TriggerUpgrade (isolated in CardEnlargedRepeatState)
Event coordination 4 events on FlippableCard, 2 on AlbumCard 4 events on CardContext (centralized)

Testing Checklist

  • Booster opening: NEW card shows badge → tap → shrinks → shows NEW idle badge
  • Booster opening: REPEAT card (2/5) shows REPEAT text + progress → blink → tap → shrinks → shows REPEAT idle badge
  • Booster opening: REPEAT card (5/5) auto-upgrades → shows NEW at higher rarity
  • Booster opening: Legendary repeat skips enlarge
  • Booster opening: Click blocking prevents multi-flip
  • Booster opening: All 3 cards complete → animate to album
  • Album placement: Card in corner → click → reveals → drag → place in slot
  • Album placement: Card in slot → click → enlarges → tap → shrinks back
  • Cascading upgrades (Common → Uncommon → Rare in one reveal)
  • Progress bar shows correctly (1/5, 2/5, 3/5, 4/5, 5/5)
  • Progress bar blinks newest element
  • Idle hover animation works in both flows
  • Hover scale works on pointer enter/exit

Integration Work Remaining

  1. Update BoosterOpeningPage to use new Card prefab instead of FlippableCard
  2. Update AlbumViewPage to use new Card prefab instead of AlbumCard
  3. Migrate album placement drag/drop to use DraggingState
  4. Remove old FlippableCard.cs and AlbumCard.cs after migration
  5. (Optional) Add Jiggle() animation to CardAnimator for clicking inactive cards

Migration Path

Phase 1: Side-by-side (Current)

  • New state machine exists alongside old FlippableCard/AlbumCard
  • Can test new system without breaking existing functionality

Phase 2: Booster Opening Migration

  • Update BoosterOpeningPage to spawn new Card prefab
  • Remove FlippableCard references
  • Test all booster flows

Phase 3: Album Migration

  • Update AlbumViewPage to spawn new Card prefab
  • Remove AlbumCard references
  • Test album placement and enlarge

Phase 4: Cleanup

  • Delete FlippableCard.cs
  • Delete AlbumCard.cs
  • Delete old wrapper components
  • Clean up unused prefab variants

Example: Adding New Card Behavior

Scenario: Add a "Trading" state where card shows trade UI?

Old system:

  1. Modify FlippableCard.cs (add boolean, methods, events)
  2. Modify AlbumCard.cs (add pass-through logic)
  3. Update 3-4 wrapper components
  4. Add new events and subscriptions
  5. Manually manage trade UI visibility

New system:

  1. Create CardTradingState.cs:
public class CardTradingState : AppleState
{
    [SerializeField] private GameObject tradeUI;
    
    public override void OnEnterState()
    {
        // tradeUI automatically activates with state!
    }
}
  1. Add TradingState GameObject under CardStateMachine
  2. Add trade UI as child of TradingState
  3. Call ChangeState("TradingState") from wherever needed

Done! Zero other files modified.


Summary

The new state machine implementation successfully replicates all core FlippableCard/AlbumCard functionality with:

  • Cleaner architecture (state pattern vs boolean soup)
  • Less code duplication (shared CardAnimator)
  • Easier debugging (visible state names)
  • Simpler extension (add states vs modify monoliths)
  • Better separation of concerns (each state owns its visuals)

Status: Core implementation complete, ready for prefab assembly and integration testing.