Files
AppleHillsProduction/docs/album_placement_flow_proposal.md
Michal Pikulski 7aca1a17ac Stash work
2025-11-17 08:39:41 +01:00

12 KiB

Album Card Placement Flow - Refactored Design

Current State Analysis

Existing Flow (Pre-Refactor):

  1. Pending cards spawn face-up in bottom-right slots
  2. User drags card to album slot
  3. Card placement triggers inventory move
  4. Next card spawns

Problems:

  • Cards spawn face-up (should be face-down)
  • No "smart selection" from pending queue based on current album page
  • No auto-flip to correct album page when card is picked up
  • States don't support "hold to reveal" behavior

Proposed New Flow

Visual Journey:

[Face-Down Card in Corner] 
    ↓ (user holds/drags)
[Card Flips to Reveal] + [Album auto-flips to correct page]
    ↓ (user drags over album)
[Card hovers over slot]
    ↓ (user releases)
[Card snaps to slot] → [Revealed State in slot]

Technical Implementation:


1. New Card States

A. CardPendingFaceDownState

Purpose: Initial state for cards in pending corner slots Visuals:

  • Card back visible (card front hidden)
  • Small scale (to fit in corner slot)
  • Idle in corner

Behavior:

  • Does NOT respond to clicks
  • Responds to drag start (OnDragStarted)
  • On drag start → trigger smart card selection + flip animation

Transitions:

  • OnDragStarted → CardFlippingPendingState

B. CardFlippingPendingState

Purpose: Transition state while card flips and album navigates Visuals:

  • Flip animation (card back → card front)
  • Card follows cursor during flip

Behavior:

  • Play flip animation (uses CardAnimator.PlayFlip)
  • Emit event to AlbumViewPage to navigate to card's page
  • Wait for flip animation complete

Transitions:

  • OnFlipComplete → CardDraggingRevealedState

C. CardDraggingRevealedState

Purpose: Card is revealed and being dragged around album Visuals:

  • Card front visible
  • No badges (clean revealed state)
  • Follow cursor/drag position
  • Slight scale-up while dragging

Behavior:

  • Respond to drag position updates
  • Detect when hovering over valid AlbumCardSlot
  • Visual feedback when over valid slot
  • On drag end → snap to slot if valid, otherwise return to corner

Transitions:

  • OnDragEnd (over valid slot) → slot's PlacedInSlotState
  • OnDragEnd (invalid) → CardPendingFaceDownState (return to corner, flip back)

2. Smart Card Selection System

AlbumViewPage Responsibilities:

public class AlbumViewPage
{
    private List<CardData> _pendingQueue; // All pending cards
    private List<Card> _cornerCards; // 3 face-down card GameObjects in corner
    private int _currentAlbumPageIndex;
    
    /// <summary>
    /// When user starts dragging ANY corner card, we pick which pending card to reveal
    /// </summary>
    private void OnCornerCardDragStarted(Card cornerCard)
    {
        // 1. Get current album page's expected cards
        var currentPageSlots = GetSlotsOnCurrentPage();
        var currentPageDefinitions = currentPageSlots
            .Select(slot => slot.TargetCardDefinition)
            .ToList();
        
        // 2. Try to find a pending card that belongs on this page
        CardData selectedCard = _pendingQueue.FirstOrDefault(card => 
            currentPageDefinitions.Any(def => def.Id == card.DefinitionId && def.Rarity == card.Rarity)
        );
        
        // 3. If none on current page, pick random pending
        if (selectedCard == null)
        {
            selectedCard = _pendingQueue[Random.Range(0, _pendingQueue.Count)];
            
            // Navigate album to the page where this card belongs
            int targetPage = FindPageForCard(selectedCard);
            NavigateToPage(targetPage);
        }
        
        // 4. Assign the selected card data to the corner card being dragged
        cornerCard.Context.SetupCard(selectedCard);
        
        // 5. Trigger flip (handled by state)
        cornerCard.Context.StateMachine.ChangeState("FlippingPendingState");
    }
}

