Files
AppleHillsProduction/docs/cards_wip/card_system_implementation_summary.md

409 lines
14 KiB
Markdown
Raw Normal View History

2025-11-12 11:58:01 +01:00
# Card State Machine Implementation Summary
2025-11-11 21:03:05 +01:00
## Architecture Overview
**Isolated State Pattern** using Pixelplacement StateMachine:
```
Card (RectTransform - primary animation target)
2025-11-12 11:58:01 +01:00
├─ CardDisplay (always visible - shows card front)
├─ CardContext (shared references + events)
2025-11-11 21:03:05 +01:00
├─ CardAnimator (reusable animations)
└─ CardStateMachine (AppleMachine)
├─ IdleState/
2025-11-12 11:58:01 +01:00
│ └─ CardBackVisual ← State owns this
2025-11-11 21:03:05 +01:00
├─ FlippingState/
│ └─ CardBackVisual ← State owns this
2025-11-12 11:58:01 +01:00
├─ RevealedState/
│ ├─ NewCardIdleBadge ← State owns this
│ └─ RepeatCardIdleBadge ← State owns this
2025-11-11 21:03:05 +01:00
├─ EnlargedNewState/
│ └─ NewCardBadge ← State owns this
2025-11-12 11:58:01 +01:00
├─ 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
5. [When all cards complete]
→ Fires OnCardInteractionComplete
→ Waits for all 3 cards to finish
4. [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
5. PlacedInSlotState (back in slot)
2025-11-11 21:03:05 +01:00
```
## 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
2025-11-12 11:58:01 +01:00
### 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();
}
```
2025-11-11 21:03:05 +01:00
### 4. State Transitions
States explicitly transition via `_context.StateMachine.ChangeState("StateName")`
2025-11-12 11:58:01 +01:00
Example:
```csharp
// In CardFlippingState.OnFlipComplete():
if (_context.IsNewCard)
_context.StateMachine.ChangeState("EnlargedNewState");
else if (_context.RepeatCardCount > 0)
_context.StateMachine.ChangeState("EnlargedRepeatState");
2025-11-11 21:03:05 +01:00
```
2025-11-12 11:58:01 +01:00
### 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:**
```csharp
progressBar.ShowProgress(currentCount, maxCount, OnProgressComplete);
2025-11-11 21:03:05 +01:00
```
## Files Created
2025-11-12 11:58:01 +01:00
### **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
2025-11-11 21:03:05 +01:00
2025-11-12 11:58:01 +01:00
**CardRevealedState:**
- progressBarContainer → ProgressBarContainer child GameObject
- progressBar → ProgressBarController component (on ProgressBarUI prefab)
- repeatCardIdleBadge → RepeatCardIdleBadge child GameObject
2025-11-11 21:03:05 +01:00
2025-11-12 11:58:01 +01:00
**CardEnlargedNewState:**
- newCardBadge → NewCardBadge child GameObject
2025-11-11 21:03:05 +01:00
2025-11-12 11:58:01 +01:00
**CardEnlargedRepeatState:**
- progressBar → ProgressBarController component (on ProgressBarUI child GameObject)
- repeatText → RepeatText child GameObject
2025-11-11 21:03:05 +01:00
2025-11-12 11:58:01 +01:00
## Integration with BoosterOpeningPage
2025-11-11 21:03:05 +01:00
2025-11-12 11:58:01 +01:00
```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;
}
}
}
}
```
2025-11-11 21:03:05 +01:00
## Benefits vs Old System
| Aspect | Old System | New System |
|--------|-----------|------------|
2025-11-12 11:58:01 +01:00
| 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
2025-11-11 21:03:05 +01:00
2025-11-12 11:58:01 +01:00
## Migration Path
2025-11-11 21:03:05 +01:00
2025-11-12 11:58:01 +01:00
**Phase 1: Side-by-side (Current)**
- New state machine exists alongside old FlippableCard/AlbumCard
- Can test new system without breaking existing functionality
2025-11-11 21:03:05 +01:00
2025-11-12 11:58:01 +01:00
**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
2025-11-11 21:03:05 +01:00
**New system:**
2025-11-12 11:58:01 +01:00
1. Create `CardTradingState.cs`:
```csharp
public class CardTradingState : AppleState
{
[SerializeField] private GameObject tradeUI;
public override void OnEnterState()
{
// tradeUI automatically activates with state!
}
}
```
2. Add TradingState GameObject under CardStateMachine
2025-11-11 21:03:05 +01:00
3. Add trade UI as child of TradingState
2025-11-12 11:58:01 +01:00
4. 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.**
2025-11-11 21:03:05 +01:00