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)
- 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
-
[When all cards complete] → Fires OnCardInteractionComplete → Waits for all 3 cards to finish
-
[When all cards complete] → BoosterOpeningPage animates cards to album → Destroy
### **Album Placement Flow:**
- IdleState (card back in corner, hover enabled) ↓ [player clicks]
- FlippingState (reveals which card it is) → OnFlipComplete ↓
- RevealedState → OnCardInteractionComplete ↓ [player drags]
- DraggingState (scaled up during drag) ↓ [drop in slot]
- PlacedInSlotState (in album permanently) ↓ [player clicks]
- AlbumEnlargedState → Fires OnEnlargeRequested (page shows backdrop, reparents) ↓ [player taps] → Fires OnShrinkRequested (page prepares) → Shrink animation ↓
- 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 + eventsStateMachine/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 timingsCore/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 shortcutStates/CardRevealedState.cs- Owns NewCardIdleBadge + RepeatCardIdleBadge, fires OnCardInteractionCompleteStates/CardEnlargedNewState.cs- Owns NewCardBadge, tap to shrinkStates/CardEnlargedRepeatState.cs- Owns RepeatText + ProgressBarUI, auto-upgrade logicStates/CardDraggingState.cs- Drag handling for album placementStates/CardPlacedInSlotState.cs- In album slot, click to enlargeStates/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
- Update BoosterOpeningPage to use new Card prefab instead of FlippableCard
- Update AlbumViewPage to use new Card prefab instead of AlbumCard
- Migrate album placement drag/drop to use DraggingState
- Remove old FlippableCard.cs and AlbumCard.cs after migration
- (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:
- Modify FlippableCard.cs (add boolean, methods, events)
- Modify AlbumCard.cs (add pass-through logic)
- Update 3-4 wrapper components
- Add new events and subscriptions
- Manually manage trade UI visibility
New system:
- Create
CardTradingState.cs:
public class CardTradingState : AppleState
{
[SerializeField] private GameObject tradeUI;
public override void OnEnterState()
{
// tradeUI automatically activates with state!
}
}
- Add TradingState GameObject under CardStateMachine
- Add trade UI as child of TradingState
- 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.