3. Card.cs Extensions

New Setup Method:

public class Card
{
    /// <summary>
    /// Setup for album pending placement (starts face-down in corner)
    /// </summary>
    public void SetupForAlbumPending()
    {
        // Start with NO card data (will be assigned on drag)
        SetupCard(null, "PendingFaceDownState");
        SetDraggingEnabled(true); // Enable drag immediately
    }
}

Drag Event Routing:

// In Card.cs
public event Action<Card> OnDragStartedEvent;

private void OnDragStarted()
{
    OnDragStartedEvent?.Invoke(this);
}

4. AlbumViewPage Modifications

Spawn Pending Cards (Face-Down):

private void SpawnPendingCards()
{
    // Spawn up to 3 "blank" face-down cards in corner
    for (int i = 0; i < MAX_VISIBLE_CARDS; i++)
    {
        GameObject cardObj = Instantiate(cardPrefab, bottomRightSlots.transform);
        var card = cardObj.GetComponent<StateMachine.Card>();
        
        if (card != null)
        {
            // Setup as pending (no data yet, face-down)
            card.SetupForAlbumPending();
            
            // Subscribe to drag start
            card.OnDragStartedEvent += OnCornerCardDragStarted;
            
            // Assign to corner slot
            DraggableSlot slot = FindSlotByIndex(i);
            card.AssignToSlot(slot, true);
            
            _cornerCards.Add(card);
        }
    }
}

Handle Drag Start (Smart Selection):

private void OnCornerCardDragStarted(Card cornerCard)
{
    if (_pendingQueue.Count == 0) return;
    
    // Smart selection logic (from section 2)
    CardData selectedCard = SelectSmartPendingCard();
    
    // Assign data to the dragged corner card
    cornerCard.Context.SetupCard(selectedCard);
    
    // State transition to flipping (handled by state machine)
    // FlippingPendingState will trigger flip animation + album navigation
}

Navigate to Card's Page:

public void NavigateToCardPage(CardData card)
{
    int targetPage = FindPageForCard(card);
    if (targetPage != _currentAlbumPageIndex)
    {
        // Trigger book page flip animation
        bookController.FlipToPage(targetPage);
    }
}

5. State Implementation Details

CardPendingFaceDownState.cs

public class CardPendingFaceDownState : AppleState
{
    private CardContext _context;
    
    public override void OnEnterState()
    {
        // Show card back, hide card front
        if (_context.CardDisplay != null)
        {
            _context.CardDisplay.gameObject.SetActive(false); // Hide front
        }
        
        var cardBack = GetComponentInChildren<CardBack>(); // Assumes CardBack component exists
        if (cardBack != null)
        {
            cardBack.gameObject.SetActive(true);
        }
        
        // Small scale for corner slot
        _context.RootTransform.localScale = Vector3.one * 0.8f;
    }
}

CardFlippingPendingState.cs

public class CardFlippingPendingState : AppleState
{
    private CardContext _context;
    
    public override void OnEnterState()
    {
        // Notify album page to navigate
        var albumPage = FindObjectOfType<AlbumViewPage>();
        if (albumPage != null)
        {
            albumPage.NavigateToCardPage(_context.CardData);
        }
        
        // Play flip animation
        if (_context.Animator != null)
        {
            _context.Animator.PlayFlip(
                startRotation: Quaternion.Euler(0, 180, 0), // back facing
                endRotation: Quaternion.identity,           // front facing
                onComplete: OnFlipComplete
            );
        }
    }
    
    private void OnFlipComplete()
    {
        _context.StateMachine.ChangeState("DraggingRevealedState");
    }
}

CardDraggingRevealedState.cs

public class CardDraggingRevealedState : AppleState
{
    private CardContext _context;
    private AlbumCardSlot _hoveredSlot;
    
    public override void OnEnterState()
    {
        // Card front visible, clean revealed (no badges)
        if (_context.CardDisplay != null)
        {
            _context.CardDisplay.gameObject.SetActive(true);
        }
        
        // Slightly larger while dragging
        _context.Animator.PlayEnlarge(1.2f);
    }
    
