Stash work

This commit is contained in:
Michal Pikulski
2025-11-17 08:39:41 +01:00
parent 78aafb9275
commit 7aca1a17ac
13 changed files with 792 additions and 1845 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -44,6 +44,8 @@ namespace UI.CardSystem
private Input.InputMode _previousInputMode; private Input.InputMode _previousInputMode;
private List<StateMachine.Card> _activeCards = new List<StateMachine.Card>(); private List<StateMachine.Card> _activeCards = new List<StateMachine.Card>();
private const int MAX_VISIBLE_CARDS = 3; private const int MAX_VISIBLE_CARDS = 3;
private List<StateMachine.Card> _pendingCornerCards = new List<StateMachine.Card>();
private const int MAX_PENDING_CORNER = 3;
internal override void OnManagedStart() internal override void OnManagedStart()
{ {
@@ -679,5 +681,87 @@ namespace UI.CardSystem
} }
return null; return null;
} }
public void SpawnPendingCornerCards()
{
if (cardPrefab == null || bottomRightSlots == null) return;
CleanupPendingCornerCards();
for (int i = 0; i < MAX_PENDING_CORNER; i++)
{
var slot = FindSlotByIndex(i);
if (slot == null) break;
GameObject cardObj = Instantiate(cardPrefab, bottomRightSlots.transform);
var card = cardObj.GetComponent<StateMachine.Card>();
if (card != null)
{
card.SetupForAlbumPending();
card.AssignToSlot(slot, true);
_pendingCornerCards.Add(card);
}
else
{
Destroy(cardObj);
}
}
}
private void CleanupPendingCornerCards()
{
foreach (var c in _pendingCornerCards)
{
if (c != null) Destroy(c.gameObject);
}
_pendingCornerCards.Clear();
}
public void HandlePendingCardDragStart(StateMachine.Card cornerCard)
{
// Select smart pending card data
var selected = SelectSmartPendingCard();
if (selected == null)
{
return; // no pending data
}
cornerCard.Context.SetupCard(selected);
// Navigate album to page
int targetPage = FindPageForCard(selected);
if (targetPage >= 0)
{
NavigateToAlbumPage(targetPage);
}
// Begin flip state
cornerCard.ChangeState("FlippingPendingState");
}
private CardData SelectSmartPendingCard()
{
if (CardSystemManager.Instance == null) return null;
var pending = CardSystemManager.Instance.GetPendingRevealCards();
if (pending.Count == 0) return null;
// Try current page match
var pageDefs = GetDefinitionsOnCurrentPage();
var match = pending.Find(c => pageDefs.Contains(c.DefinitionId));
if (match != null) return match;
// Fallback random
int idx = Random.Range(0, pending.Count);
return pending[idx];
}
private List<string> GetDefinitionsOnCurrentPage()
{
// Placeholder: gather from slots on current page
return new List<string>();
}
private int FindPageForCard(CardData data)
{
// Placeholder: map definition to page index
return -1;
}
private void NavigateToAlbumPage(int pageIndex)
{
// Placeholder: call book/page flip controller
}
} }
} }

View File

@@ -0,0 +1,23 @@
using UnityEngine;
using UnityEngine.UI;
namespace UI.CardSystem
{
/// <summary>
/// Simple component representing card back visuals; toggle visibility.
/// </summary>
public class CardBack : MonoBehaviour
{
[SerializeField] private Image backImage;
public void Show()
{
gameObject.SetActive(true);
}
public void Hide()
{
gameObject.SetActive(false);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 37d815ba7b02481786cc1953678a3e8e
timeCreated: 1763322207

View File

@@ -56,39 +56,65 @@ namespace UI.CardSystem.StateMachine
protected override void OnDragStartedHook() protected override void OnDragStartedHook()
{ {
base.OnDragStartedHook(); base.OnDragStartedHook();
string current = GetCurrentStateName();
// Transition to dragging state when drag begins if (current == "PendingFaceDownState")
{
// Notify AlbumViewPage to assign data & flip
var albumPage = FindObjectOfType<AlbumViewPage>();
if (albumPage != null)
{
albumPage.HandlePendingCardDragStart(this);
}
// State change will be triggered by album page after data assignment
}
else
{
Logging.Debug($"[Card] Drag started on {CardData?.Name}, transitioning to DraggingState"); Logging.Debug($"[Card] Drag started on {CardData?.Name}, transitioning to DraggingState");
ChangeState("DraggingState"); ChangeState("DraggingState");
} }
}
protected override void OnDragEndedHook() protected override void OnDragEndedHook()
{ {
base.OnDragEndedHook(); base.OnDragEndedHook();
string current = GetCurrentStateName();
// Check if we dropped in a valid album slot if (current == "DraggingState")
{
// Existing logic
if (CurrentSlot is AlbumCardSlot albumSlot) if (CurrentSlot is AlbumCardSlot albumSlot)
{ {
Logging.Debug($"[Card] Dropped in album slot, transitioning to PlacedInSlotState"); Logging.Debug($"[Card] Dropped in album slot, transitioning to PlacedInSlotState");
// Set the parent slot on PlacedInSlotState
var placedState = GetStateComponent<States.CardPlacedInSlotState>("PlacedInSlotState"); var placedState = GetStateComponent<States.CardPlacedInSlotState>("PlacedInSlotState");
if (placedState != null) if (placedState != null)
{ {
placedState.SetParentSlot(albumSlot); placedState.SetParentSlot(albumSlot);
} }
ChangeState("PlacedInSlotState"); ChangeState("PlacedInSlotState");
// Notify listeners (AlbumViewPage) that this pending card was placed
OnPlacedInAlbumSlot?.Invoke(this, albumSlot); OnPlacedInAlbumSlot?.Invoke(this, albumSlot);
} }
else else
{ {
Logging.Debug($"[Card] Dropped outside valid slot, returning to RevealedState"); Logging.Debug("[Card] Dropped outside valid slot, returning to RevealedState");
ChangeState("RevealedState"); ChangeState("RevealedState");
} }
} }
else if (current == "DraggingRevealedState")
{
// Pending revealed drag state end
if (CurrentSlot is AlbumCardSlot albumSlot)
{
var placedState = GetStateComponent<States.CardPlacedInSlotState>("PlacedInSlotState");
if (placedState != null) placedState.SetParentSlot(albumSlot);
ChangeState("PlacedInSlotState");
OnPlacedInAlbumSlot?.Invoke(this, albumSlot);
}
else
{
// Return to corner face-down
ChangeState("PendingFaceDownState");
}
}
}
#endregion #endregion
@@ -148,6 +174,17 @@ namespace UI.CardSystem.StateMachine
} }
} }
/// <summary>
/// Setup for album pending state (starts at PendingFaceDownState)
/// Dragging is ENABLED; state will assign data when dragged
/// </summary>
public void SetupForAlbumPending()
{
// Start with no data; state will assign when dragged
SetupCard(null, "PendingFaceDownState");
SetDraggingEnabled(true);
}
/// <summary> /// <summary>
/// Transition to a specific state /// Transition to a specific state
/// </summary> /// </summary>

