Merge a card refresh (#59)
- **Refactored Card Placement Flow** - Separated card presentation from orchestration logic - Extracted `CornerCardManager` for pending card lifecycle (spawn, shuffle, rebuild) - Extracted `AlbumNavigationService` for book page navigation and zone mapping - Extracted `CardEnlargeController` for backdrop management and card reparenting - Implemented controller pattern (non-MonoBehaviour) for complex logic - Cards now unparent from slots before rebuild to prevent premature destruction - **Improved Corner Card Display** - Fixed cards spawning on top of each other during rebuild - Implemented shuffle-to-front logic (remaining cards occupy slots 0→1→2) - Added smart card selection (prioritizes cards matching current album page) - Pending cards now removed from queue immediately on drag start - Corner cards rebuild after each placement with proper slot reassignment - **Enhanced Album Card Viewing** - Added dramatic scale increase when viewing cards from album slots - Implemented shrink animation when dismissing enlarged cards - Cards transition: `PlacedInSlotState` → `AlbumEnlargedState` → `PlacedInSlotState` - Backdrop shows/hides with card enlarge/shrink cycle - Cards reparent to enlarged container while viewing, return to slot after - **State Machine Improvements** - Added `CardStateNames` constants for type-safe state transitions - Implemented `ICardClickHandler` and `ICardStateDragHandler` interfaces - State transitions now use cached property indices - `BoosterCardContext` separated from `CardContext` for single responsibility - **Code Cleanup** - Identified unused `SlotContainerHelper.cs` (superseded by `CornerCardManager`) - Identified unused `BoosterPackDraggable.canOpenOnDrop` field - Identified unused `AlbumViewPage._previousInputMode` (hardcoded value) - Identified unused `Card.OnPlacedInAlbumSlot` event (no subscribers) Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com> Reviewed-on: #59
This commit is contained in:
172
Assets/Scripts/CardSystem/Controllers/AlbumNavigationService.cs
Normal file
172
Assets/Scripts/CardSystem/Controllers/AlbumNavigationService.cs
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5dce94e276b8456996d9ddacef25c744
|
||||
timeCreated: 1763425777
|
||||
128
Assets/Scripts/CardSystem/Controllers/CardEnlargeController.cs
Normal file
128
Assets/Scripts/CardSystem/Controllers/CardEnlargeController.cs
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f0c2c6c12ee64ccabe778e848ac9c822
|
||||
timeCreated: 1763425800
|
||||
392
Assets/Scripts/CardSystem/Controllers/CornerCardManager.cs
Normal file
392
Assets/Scripts/CardSystem/Controllers/CornerCardManager.cs
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7c68770a6974aa4a22c645fe0517fe2
|
||||
timeCreated: 1763425113
|
||||
205
Assets/Scripts/CardSystem/Controllers/ProgressBarController.cs
Normal file
205
Assets/Scripts/CardSystem/Controllers/ProgressBarController.cs
Normal file
@@ -0,0 +1,205 @@
|
||||
using System.Collections;
|
||||
using Core;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using AppleHills.Core.Settings;
|
||||
|
||||
namespace UI.CardSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Controls a vertical progress bar made of individual Image elements.
|
||||
/// Fills from bottom to top and animates the newest element with a blink effect.
|
||||
///
|
||||
/// Setup: Place on GameObject with VerticalLayoutGroup (Reverse Arrangement enabled).
|
||||
/// First child = first progress element (1/5), last child = last progress element (5/5).
|
||||
/// </summary>
|
||||
public class ProgressBarController : MonoBehaviour
|
||||
{
|
||||
[Header("Progress Elements")]
|
||||
[Tooltip("The individual Image components representing progress segments (auto-detected from children)")]
|
||||
private Image[] _progressElements;
|
||||
|
||||
private ICardSystemSettings _settings;
|
||||
private Coroutine _currentBlinkCoroutine;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_settings = GameManager.GetSettingsObject<ICardSystemSettings>();
|
||||
|
||||
// Auto-detect all child Image components
|
||||
_progressElements = GetComponentsInChildren<Image>(true);
|
||||
|
||||
if (_progressElements.Length == 0)
|
||||
{
|
||||
Logging.Warning("[ProgressBarController] No child Image components found! Add Image children to this GridLayout.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show progress and animate the newest element with a blink effect.
|
||||
/// </summary>
|
||||
/// <param name="currentCount">Current progress (1 to maxCount)</param>
|
||||
/// <param name="maxCount">Maximum progress value (typically 5)</param>
|
||||
/// <param name="onComplete">Callback invoked after blink animation completes</param>
|
||||
public void ShowProgress(int currentCount, int maxCount, System.Action onComplete)
|
||||
{
|
||||
// Validate input
|
||||
if (currentCount < 0 || currentCount > maxCount)
|
||||
{
|
||||
Logging.Warning($"[ProgressBarController] Invalid progress: {currentCount}/{maxCount}");
|
||||
onComplete?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate element count
|
||||
if (_progressElements.Length < maxCount)
|
||||
{
|
||||
Logging.Warning($"[ProgressBarController] Not enough progress elements! Expected {maxCount}, found {_progressElements.Length}");
|
||||
onComplete?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop any existing blink animation
|
||||
if (_currentBlinkCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_currentBlinkCoroutine);
|
||||
_currentBlinkCoroutine = null;
|
||||
}
|
||||
|
||||
// Disable all elements first
|
||||
foreach (var element in _progressElements)
|
||||
{
|
||||
element.enabled = false;
|
||||
}
|
||||
|
||||
// Enable first N elements (since first child = 1/5, last child = 5/5)
|
||||
// If currentCount = 3, enable elements [0], [1], [2] (first 3 elements)
|
||||
for (int i = 0; i < currentCount && i < _progressElements.Length; i++)
|
||||
{
|
||||
_progressElements[i].enabled = true;
|
||||
}
|
||||
|
||||
Logging.Debug($"[ProgressBarController] Showing progress {currentCount}/{maxCount}");
|
||||
|
||||
// Blink the NEWEST element (the last one we just enabled)
|
||||
// Newest element is at index (currentCount - 1)
|
||||
int newestIndex = currentCount - 1;
|
||||
if (newestIndex >= 0 && newestIndex < _progressElements.Length && currentCount > 0)
|
||||
{
|
||||
_currentBlinkCoroutine = StartCoroutine(BlinkElement(newestIndex, onComplete));
|
||||
}
|
||||
else
|
||||
{
|
||||
// No element to blink (e.g., currentCount = 0)
|
||||
onComplete?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show progress without blink animation (instant display).
|
||||
/// </summary>
|
||||
/// <param name="currentCount">Current progress (1 to maxCount)</param>
|
||||
/// <param name="maxCount">Maximum progress value</param>
|
||||
public void ShowProgressInstant(int currentCount, int maxCount)
|
||||
{
|
||||
// Validate
|
||||
if (currentCount < 0 || currentCount > maxCount || _progressElements.Length < maxCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable all
|
||||
foreach (var element in _progressElements)
|
||||
{
|
||||
element.enabled = false;
|
||||
}
|
||||
|
||||
// Enable first N elements
|
||||
for (int i = 0; i < currentCount && i < _progressElements.Length; i++)
|
||||
{
|
||||
_progressElements[i].enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hide all progress elements.
|
||||
/// </summary>
|
||||
public void HideProgress()
|
||||
{
|
||||
foreach (var element in _progressElements)
|
||||
{
|
||||
element.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Blink a specific element by toggling enabled/disabled.
|
||||
/// </summary>
|
||||
private IEnumerator BlinkElement(int index, System.Action onComplete)
|
||||
{
|
||||
if (index < 0 || index >= _progressElements.Length)
|
||||
{
|
||||
onComplete?.Invoke();
|
||||
yield break;
|
||||
}
|
||||
|
||||
Image element = _progressElements[index];
|
||||
|
||||
// Get blink settings (or use defaults if not available)
|
||||
float blinkDuration = 0.15f; // Duration for each on/off toggle
|
||||
int blinkCount = 3; // Number of full blinks (on->off->on = 1 blink)
|
||||
|
||||
// Try to get settings if available
|
||||
if (_settings != null)
|
||||
{
|
||||
// Settings could expose these if needed:
|
||||
// blinkDuration = _settings.ProgressBlinkDuration;
|
||||
// blinkCount = _settings.ProgressBlinkCount;
|
||||
}
|
||||
|
||||
Logging.Debug($"[ProgressBarController] Blinking element {index} ({blinkCount} times)");
|
||||
|
||||
// Wait a brief moment before starting blink
|
||||
yield return new WaitForSeconds(0.3f);
|
||||
|
||||
// Perform blinks (on->off->on = 1 full blink)
|
||||
for (int i = 0; i < blinkCount; i++)
|
||||
{
|
||||
// Off
|
||||
element.enabled = false;
|
||||
yield return new WaitForSeconds(blinkDuration);
|
||||
|
||||
// On
|
||||
element.enabled = true;
|
||||
yield return new WaitForSeconds(blinkDuration);
|
||||
}
|
||||
|
||||
// Ensure element is enabled at the end
|
||||
element.enabled = true;
|
||||
|
||||
Logging.Debug($"[ProgressBarController] Blink complete for element {index}");
|
||||
|
||||
_currentBlinkCoroutine = null;
|
||||
onComplete?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the total number of progress elements available.
|
||||
/// </summary>
|
||||
public int GetElementCount()
|
||||
{
|
||||
return _progressElements?.Length ?? 0;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// Clean up any running coroutines
|
||||
if (_currentBlinkCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_currentBlinkCoroutine);
|
||||
_currentBlinkCoroutine = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e91de41001c14101b8fa4216d6c7888b
|
||||
timeCreated: 1762939781
|
||||
Reference in New Issue
Block a user