    void Update()
    {
        // Detect hover over valid album slots
        _hoveredSlot = DetectValidSlotUnderCursor();
        
        if (_hoveredSlot != null)
        {
            // Visual feedback: highlight slot or card
        }
    }
    
    public void OnDragEnded()
    {
        if (_hoveredSlot != null && _hoveredSlot.CanAcceptCard(_context.CardData))
        {
            // Snap to slot and transition to PlacedInSlotState
            SnapToSlot(_hoveredSlot);
        }
        else
        {
            // Return to corner, flip back to face-down
            ReturnToCorner();
        }
    }
}

6. Required New Components

CardBack Component

public class CardBack : MonoBehaviour
{
    [SerializeField] private Image backImage;
    
    public void Show() => gameObject.SetActive(true);
    public void Hide() => gameObject.SetActive(false);
}

Attach to Card prefab as a sibling to CardDisplay.


7. Prefab Structure

Card (GameObject)
├── StateMachine (AppleMachine)
│   ├── PendingFaceDownState
│   ├── FlippingPendingState
│   ├── DraggingRevealedState
│   ├── PlacedInSlotState (existing)
│   └── ... (other states)
├── CardContext
├── CardAnimator
├── CardDisplay (front visuals)
├── CardBack (back visuals - NEW)
└── DraggableObject

8. Migration Steps

Step 1: Create New States

  • CardPendingFaceDownState.cs
  • CardFlippingPendingState.cs
  • CardDraggingRevealedState.cs

Step 2: Add CardBack Component

  • Create CardBack.cs script
  • Add CardBack GameObject to Card prefab
  • Design card back visual (sprite, frame, etc.)

Step 3: Update Card.cs

  • Add SetupForAlbumPending() method
  • Add OnDragStartedEvent
  • Wire drag events to state machine

Step 4: Update AlbumViewPage

  • Modify SpawnPendingCards() to spawn face-down
  • Implement smart selection logic
  • Add NavigateToCardPage() method
  • Connect to book flip controller

Step 5: Update CardAnimator

  • Ensure PlayFlip() can handle arbitrary start/end rotations
  • Add any needed drag-follow animation helpers

Step 6: Testing

  • Test corner card drag → flip → album navigation
  • Test smart selection (page match prioritization)
  • Test return-to-corner on invalid drop
  • Test snap-to-slot on valid drop
  • Test multiple cards in queue

9. Edge Cases & Considerations

No Pending Cards

  • Don't spawn corner cards if pending queue is empty
  • Hide corner slots when no cards to place

Album Page Navigation During Drag

  • Lock page flipping while dragging (prevent user manual flip)
  • Queue navigation if flip animation in progress

Multiple Cards Dragged Simultaneously

  • Only allow one card to be in FlippingPending/DraggingRevealed at a time
  • Disable other corner cards while one is being dragged

Card Returns to Corner

  • Flip back animation (reverse of reveal)
  • Re-enter PendingFaceDownState
  • Unassign CardData (become "blank" again for next drag)

Invalid Slot Drop

  • Visual feedback (shake, red highlight)
  • Smooth return animation to corner

10. Benefits of This Approach

Consistent State Architecture - Uses same state machine pattern as booster flow
Smart UX - Auto-navigation to correct album page
Clean Separation - States handle visuals/behavior, page handles logic
Reusable - States can be reused for other card flows
Extensible - Easy to add new behaviors (e.g., card preview on hover)
Testable - Each state can be tested independently


Open Questions for Approval

  1. Card Back Design: Should we use a generic back for all cards, or rarity-specific backs?
  2. Navigation Timing: Should album flip happen instantly or animated during card flip?
  3. Return Animation: Fast snap-back or gentle float-back when invalid drop?
  4. Multiple Rarities: If pending queue has same card at multiple rarities, which to prioritize?
  5. Corner Slot Count: Keep at 3, or make configurable?

Ready to implement once approved! 🎉