View File

@@ -0,0 +1,39 @@
using Core.SaveLoad;
using UnityEngine;
namespace UI.CardSystem.StateMachine.States
{
/// <summary>
/// Dragging revealed state for pending cards after flip; minimal overlay of CardDraggingState but no badges.
/// </summary>
public class CardDraggingRevealedState : AppleState
{
private CardContext context;
private Vector3 originalScale;
private void Awake()
{
context = GetComponentInParent<CardContext>();
}
public override void OnEnterState()
{
if (context == null) return;
if (context.CardDisplay != null)
{
context.CardDisplay.gameObject.SetActive(true);
context.CardDisplay.transform.localRotation = Quaternion.Euler(0,0,0);
}
originalScale = context.RootTransform.localScale;
context.RootTransform.localScale = originalScale * 1.15f;
}
private void OnDisable()
{
if (context?.RootTransform != null)
{
context.RootTransform.localScale = originalScale;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ce2483293cdd4680b5095afc1fcb2fde
timeCreated: 1763322199

View File

@@ -0,0 +1,57 @@
using Core;
using Core.SaveLoad;
using UnityEngine;
namespace UI.CardSystem.StateMachine.States
{
/// <summary>
/// Handles flipping a pending face-down card after data assignment.
/// Transitions to DraggingRevealedState when flip completes.
/// </summary>
public class CardFlippingPendingState : AppleState
{
private CardContext context;
private GameObject cardBack;
private void Awake()
{
context = GetComponentInParent<CardContext>();
if (context != null)
{
var backTransform = context.RootTransform.Find("CardBack");
if (backTransform != null) cardBack = backTransform.gameObject;
}
}
public override void OnEnterState()
{
if (context == null) return;
// Ensure card back visible and front hidden at start
if (cardBack != null) cardBack.SetActive(true);
if (context.CardDisplay != null) context.CardDisplay.gameObject.SetActive(false);
// Optional: album navigation
var albumPage = Object.FindObjectOfType<AlbumViewPage>();
if (albumPage != null && context.CardData != null)
{
int targetPage = albumPage.FindPageForCard(context.CardData); // placeholder; method may be private
}
if (context.Animator != null)
{
Transform back = cardBack != null ? cardBack.transform : null;
Transform front = context.CardDisplay != null ? context.CardDisplay.transform : null;
context.Animator.PlayFlip(back, front, onComplete: () =>
{
context.StateMachine.ChangeState("DraggingRevealedState");
});
}
else
{
if (cardBack != null) cardBack.SetActive(false);
if (context.CardDisplay != null) context.CardDisplay.gameObject.SetActive(true);
context.StateMachine.ChangeState("DraggingRevealedState");
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: edffabfce37d42ceac2194c23470acab
timeCreated: 1763322190

View File

@@ -0,0 +1,39 @@
using Core.SaveLoad;
using UnityEngine;
namespace UI.CardSystem.StateMachine.States
{
/// <summary>
/// Card is in pending face-down state in corner, awaiting drag.
/// Front hidden, back visible (assumes CardBack child exists).
/// </summary>
public class CardPendingFaceDownState : AppleState
{
private CardContext context;
private GameObject cardBack;
private void Awake()
{
context = GetComponentInParent<CardContext>();
if (context != null)
{
var backTransform = context.RootTransform.Find("CardBack");
if (backTransform != null) cardBack = backTransform.gameObject;
}
}
public override void OnEnterState()
{
if (context == null) return;
// Hide front
if (context.CardDisplay != null)
{
context.CardDisplay.gameObject.SetActive(false);
}
// Show back
if (cardBack != null) cardBack.SetActive(true);
// Scale
context.RootTransform.localScale = context.OriginalScale * 0.8f;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6fab9d595905435b82253cd4d1bf49de
timeCreated: 1763322180

View File

@@ -0,0 +1,460 @@
# 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:
```csharp
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:
```csharp
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:
```csharp
// In Card.cs
public event Action<Card> OnDragStartedEvent;
private void OnDragStarted()
{
OnDragStartedEvent?.Invoke(this);
}
```
---
## 4. AlbumViewPage Modifications
### Spawn Pending Cards (Face-Down):
```csharp
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):
```csharp
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:
```csharp
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
```csharp
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
```csharp
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
```csharp
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
```csharp
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! 🎉