Stash factoring out functionality out of album view page

This commit is contained in:
Michal Pikulski
2025-11-18 09:20:00 +01:00
parent 64c304bb6d
commit 034654c308
10 changed files with 766 additions and 2664 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,172 @@
using System.Collections.Generic;
using AppleHills.Data.CardSystem;
using Core;
using UnityEngine;
namespace UI.CardSystem
{
/// <summary>
/// Manages album page navigation, zone mapping, and page flip tracking.
/// Created and owned by AlbumViewPage (not a Unity component).
/// </summary>
public class AlbumNavigationService
{
private readonly BookCurlPro.BookPro _book;
private readonly BookTabButton[] _zoneTabs;
private bool _isPageFlipping = false;
/// <summary>
/// Is the book currently flipping to a page?
/// </summary>
public bool IsPageFlipping => _isPageFlipping;
/// <summary>
/// Constructor - called by AlbumViewPage's lazy property
/// </summary>
public AlbumNavigationService(BookCurlPro.BookPro book, BookTabButton[] zoneTabs)
{
_book = book;
_zoneTabs = zoneTabs;
}
/// <summary>
/// Check if we're currently viewing the album proper (not the menu page)
/// </summary>
public bool IsInAlbumProper()
{
if (_book == null)
{
Logging.Warning("[AlbumNavigationService] Book reference is null");
return false;
}
// Page 1 is the menu/cover, page 2+ are album pages with card slots
return _book.CurrentPaper > 1;
}
/// <summary>
/// Navigate to the page where a specific card belongs
/// </summary>
public void NavigateToCardPage(CardData cardData, System.Action onComplete)
{
if (cardData == null || _book == null)
{
onComplete?.Invoke();
return;
}
// Find target page based on card's zone
int targetPage = FindPageForZone(cardData.Zone);
if (targetPage < 0)
{
Logging.Warning($"[AlbumNavigationService] No page found for zone {cardData.Zone}");
onComplete?.Invoke();
return;
}
// Mark as flipping
_isPageFlipping = true;
Logging.Debug($"[AlbumNavigationService] Starting page flip to page {targetPage}");
// Get or add AutoFlip component
BookCurlPro.AutoFlip autoFlip = _book.GetComponent<BookCurlPro.AutoFlip>();
if (autoFlip == null)
{
autoFlip = _book.gameObject.AddComponent<BookCurlPro.AutoFlip>();
}
// Start flipping with callback
autoFlip.enabled = true;
autoFlip.StartFlipping(targetPage, () =>
{
// Mark as complete
_isPageFlipping = false;
Logging.Debug($"[AlbumNavigationService] Page flip to {targetPage} completed");
// Call original callback if provided
onComplete?.Invoke();
});
}
/// <summary>
/// Get list of card definition IDs on the current page
/// </summary>
public List<string> GetDefinitionsOnCurrentPage()
{
var result = new List<string>();
if (_book == null) return result;
int currentPage = _book.CurrentPaper;
// Find all AlbumCardSlot in scene
var allSlots = Object.FindObjectsByType<AlbumCardSlot>(FindObjectsSortMode.None);
foreach (var slot in allSlots)
{
if (IsSlotOnPage(slot.transform, currentPage))
{
if (slot.TargetCardDefinition != null && !string.IsNullOrEmpty(slot.TargetCardDefinition.Id))
{
result.Add(slot.TargetCardDefinition.Id);
}
}
}
return result;
}
#region Private Helpers
/// <summary>
/// Find the target page for a card zone using BookTabButtons
/// </summary>
private int FindPageForZone(CardZone zone)
{
if (_zoneTabs == null || _zoneTabs.Length == 0)
{
Logging.Warning("[AlbumNavigationService] No zone tabs available!");
return -1;
}
foreach (var tab in _zoneTabs)
{
if (tab.Zone == zone)
{
return tab.TargetPage;
}
}
Logging.Warning($"[AlbumNavigationService] No BookTabButton found for zone {zone}");
return -1;
}
/// <summary>
/// Check if a slot's transform is on a specific book page
/// </summary>
private bool IsSlotOnPage(Transform slotTransform, int pageIndex)
{
if (_book == null || _book.papers == null || pageIndex < 0 || pageIndex >= _book.papers.Length)
return false;
var paper = _book.papers[pageIndex];
if (paper == null) return false;
// Check if slotTransform parent hierarchy contains paper.Front or paper.Back
Transform current = slotTransform;
while (current != null)
{
if ((paper.Front != null && current.gameObject == paper.Front) ||
(paper.Back != null && current.gameObject == paper.Back))
return true;
current = current.parent;
}
return false;
}
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5dce94e276b8456996d9ddacef25c744
timeCreated: 1763425777

View File

@@ -40,17 +40,32 @@ namespace UI.CardSystem
[SerializeField] private BoosterOpeningPage boosterOpeningPage; [SerializeField] private BoosterOpeningPage boosterOpeningPage;
private Input.InputMode _previousInputMode; private Input.InputMode _previousInputMode;
private List<StateMachine.Card> _pendingCornerCards = new List<StateMachine.Card>();
private const int MaxPendingCorner = 3;
// Page flip tracking (for card placement coordination) // Controllers: Lazy-initialized services (auto-created on first use)
private bool _isPageFlipping = false; private CornerCardManager _cornerCardManager;
private CornerCardManager CornerCards => _cornerCardManager ??= new CornerCardManager(
bottomRightSlots,
cardPrefab,
this
);
private AlbumNavigationService _navigationService;
private AlbumNavigationService Navigation => _navigationService ??= new AlbumNavigationService(
book,
_zoneTabs
);
private CardEnlargeController _enlargeController;
private CardEnlargeController Enlarge => _enlargeController ??= new CardEnlargeController(
cardEnlargedBackdrop,
cardEnlargedContainer
);
/// <summary> /// <summary>
/// Query method: Check if the book is currently flipping to a page. /// Query method: Check if the book is currently flipping to a page.
/// Used by card states to know if they should wait before placing. /// Used by card states to know if they should wait before placing.
/// </summary> /// </summary>
public bool IsPageFlipping => _isPageFlipping; public bool IsPageFlipping => Navigation.IsPageFlipping;
internal override void OnManagedStart() internal override void OnManagedStart()
{ {
@@ -325,32 +340,7 @@ namespace UI.CardSystem
/// </summary> /// </summary>
private void CleanupEnlargedCardState() private void CleanupEnlargedCardState()
{ {
// Hide backdrop if visible Enlarge.CleanupEnlargedState();
if (cardEnlargedBackdrop != null && cardEnlargedBackdrop.activeSelf)
{
cardEnlargedBackdrop.SetActive(false);
}
// If there's an enlarged card in the container, return it to its slot
if (cardEnlargedContainer != null && cardEnlargedContainer.childCount > 0)
{
for (int i = cardEnlargedContainer.childCount - 1; i >= 0; i--)
{
Transform cardTransform = cardEnlargedContainer.GetChild(i);
var card = cardTransform.GetComponent<StateMachine.Card>();
var state = cardTransform.GetComponentInChildren<StateMachine.States.CardAlbumEnlargedState>(true);
if (card != null && state != null)
{
Transform originalParent = state.GetOriginalParent();
if (originalParent != null)
{
cardTransform.SetParent(originalParent, true);
cardTransform.localPosition = state.GetOriginalLocalPosition();
cardTransform.localRotation = state.GetOriginalLocalRotation();
}
}
}
}
} }
/// <summary> /// <summary>
@@ -358,15 +348,7 @@ namespace UI.CardSystem
/// </summary> /// </summary>
private bool IsInAlbumProper() private bool IsInAlbumProper()
{ {
if (book == null) return Navigation.IsInAlbumProper();
{
Logging.Warning("[AlbumViewPage] Book reference is null in IsInAlbumProper check");
return false;
}
// Page 1 is the menu/cover, page 2+ are album pages with card slots
bool inAlbum = book.CurrentPaper > 1;
return inAlbum;
} }
/// <summary> /// <summary>
@@ -375,13 +357,13 @@ namespace UI.CardSystem
private void OnPageFlipped() private void OnPageFlipped()
{ {
bool isInAlbum = IsInAlbumProper(); bool isInAlbum = IsInAlbumProper();
if (isInAlbum && _pendingCornerCards.Count == 0) if (isInAlbum && CornerCards.PendingCards.Count == 0)
{ {
// Entering album proper and no cards spawned yet - spawn them with animation // Entering album proper and no cards spawned yet - spawn them with animation
Logging.Debug("[AlbumViewPage] Entering album proper - spawning pending cards with animation"); Logging.Debug("[AlbumViewPage] Entering album proper - spawning pending cards with animation");
SpawnPendingCornerCards(); SpawnPendingCornerCards();
} }
else if (!isInAlbum && _pendingCornerCards.Count > 0) else if (!isInAlbum && CornerCards.PendingCards.Count > 0)
{ {
// Returning to menu page - cleanup cards // Returning to menu page - cleanup cards
Logging.Debug("[AlbumViewPage] Returning to menu page - cleaning up pending cards"); Logging.Debug("[AlbumViewPage] Returning to menu page - cleaning up pending cards");
@@ -401,55 +383,12 @@ namespace UI.CardSystem
/// </summary> /// </summary>
public void RegisterCardInAlbum(StateMachine.Card card) public void RegisterCardInAlbum(StateMachine.Card card)
{ {
if (card == null) return; Enlarge.RegisterCard(card);
var enlargeState = card.GetStateComponent<StateMachine.States.CardAlbumEnlargedState>("AlbumEnlargedState");
if (enlargeState != null)
{
enlargeState.OnEnlargeRequested += OnCardEnlargeRequested;
enlargeState.OnShrinkRequested += OnCardShrinkRequested;
}
} }
public void UnregisterCardInAlbum(StateMachine.Card card) public void UnregisterCardInAlbum(StateMachine.Card card)
{ {
if (card == null) return; Enlarge.UnregisterCard(card);
var enlargeState = card.GetStateComponent<StateMachine.States.CardAlbumEnlargedState>("AlbumEnlargedState");
if (enlargeState != null)
{
enlargeState.OnEnlargeRequested -= OnCardEnlargeRequested;
enlargeState.OnShrinkRequested -= OnCardShrinkRequested;
}
}
private void OnCardEnlargeRequested(StateMachine.States.CardAlbumEnlargedState state)
{
if (state == null) return;
// Show backdrop
if (cardEnlargedBackdrop != null)
{
cardEnlargedBackdrop.SetActive(true);
}
// Reparent card root to enlarged container preserving world transform
if (cardEnlargedContainer != null)
{
var ctx = state.GetComponentInParent<StateMachine.CardContext>();
if (ctx != null)
{
ctx.RootTransform.SetParent(cardEnlargedContainer, true);
ctx.RootTransform.SetAsLastSibling();
}
}
}
private void OnCardShrinkRequested(StateMachine.States.CardAlbumEnlargedState state)
{
if (state == null) return;
// Hide backdrop; state will animate back to slot and reparent on completion
if (cardEnlargedBackdrop != null)
{
cardEnlargedBackdrop.SetActive(false);
}
// Do not reparent here; reverse animation is orchestrated by the state
} }
#endregion #endregion
@@ -474,267 +413,12 @@ namespace UI.CardSystem
public void SpawnPendingCornerCards() public void SpawnPendingCornerCards()
{ {
RebuildCornerCards(); CornerCards.SpawnCards();
}
/// <summary>
/// Rebuild corner card display incrementally.
/// Flow: 1) Shuffle remaining cards to front slots (0→1→2)
/// 2) Spawn new card in last slot if needed
/// Called on initial spawn and after card is removed.
/// </summary>
private void RebuildCornerCards()
{
if (cardPrefab == null || bottomRightSlots == null) return;
// Step 1: Determine how many cards should be displayed
var uniquePending = GetUniquePendingCards();
int totalPendingCards = uniquePending.Count;
int cardsToDisplay = Mathf.Min(totalPendingCards, MaxPendingCorner);
int currentCardCount = _pendingCornerCards.Count;
Logging.Debug($"[AlbumViewPage] RebuildCornerCards: current={currentCardCount}, target={cardsToDisplay}");
// Step 2: Remove excess cards if we have too many
while (_pendingCornerCards.Count > cardsToDisplay)
{
int lastIndex = _pendingCornerCards.Count - 1;
var cardToRemove = _pendingCornerCards[lastIndex];
if (cardToRemove != null)
{
if (cardToRemove.Context != null)
{
cardToRemove.Context.OnDragStarted -= OnCardDragStarted;
}
Destroy(cardToRemove.gameObject);
}
_pendingCornerCards.RemoveAt(lastIndex);
Logging.Debug($"[AlbumViewPage] Removed excess card, now have {_pendingCornerCards.Count}");
}
// Step 3: Shuffle remaining cards to occupy first slots (0, 1, 2)
ShuffleCardsToFrontSlots();
// Step 4: Spawn new cards in remaining slots if needed
while (_pendingCornerCards.Count < cardsToDisplay)
{
int slotIndex = _pendingCornerCards.Count; // Next available slot
var slot = FindSlotByIndex(slotIndex);
if (slot == null)
{
Logging.Warning($"[AlbumViewPage] Slot {slotIndex} not found, stopping spawn");
break;
}
SpawnCardInSlot(slot);
Logging.Debug($"[AlbumViewPage] Added new card in slot {slotIndex}, now have {_pendingCornerCards.Count}");
}
if (cardsToDisplay == 0)
{
Logging.Debug("[AlbumViewPage] No pending cards to display in corner");
}
}
/// <summary>
/// Shuffle remaining cards to occupy the first available slots (0 → 1 → 2).
/// Example: If we have 2 cards in slots 0 and 2, move the card from slot 2 to slot 1.
/// </summary>
private void ShuffleCardsToFrontSlots()
{
if (_pendingCornerCards.Count == 0) return;
Logging.Debug($"[AlbumViewPage] Shuffling {_pendingCornerCards.Count} cards to front slots");
// Reassign each card to the first N slots (0, 1, 2...)
for (int i = 0; i < _pendingCornerCards.Count; i++)
{
var card = _pendingCornerCards[i];
var targetSlot = FindSlotByIndex(i);
if (targetSlot == null)
{
Logging.Warning($"[AlbumViewPage] Could not find slot with index {i} during shuffle");
continue;
}
// Check if card is already in the correct slot
if (card.CurrentSlot == targetSlot)
{
// Already in correct slot, skip
continue;
}
// Vacate current slot if occupied
if (card.CurrentSlot != null)
{
card.CurrentSlot.Vacate();
}
// Assign to target slot with animation
card.AssignToSlot(targetSlot, true); // true = animate
Logging.Debug($"[AlbumViewPage] Shuffled card {i} to slot {targetSlot.SlotIndex}");
}
}
/// <summary>
/// Spawn a single card in the specified slot.
/// Card will be in PendingFaceDownState and match slot transform.
/// </summary>
private void SpawnCardInSlot(DraggableSlot slot)
{
if (slot == null || cardPrefab == null) return;
// Instantiate card as child of slot (not container)
GameObject cardObj = Instantiate(cardPrefab, slot.transform);
var card = cardObj.GetComponent<StateMachine.Card>();
if (card == null)
{
Logging.Warning("[AlbumViewPage] Card prefab missing Card component!");
Destroy(cardObj);
return;
}
// IMPORTANT: Assign to slot FIRST (establishes correct transform)
card.AssignToSlot(slot, false); // false = instant, no animation
// Inject AlbumViewPage dependency
card.Context.SetAlbumViewPage(this);
// THEN setup card for pending state (transitions to PendingFaceDownState)
// This ensures OriginalScale is captured AFTER slot assignment
card.SetupForAlbumPending();
// Subscribe to drag events for reorganization
card.Context.OnDragStarted += OnCardDragStarted;
// Track in list
_pendingCornerCards.Add(card);
Logging.Debug($"[AlbumViewPage] Spawned card in slot {slot.SlotIndex}, state: {card.GetCurrentStateName()}");
}
/// <summary>
/// Handle card drag started - cleanup and unparent from corner slot
/// Rebuild happens in GetCardForPendingSlot after pending list is updated
/// </summary>
private void OnCardDragStarted(StateMachine.CardContext context)
{
if (context == null) return;
var card = context.GetComponent<StateMachine.Card>();
if (card == null) return;
// Only handle pending corner cards
if (!_pendingCornerCards.Contains(card)) return;
Logging.Debug($"[AlbumViewPage] Card drag started, removing from corner");
// 1. Remove from tracking (card is transitioning to placement flow)
_pendingCornerCards.Remove(card);
// 2. Unsubscribe from this card's events
if (card.Context != null)
{
card.Context.OnDragStarted -= OnCardDragStarted;
}
// 3. CRITICAL: Unparent from corner slot BEFORE rebuild happens
// This prevents the card from being destroyed when CleanupPendingCornerCards runs
// Reparent to this page's transform (or canvas) to keep it alive during drag
if (card.transform.parent != null)
{
card.transform.SetParent(transform, true); // Keep world position
Logging.Debug($"[AlbumViewPage] Card unparented from corner slot - safe for rebuild");
}
// Note: RebuildCornerCards() is called in GetCardForPendingSlot()
// after the card is removed from CardSystemManager's pending list
// The card is now safe from being destroyed since it's no longer a child of corner slots
}
/// <summary>
/// Get unique pending cards (one per definition+rarity combo)
/// </summary>
private List<CardData> GetUniquePendingCards()
{
if (CardSystemManager.Instance == null) return new List<CardData>();
var pending = CardSystemManager.Instance.GetPendingRevealCards();
// Group by definition+rarity, take first of each group
var uniqueDict = new Dictionary<string, CardData>();
int duplicateCount = 0;
foreach (var card in pending)
{
string key = $"{card.DefinitionId}_{card.Rarity}";
if (!uniqueDict.ContainsKey(key))
{
uniqueDict[key] = card;
}
else
{
duplicateCount++;
}
}
if (duplicateCount > 0)
{
Logging.Warning($"[AlbumViewPage] Found {duplicateCount} duplicate pending cards (same definition+rarity combo)");
}
return new List<CardData>(uniqueDict.Values);
} }
private void CleanupPendingCornerCards() private void CleanupPendingCornerCards()
{ {
// First, unsubscribe and destroy tracked cards CornerCards.CleanupAllCards();
foreach (var c in _pendingCornerCards)
{
if (c != null)
{
if (c.Context != null)
{
c.Context.OnDragStarted -= OnCardDragStarted;
}
Destroy(c.gameObject);
}
}
_pendingCornerCards.Clear();
// IMPORTANT: Also clear ALL children from corner slots
// This catches cards that were removed from tracking but not destroyed
// (e.g., cards being dragged to album)
if (bottomRightSlots != null)
{
foreach (var slot in bottomRightSlots.Slots)
{
if (slot == null || slot.transform == null) continue;
// Destroy all card children in this slot
for (int i = slot.transform.childCount - 1; i >= 0; i--)
{
var child = slot.transform.GetChild(i);
var card = child.GetComponent<StateMachine.Card>();
if (card != null)
{
// Unsubscribe if somehow still subscribed
if (card.Context != null)
{
card.Context.OnDragStarted -= OnCardDragStarted;
}
Destroy(child.gameObject);
Logging.Debug($"[AlbumViewPage] Cleaned up orphaned card from slot {slot.SlotIndex}");
}
}
}
}
} }
#region Query Methods for Card States (Data Providers) #region Query Methods for Card States (Data Providers)
@@ -746,35 +430,7 @@ namespace UI.CardSystem
/// </summary> /// </summary>
public CardData GetCardForPendingSlot() public CardData GetCardForPendingSlot()
{ {
if (CardSystemManager.Instance == null) return null; return CornerCards.GetSmartSelection();
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 no match, use random
if (match == null)
{
int idx = Random.Range(0, pending.Count);
match = pending[idx];
}
// IMPORTANT: Remove from pending list immediately
// Card is now in "reveal flow" and will be added to collection when placed
if (match != null)
{
// Remove from pending using the manager (fires OnPendingCardRemoved event)
CardSystemManager.Instance.RemoveFromPending(match);
Logging.Debug($"[AlbumViewPage] Removed '{match.Name}' from pending cards, starting reveal flow");
// Rebuild corner cards AFTER removing from pending list
// This ensures the removed card doesn't get re-spawned
RebuildCornerCards();
}
return match;
} }
/// <summary> /// <summary>
@@ -805,44 +461,7 @@ namespace UI.CardSystem
/// </summary> /// </summary>
public void NavigateToCardPage(CardData cardData, System.Action onComplete) public void NavigateToCardPage(CardData cardData, System.Action onComplete)
{ {
if (cardData == null || book == null) Navigation.NavigateToCardPage(cardData, onComplete);
{
onComplete?.Invoke();
return;
}
// Find target page based on card's zone
int targetPage = FindPageForZone(cardData.Zone);
if (targetPage < 0)
{
Logging.Warning($"[AlbumViewPage] No page found for zone {cardData.Zone}");
onComplete?.Invoke();
return;
}
// Mark as flipping
_isPageFlipping = true;
Logging.Debug($"[AlbumViewPage] Starting page flip to page {targetPage}");
// Get or add AutoFlip component
BookCurlPro.AutoFlip autoFlip = book.GetComponent<BookCurlPro.AutoFlip>();
if (autoFlip == null)
{
autoFlip = book.gameObject.AddComponent<BookCurlPro.AutoFlip>();
}
// Start flipping with callback
autoFlip.enabled = true;
autoFlip.StartFlipping(targetPage, () =>
{
// Mark as complete
_isPageFlipping = false;
Logging.Debug($"[AlbumViewPage] Page flip to {targetPage} completed");
// Call original callback if provided
onComplete?.Invoke();
});
} }
/// <summary> /// <summary>
@@ -851,95 +470,20 @@ namespace UI.CardSystem
/// </summary> /// </summary>
public void NotifyCardPlaced(StateMachine.Card card) public void NotifyCardPlaced(StateMachine.Card card)
{ {
if (card != null) // Delegate to corner card manager for tracking removal
{ CornerCards.NotifyCardPlaced(card);
// Remove from tracking list
_pendingCornerCards.Remove(card); // Register for enlarge/shrink functionality
RegisterCardInAlbum(card);
// IMPORTANT: Unsubscribe from drag events
// Placed cards should never respond to AlbumViewPage drag events
if (card.Context != null)
{
card.Context.OnDragStarted -= OnCardDragStarted;
}
// Register for enlarge/shrink functionality
RegisterCardInAlbum(card);
Logging.Debug($"[AlbumViewPage] Card placed and unsubscribed from corner events: {card.CardData?.Name}");
}
} }
#endregion #endregion
#region Helper Methods #region Helper Methods
/// <summary> public List<string> GetDefinitionsOnCurrentPage()
/// Find the target page for a card zone using BookTabButtons
/// </summary>
private int FindPageForZone(CardZone zone)
{ {
if (_zoneTabs == null || _zoneTabs.Length == 0) return Navigation.GetDefinitionsOnCurrentPage();
{
Logging.Warning("[AlbumViewPage] No zone tabs discovered!");
return -1;
}
foreach (var tab in _zoneTabs)
{
if (tab.Zone == zone)
{
return tab.TargetPage;
}
}
Logging.Warning($"[AlbumViewPage] No BookTabButton found for zone {zone}");
return -1;
}
private List<string> GetDefinitionsOnCurrentPage()
{
var result = new List<string>();
if (book == null) return result;
int currentPage = book.CurrentPaper;
// Find all AlbumCardSlot in scene
var allSlots = FindObjectsByType<AlbumCardSlot>(FindObjectsSortMode.None);
foreach (var slot in allSlots)
{
if (IsSlotOnPage(slot.transform, currentPage))
{
if (slot.TargetCardDefinition != null && !string.IsNullOrEmpty(slot.TargetCardDefinition.Id))
{
result.Add(slot.TargetCardDefinition.Id);
}
}
}
return result;
}
private bool IsSlotOnPage(Transform slotTransform, int pageIndex)
{
if (book == null || book.papers == null || pageIndex < 0 || pageIndex >= book.papers.Length)
return false;
var paper = book.papers[pageIndex];
if (paper == null) return false;
// Check if slotTransform parent hierarchy contains paper.Front or paper.Back
Transform current = slotTransform;
while (current != null)
{
if ((paper.Front != null && current.gameObject == paper.Front) ||
(paper.Back != null && current.gameObject == paper.Back))
return true;
current = current.parent;
}
return false;
} }
#endregion #endregion

View File

@@ -0,0 +1,128 @@
using Core;
using UI.CardSystem.StateMachine;
using UnityEngine;
namespace UI.CardSystem
{
/// <summary>
/// Manages card enlarge system: backdrop display and card reparenting.
/// Created and owned by AlbumViewPage (not a Unity component).
/// </summary>
public class CardEnlargeController
{
private readonly GameObject _backdrop;
private readonly Transform _enlargedContainer;
/// <summary>
/// Constructor - called by AlbumViewPage's lazy property
/// </summary>
public CardEnlargeController(GameObject backdrop, Transform enlargedContainer)
{
_backdrop = backdrop;
_enlargedContainer = enlargedContainer;
}
/// <summary>
/// Subscribe to a placed card's enlarged state events
/// </summary>
public void RegisterCard(StateMachine.Card card)
{
if (card == null) return;
var enlargeState = card.GetStateComponent<StateMachine.States.CardAlbumEnlargedState>(CardStateNames.AlbumEnlarged);
if (enlargeState != null)
{
enlargeState.OnEnlargeRequested += OnCardEnlargeRequested;
enlargeState.OnShrinkRequested += OnCardShrinkRequested;
}
}
/// <summary>
/// Unsubscribe from a card's enlarged state events
/// </summary>
public void UnregisterCard(StateMachine.Card card)
{
if (card == null) return;
var enlargeState = card.GetStateComponent<StateMachine.States.CardAlbumEnlargedState>(CardStateNames.AlbumEnlarged);
if (enlargeState != null)
{
enlargeState.OnEnlargeRequested -= OnCardEnlargeRequested;
enlargeState.OnShrinkRequested -= OnCardShrinkRequested;
}
}
/// <summary>
/// Clean up enlarged card state (on page close)
/// </summary>
public void CleanupEnlargedState()
{
// Hide backdrop if visible
if (_backdrop != null && _backdrop.activeSelf)
{
_backdrop.SetActive(false);
}
// If there's an enlarged card in the container, return it to its slot
if (_enlargedContainer != null && _enlargedContainer.childCount > 0)
{
for (int i = _enlargedContainer.childCount - 1; i >= 0; i--)
{
Transform cardTransform = _enlargedContainer.GetChild(i);
var card = cardTransform.GetComponent<StateMachine.Card>();
var state = cardTransform.GetComponentInChildren<StateMachine.States.CardAlbumEnlargedState>(true);
if (card != null && state != null)
{
Transform originalParent = state.GetOriginalParent();
if (originalParent != null)
{
cardTransform.SetParent(originalParent, true);
cardTransform.localPosition = state.GetOriginalLocalPosition();
cardTransform.localRotation = state.GetOriginalLocalRotation();
}
}
}
}
}
#region Event Handlers
private void OnCardEnlargeRequested(StateMachine.States.CardAlbumEnlargedState state)
{
if (state == null) return;
// Show backdrop
if (_backdrop != null)
{
_backdrop.SetActive(true);
}
// Reparent card root to enlarged container preserving world transform
if (_enlargedContainer != null)
{
var ctx = state.GetComponentInParent<StateMachine.CardContext>();
if (ctx != null)
{
ctx.RootTransform.SetParent(_enlargedContainer, true);
ctx.RootTransform.SetAsLastSibling();
}
}
}
private void OnCardShrinkRequested(StateMachine.States.CardAlbumEnlargedState state)
{
if (state == null) return;
// Hide backdrop; state will animate back to slot and reparent on completion
if (_backdrop != null)
{
_backdrop.SetActive(false);
}
// Do not reparent here; reverse animation is orchestrated by the state
}
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f0c2c6c12ee64ccabe778e848ac9c822
timeCreated: 1763425800

View File

@@ -0,0 +1,392 @@
using System.Collections.Generic;
using System.Linq;
using AppleHills.Data.CardSystem;
using Core;
using Data.CardSystem;
using UI.DragAndDrop.Core;
using UnityEngine;
namespace UI.CardSystem
{
/// <summary>
/// Manages corner card spawning, selection, shuffling, and rebuilding.
/// Created and owned by AlbumViewPage (not a Unity component).
/// </summary>
public class CornerCardManager
{
private readonly SlotContainer _slots;
private readonly GameObject _cardPrefab;
private readonly AlbumViewPage _owner;
private List<StateMachine.Card> _pendingCards = new List<StateMachine.Card>();
private const int MaxVisible = 3;
/// <summary>
/// Constructor - called by AlbumViewPage's lazy property
/// </summary>
public CornerCardManager(SlotContainer slots, GameObject cardPrefab, AlbumViewPage owner)
{
_slots = slots;
_cardPrefab = cardPrefab;
_owner = owner;
}
/// <summary>
/// Get the list of currently spawned corner cards
/// </summary>
public List<StateMachine.Card> PendingCards => _pendingCards;
/// <summary>
/// Spawn pending corner cards with initial setup
/// </summary>
public void SpawnCards()
{
RebuildCards();
}
/// <summary>
/// Rebuild corner card display incrementally.
/// Flow: 1) Shuffle remaining cards to front slots (0→1→2)
/// 2) Spawn new card in last slot if needed
/// Called on initial spawn and after card is removed.
/// </summary>
public void RebuildCards()
{
if (_cardPrefab == null || _slots == null) return;
// Step 1: Determine how many cards should be displayed
var uniquePending = GetUniquePendingCards();
int totalPendingCards = uniquePending.Count;
int cardsToDisplay = Mathf.Min(totalPendingCards, MaxVisible);
int currentCardCount = _pendingCards.Count;
Logging.Debug($"[CornerCardManager] RebuildCards: current={currentCardCount}, target={cardsToDisplay}");
// Step 2: Remove excess cards if we have too many
while (_pendingCards.Count > cardsToDisplay)
{
int lastIndex = _pendingCards.Count - 1;
var cardToRemove = _pendingCards[lastIndex];
if (cardToRemove != null)
{
if (cardToRemove.Context != null)
{
cardToRemove.Context.OnDragStarted -= OnCardDragStarted;
}
Object.Destroy(cardToRemove.gameObject);
}
_pendingCards.RemoveAt(lastIndex);
Logging.Debug($"[CornerCardManager] Removed excess card, now have {_pendingCards.Count}");
}
// Step 3: Shuffle remaining cards to occupy first slots (0, 1, 2)
ShuffleCardsToFrontSlots();
// Step 4: Spawn new cards in remaining slots if needed
while (_pendingCards.Count < cardsToDisplay)
{
int slotIndex = _pendingCards.Count; // Next available slot
var slot = FindSlotByIndex(slotIndex);
if (slot == null)
{
Logging.Warning($"[CornerCardManager] Slot {slotIndex} not found, stopping spawn");
break;
}
SpawnCardInSlot(slot);
Logging.Debug($"[CornerCardManager] Added new card in slot {slotIndex}, now have {_pendingCards.Count}");
}
if (cardsToDisplay == 0)
{
Logging.Debug("[CornerCardManager] No pending cards to display in corner");
}
}
/// <summary>
/// Smart card selection from pending queue.
/// Prioritizes cards that belong on current album page, falls back to random.
/// Removes selected card from pending and rebuilds corner.
/// </summary>
public CardData GetSmartSelection()
{
if (CardSystemManager.Instance == null) return null;
var pending = CardSystemManager.Instance.GetPendingRevealCards();
if (pending.Count == 0) return null;
// Try current page match
var pageDefs = _owner.GetDefinitionsOnCurrentPage();
var match = pending.Find(c => pageDefs.Contains(c.DefinitionId));
// If no match, use random
if (match == null)
{
int idx = Random.Range(0, pending.Count);
match = pending[idx];
}
// IMPORTANT: Remove from pending list immediately
// Card is now in "reveal flow" and will be added to collection when placed
if (match != null)
{
// Remove from pending using the manager (fires OnPendingCardRemoved event)
CardSystemManager.Instance.RemoveFromPending(match);
Logging.Debug($"[CornerCardManager] Removed '{match.Name}' from pending cards, starting reveal flow");
// Rebuild corner cards AFTER removing from pending list
// This ensures the removed card doesn't get re-spawned
RebuildCards();
}
return match;
}
/// <summary>
/// Cleanup all corner cards (on page close)
/// </summary>
public void CleanupAllCards()
{
// First, unsubscribe and destroy tracked cards
foreach (var c in _pendingCards)
{
if (c != null)
{
if (c.Context != null)
{
c.Context.OnDragStarted -= OnCardDragStarted;
}
Object.Destroy(c.gameObject);
}
}
_pendingCards.Clear();
// IMPORTANT: Also clear ALL children from corner slots
// This catches cards that were removed from tracking but not destroyed
// (e.g., cards being dragged to album)
if (_slots != null)
{
foreach (var slot in _slots.Slots)
{
if (slot == null || slot.transform == null) continue;
// Destroy all card children in this slot
for (int i = slot.transform.childCount - 1; i >= 0; i--)
{
var child = slot.transform.GetChild(i);
var card = child.GetComponent<StateMachine.Card>();
if (card != null)
{
// Unsubscribe if somehow still subscribed
if (card.Context != null)
{
card.Context.OnDragStarted -= OnCardDragStarted;
}
Object.Destroy(child.gameObject);
Logging.Debug($"[CornerCardManager] Cleaned up orphaned card from slot {slot.SlotIndex}");
}
}
}
}
}
/// <summary>
/// Notify that a card has been placed in album (remove from tracking)
/// </summary>
public void NotifyCardPlaced(StateMachine.Card card)
{
if (card != null)
{
// Remove from tracking list
_pendingCards.Remove(card);
// IMPORTANT: Unsubscribe from drag events
// Placed cards should never respond to corner drag events
if (card.Context != null)
{
card.Context.OnDragStarted -= OnCardDragStarted;
}
Logging.Debug($"[CornerCardManager] Card placed and unsubscribed from corner events: {card.CardData?.Name}");
}
}
#region Private Helpers
/// <summary>
/// Shuffle remaining cards to occupy the first available slots (0 → 1 → 2).
/// Example: If we have 2 cards in slots 0 and 2, move the card from slot 2 to slot 1.
/// </summary>
private void ShuffleCardsToFrontSlots()
{
if (_pendingCards.Count == 0) return;
Logging.Debug($"[CornerCardManager] Shuffling {_pendingCards.Count} cards to front slots");
// Reassign each card to the first N slots (0, 1, 2...)
for (int i = 0; i < _pendingCards.Count; i++)
{
var card = _pendingCards[i];
var targetSlot = FindSlotByIndex(i);
if (targetSlot == null)
{
Logging.Warning($"[CornerCardManager] Could not find slot with index {i} during shuffle");
continue;
}
// Check if card is already in the correct slot
if (card.CurrentSlot == targetSlot)
{
// Already in correct slot, skip
continue;
}
// Vacate current slot if occupied
if (card.CurrentSlot != null)
{
card.CurrentSlot.Vacate();
}
// Assign to target slot with animation
card.AssignToSlot(targetSlot, true); // true = animate
Logging.Debug($"[CornerCardManager] Shuffled card {i} to slot {targetSlot.SlotIndex}");
}
}
/// <summary>
/// Spawn a single card in the specified slot.
/// Card will be in PendingFaceDownState and match slot transform.
/// </summary>
private void SpawnCardInSlot(DraggableSlot slot)
{
if (slot == null || _cardPrefab == null) return;
// Instantiate card as child of slot (not container)
GameObject cardObj = Object.Instantiate(_cardPrefab, slot.transform);
var card = cardObj.GetComponent<StateMachine.Card>();
if (card == null)
{
Logging.Warning("[CornerCardManager] Card prefab missing Card component!");
Object.Destroy(cardObj);
return;
}
// IMPORTANT: Assign to slot FIRST (establishes correct transform)
card.AssignToSlot(slot, false); // false = instant, no animation
// Inject AlbumViewPage dependency
card.Context.SetAlbumViewPage(_owner);
// THEN setup card for pending state (transitions to PendingFaceDownState)
// This ensures OriginalScale is captured AFTER slot assignment
card.SetupForAlbumPending();
// Subscribe to drag events for reorganization
card.Context.OnDragStarted += OnCardDragStarted;
// Track in list
_pendingCards.Add(card);
Logging.Debug($"[CornerCardManager] Spawned card in slot {slot.SlotIndex}, state: {card.GetCurrentStateName()}");
}
/// <summary>
/// Handle card drag started - cleanup and unparent from corner slot
/// Rebuild happens in GetSmartSelection after pending list is updated
/// </summary>
private void OnCardDragStarted(StateMachine.CardContext context)
{
if (context == null) return;
var card = context.GetComponent<StateMachine.Card>();
if (card == null) return;
// Only handle pending corner cards
if (!_pendingCards.Contains(card)) return;
Logging.Debug($"[CornerCardManager] Card drag started, removing from corner");
// 1. Remove from tracking (card is transitioning to placement flow)
_pendingCards.Remove(card);
// 2. Unsubscribe from this card's events
if (card.Context != null)
{
card.Context.OnDragStarted -= OnCardDragStarted;
}
// 3. CRITICAL: Unparent from corner slot BEFORE rebuild happens
// This prevents the card from being destroyed when CleanupAllCards runs
// Reparent to page's transform to keep it alive during drag
if (card.transform.parent != null)
{
card.transform.SetParent(_owner.transform, true); // Keep world position
Logging.Debug($"[CornerCardManager] Card unparented from corner slot - safe for rebuild");
}
// Note: RebuildCards() is called in GetSmartSelection()
// after the card is removed from CardSystemManager's pending list
// The card is now safe from being destroyed since it's no longer a child of corner slots
}
/// <summary>
/// Get unique pending cards (one per definition+rarity combo)
/// </summary>
private List<CardData> GetUniquePendingCards()
{
if (CardSystemManager.Instance == null) return new List<CardData>();
var pending = CardSystemManager.Instance.GetPendingRevealCards();
// Group by definition+rarity, take first of each group
var uniqueDict = new Dictionary<string, CardData>();
int duplicateCount = 0;
foreach (var card in pending)
{
string key = $"{card.DefinitionId}_{card.Rarity}";
if (!uniqueDict.ContainsKey(key))
{
uniqueDict[key] = card;
}
else
{
duplicateCount++;
}
}
if (duplicateCount > 0)
{
Logging.Warning($"[CornerCardManager] Found {duplicateCount} duplicate pending cards (same definition+rarity combo)");
}
return new List<CardData>(uniqueDict.Values);
}
/// <summary>
/// Find a slot by its SlotIndex property
/// </summary>
private DraggableSlot FindSlotByIndex(int slotIndex)
{
if (_slots == null)
return null;
foreach (var slot in _slots.Slots)
{
if (slot.SlotIndex == slotIndex)
{
return slot;
}
}
return null;
}
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c7c68770a6974aa4a22c645fe0517fe2
timeCreated: 1763425113