Files
AppleHillsProduction/Assets/Scripts/UI/CardSystem/AlbumViewPage.cs

945 lines
36 KiB
C#
Raw Normal View History

using System.Collections.Generic;
using AppleHills.Data.CardSystem;
using Core;
2025-11-06 10:10:54 +01:00
using Data.CardSystem;
using Pixelplacement;
using UI.Core;
using UI.DragAndDrop.Core;
using UnityEngine;
using UnityEngine.UI;
2025-11-16 20:35:54 +01:00
using UnityEngine.Serialization;
namespace UI.CardSystem
{
/// <summary>
2025-10-20 08:32:57 +02:00
/// UI page for viewing the player's card collection in an album.
2025-11-06 10:10:54 +01:00
/// Manages booster pack button visibility and opening flow.
/// </summary>
public class AlbumViewPage : UIPage
{
2025-11-06 10:10:54 +01:00
[Header("UI References")]
2025-10-15 09:22:13 +02:00
[SerializeField] private CanvasGroup canvasGroup;
[SerializeField] private Button exitButton;
[SerializeField] private BookCurlPro.BookPro book;
2025-11-06 10:10:54 +01:00
[Header("Zone Navigation")]
[SerializeField] private Transform tabContainer; // Container holding all BookTabButton children
2025-11-17 14:30:07 +01:00
private BookTabButton[] _zoneTabs; // Discovered zone tab buttons
[Header("Album Card Reveal")]
[SerializeField] private SlotContainer bottomRightSlots;
2025-11-16 20:35:54 +01:00
[FormerlySerializedAs("albumCardPlacementPrefab")]
[SerializeField] private GameObject cardPrefab; // New Card prefab for placement
[Header("Card Enlarge System")]
[SerializeField] private GameObject cardEnlargedBackdrop; // Backdrop to block interactions
[SerializeField] private Transform cardEnlargedContainer; // Container for enlarged cards (sits above backdrop)
2025-11-06 10:10:54 +01:00
[Header("Booster Pack UI")]
[SerializeField] private GameObject[] boosterPackButtons;
[SerializeField] private BoosterOpeningPage boosterOpeningPage;
2025-11-06 13:18:39 +01:00
private Input.InputMode _previousInputMode;
2025-11-17 08:39:41 +01:00
private List<StateMachine.Card> _pendingCornerCards = new List<StateMachine.Card>();
2025-11-17 14:30:07 +01:00
private const int MaxPendingCorner = 3;
2025-11-17 17:10:24 +01:00
// Page flip tracking (for card placement coordination)
private bool _isPageFlipping = false;
/// <summary>
/// Query method: Check if the book is currently flipping to a page.
/// Used by card states to know if they should wait before placing.
/// </summary>
public bool IsPageFlipping => _isPageFlipping;
2025-11-17 08:39:41 +01:00
Lifecycle System Refactor & Logging Centralization (#56) ## ManagedBehaviour System Refactor - **Sealed `Awake()`** to prevent override mistakes that break singleton registration - **Added `OnManagedAwake()`** for early initialization (fires during registration) - **Renamed lifecycle hook:** `OnManagedAwake()` → `OnManagedStart()` (fires after boot, mirrors Unity's Awake→Start) - **40 files migrated** to new pattern (2 core, 38 components) - Eliminated all fragile `private new void Awake()` patterns - Zero breaking changes - backward compatible ## Centralized Logging System - **Automatic tagging** via `CallerMemberName` and `CallerFilePath` - logs auto-tagged as `[ClassName][MethodName] message` - **Unified API:** Single `Logging.Debug/Info/Warning/Error()` replaces custom `LogDebugMessage()` implementations - **~90 logging call sites** migrated across 10 files - **10 redundant helper methods** removed - All logs broadcast via `Logging.OnLogEntryAdded` event for real-time monitoring ## Custom Log Console (Editor Window) - **Persistent filter popups** for multi-selection (classes, methods, log levels) - windows stay open during selection - **Search** across class names, methods, and message content - **Time range filter** with MinMaxSlider - **Export** filtered logs to timestamped `.txt` files - **Right-click context menu** for quick filtering and copy actions - **Visual improvements:** White text, alternating row backgrounds, color-coded log levels - **Multiple instances** supported for simultaneous system monitoring - Open via `AppleHills > Custom Log Console` Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com> Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Reviewed-on: https://homelab.tailf7f81b.ts.net/tschesky/AppleHillsProduction/pulls/56
2025-11-11 08:48:29 +00:00
internal override void OnManagedStart()
{
// Discover zone tabs from container
DiscoverZoneTabs();
2025-10-15 09:22:13 +02:00
// Make sure we have a CanvasGroup for transitions
if (canvasGroup == null)
canvasGroup = GetComponent<CanvasGroup>();
if (canvasGroup == null)
canvasGroup = gameObject.AddComponent<CanvasGroup>();
2025-11-10 12:41:28 +01:00
// Hide backdrop initially
if (cardEnlargedBackdrop != null)
{
cardEnlargedBackdrop.SetActive(false);
}
// Set up exit button
if (exitButton != null)
{
exitButton.onClick.AddListener(OnExitButtonClicked);
}
2025-10-15 09:22:13 +02:00
2025-11-06 10:10:54 +01:00
// Set up booster pack button listeners
SetupBoosterButtonListeners();
2025-11-10 12:41:28 +01:00
// Subscribe to book page flip events
if (book != null)
{
book.OnFlip.AddListener(OnPageFlipped);
2025-11-10 13:03:36 +01:00
Logging.Debug("[AlbumViewPage] Subscribed to book.OnFlip event");
2025-11-10 12:41:28 +01:00
}
else
{
2025-11-10 13:03:36 +01:00
Logging.Warning("[AlbumViewPage] Book reference is null, cannot subscribe to OnFlip event!");
2025-11-10 12:41:28 +01:00
}
Refactor interactions, introduce template-method lifecycle management, work on save-load system (#51) # Lifecycle Management & Save System Revamp ## Overview Complete overhaul of game lifecycle management, interactable system, and save/load architecture. Introduces centralized `ManagedBehaviour` base class for consistent initialization ordering and lifecycle hooks across all systems. ## Core Architecture ### New Lifecycle System - **`LifecycleManager`**: Centralized coordinator for all managed objects - **`ManagedBehaviour`**: Base class replacing ad-hoc initialization patterns - `OnManagedAwake()`: Priority-based initialization (0-100, lower = earlier) - `OnSceneReady()`: Scene-specific setup after managers ready - Replaces `BootCompletionService` (deleted) - **Priority groups**: Infrastructure (0-20) → Game Systems (30-50) → Data (60-80) → UI/Gameplay (90-100) - **Editor support**: `EditorLifecycleBootstrap` ensures lifecycle works in editor mode ### Unified SaveID System - Consistent format: `{ParentName}_{ComponentType}` - Auto-registration via `AutoRegisterForSave = true` - New `DebugSaveIds` editor tool for inspection ## Save/Load Improvements ### Enhanced State Management - **Extended SaveLoadData**: Unlocked minigames, card collection states, combination items, slot occupancy - **Async loading**: `ApplyCardCollectionState()` waits for card definitions before restoring - **New `SaveablePlayableDirector`**: Timeline sequences save/restore playback state - **Fixed race conditions**: Proper initialization ordering prevents data corruption ## Interactable & Pickup System - Migrated to `OnManagedAwake()` for consistent initialization - Template method pattern for state restoration (`RestoreInteractionState()`) - Fixed combination item save/load bugs (items in slots vs. follower hand) - Dynamic spawning support for combined items on load - **Breaking**: `Interactable.Awake()` now sealed, use `OnManagedAwake()` instead ## UI System Changes - **AlbumViewPage** and **BoosterNotificationDot**: Migrated to `ManagedBehaviour` - **Fixed menu persistence bug**: Menus no longer reappear after scene transitions - **Pause Menu**: Now reacts to all scene loads (not just first scene) - **Orientation Enforcer**: Enforces per-scene via `SceneManagementService` - **Loading Screen**: Integrated with new lifecycle ## ⚠️ Breaking Changes 1. **`BootCompletionService` removed** → Use `ManagedBehaviour.OnManagedAwake()` with priority 2. **`Interactable.Awake()` sealed** → Override `OnManagedAwake()` instead 3. **SaveID format changed** → Now `{ParentName}_{ComponentType}` consistently 4. **MonoBehaviours needing init ordering** → Must inherit from `ManagedBehaviour` Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com> Reviewed-on: https://homelab.tailf7f81b.ts.net/tschesky/AppleHillsProduction/pulls/51
2025-11-07 15:38:31 +00:00
// Subscribe to CardSystemManager events (managers are guaranteed to be initialized)
2025-11-06 10:10:54 +01:00
if (CardSystemManager.Instance != null)
{
CardSystemManager.Instance.OnBoosterCountChanged += OnBoosterCountChanged;
// NOTE: OnPendingCardAdded is subscribed in TransitionIn, not here
// This prevents spawning cards when page is not active
2025-11-06 10:10:54 +01:00
// Update initial button visibility
int initialCount = CardSystemManager.Instance.GetBoosterPackCount();
UpdateBoosterButtons(initialCount);
}
Refactor interactions, introduce template-method lifecycle management, work on save-load system (#51) # Lifecycle Management & Save System Revamp ## Overview Complete overhaul of game lifecycle management, interactable system, and save/load architecture. Introduces centralized `ManagedBehaviour` base class for consistent initialization ordering and lifecycle hooks across all systems. ## Core Architecture ### New Lifecycle System - **`LifecycleManager`**: Centralized coordinator for all managed objects - **`ManagedBehaviour`**: Base class replacing ad-hoc initialization patterns - `OnManagedAwake()`: Priority-based initialization (0-100, lower = earlier) - `OnSceneReady()`: Scene-specific setup after managers ready - Replaces `BootCompletionService` (deleted) - **Priority groups**: Infrastructure (0-20) → Game Systems (30-50) → Data (60-80) → UI/Gameplay (90-100) - **Editor support**: `EditorLifecycleBootstrap` ensures lifecycle works in editor mode ### Unified SaveID System - Consistent format: `{ParentName}_{ComponentType}` - Auto-registration via `AutoRegisterForSave = true` - New `DebugSaveIds` editor tool for inspection ## Save/Load Improvements ### Enhanced State Management - **Extended SaveLoadData**: Unlocked minigames, card collection states, combination items, slot occupancy - **Async loading**: `ApplyCardCollectionState()` waits for card definitions before restoring - **New `SaveablePlayableDirector`**: Timeline sequences save/restore playback state - **Fixed race conditions**: Proper initialization ordering prevents data corruption ## Interactable & Pickup System - Migrated to `OnManagedAwake()` for consistent initialization - Template method pattern for state restoration (`RestoreInteractionState()`) - Fixed combination item save/load bugs (items in slots vs. follower hand) - Dynamic spawning support for combined items on load - **Breaking**: `Interactable.Awake()` now sealed, use `OnManagedAwake()` instead ## UI System Changes - **AlbumViewPage** and **BoosterNotificationDot**: Migrated to `ManagedBehaviour` - **Fixed menu persistence bug**: Menus no longer reappear after scene transitions - **Pause Menu**: Now reacts to all scene loads (not just first scene) - **Orientation Enforcer**: Enforces per-scene via `SceneManagementService` - **Loading Screen**: Integrated with new lifecycle ## ⚠️ Breaking Changes 1. **`BootCompletionService` removed** → Use `ManagedBehaviour.OnManagedAwake()` with priority 2. **`Interactable.Awake()` sealed** → Override `OnManagedAwake()` instead 3. **SaveID format changed** → Now `{ParentName}_{ComponentType}` consistently 4. **MonoBehaviours needing init ordering** → Must inherit from `ManagedBehaviour` Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com> Reviewed-on: https://homelab.tailf7f81b.ts.net/tschesky/AppleHillsProduction/pulls/51
2025-11-07 15:38:31 +00:00
// UI pages should start disabled
gameObject.SetActive(false);
2025-11-06 10:10:54 +01:00
}
/// <summary>
/// Discover all BookTabButton components from the tab container
/// </summary>
private void DiscoverZoneTabs()
{
if (tabContainer == null)
{
Debug.LogError("[AlbumViewPage] Tab container is not assigned! Cannot discover zone tabs.");
2025-11-17 14:30:07 +01:00
_zoneTabs = new BookTabButton[0];
return;
}
// Get all BookTabButton components from children
2025-11-17 14:30:07 +01:00
_zoneTabs = tabContainer.GetComponentsInChildren<BookTabButton>(includeInactive: false);
2025-11-17 14:30:07 +01:00
if (_zoneTabs == null || _zoneTabs.Length == 0)
{
2025-11-10 13:03:36 +01:00
Logging.Warning($"[AlbumViewPage] No BookTabButton components found in tab container '{tabContainer.name}'!");
2025-11-17 14:30:07 +01:00
_zoneTabs = new BookTabButton[0];
}
else
{
2025-11-17 14:30:07 +01:00
Logging.Debug($"[AlbumViewPage] Discovered {_zoneTabs.Length} zone tabs from container '{tabContainer.name}'");
foreach (var tab in _zoneTabs)
{
2025-11-10 13:03:36 +01:00
Logging.Debug($" - Tab: {tab.name}, Zone: {tab.Zone}, TargetPage: {tab.TargetPage}");
}
}
}
2025-11-06 10:10:54 +01:00
private void SetupBoosterButtonListeners()
{
if (boosterPackButtons == null) return;
for (int i = 0; i < boosterPackButtons.Length; i++)
{
if (boosterPackButtons[i] == null) continue;
2025-11-17 10:59:59 +01:00
2025-11-06 10:10:54 +01:00
Button button = boosterPackButtons[i].GetComponent<Button>();
if (button != null)
{
button.onClick.AddListener(OnBoosterButtonClicked);
}
}
}
internal override void OnManagedDestroy()
{
2025-11-17 10:59:59 +01:00
// Unsubscribe from book events
if (book != null)
{
book.OnFlip.RemoveListener(OnPageFlipped);
}
2025-11-06 10:10:54 +01:00
// Unsubscribe from CardSystemManager
if (CardSystemManager.Instance != null)
{
CardSystemManager.Instance.OnBoosterCountChanged -= OnBoosterCountChanged;
}
// Clean up exit button
if (exitButton != null)
2025-10-15 09:22:13 +02:00
{
exitButton.onClick.RemoveListener(OnExitButtonClicked);
2025-10-15 09:22:13 +02:00
}
2025-11-06 10:10:54 +01:00
// Clean up booster button listeners
if (boosterPackButtons != null)
{
foreach (var buttonObj in boosterPackButtons)
{
if (buttonObj == null) continue;
Button button = buttonObj.GetComponent<Button>();
if (button != null)
{
button.onClick.RemoveListener(OnBoosterButtonClicked);
}
}
}
2025-11-17 10:59:59 +01:00
// Clean up pending corner cards
CleanupPendingCornerCards();
}
2025-10-20 13:45:56 +02:00
private void OnExitButtonClicked()
{
if (book != null && book.CurrentPaper != 1)
{
// Not on page 0, flip to page 0 first
BookCurlPro.AutoFlip autoFlip = book.GetComponent<BookCurlPro.AutoFlip>();
if (autoFlip == null)
2025-10-15 09:22:13 +02:00
{
autoFlip = book.gameObject.AddComponent<BookCurlPro.AutoFlip>();
2025-10-15 09:22:13 +02:00
}
autoFlip.enabled = true;
autoFlip.StartFlipping(1);
}
else
{
// Already on page 0 or no book reference, exit
2025-11-06 13:18:39 +01:00
// Restore input mode before popping
if (Input.InputManager.Instance != null)
{
Input.InputManager.Instance.SetInputMode(_previousInputMode);
2025-11-10 13:03:36 +01:00
Logging.Debug($"[AlbumViewPage] Restored input mode to {_previousInputMode} on exit");
2025-11-06 13:18:39 +01:00
}
if (UIPageController.Instance != null)
2025-10-15 09:22:13 +02:00
{
UIPageController.Instance.PopPage();
2025-10-15 09:22:13 +02:00
}
}
2025-10-15 09:22:13 +02:00
}
2025-11-06 10:10:54 +01:00
private void OnBoosterCountChanged(int newCount)
{
UpdateBoosterButtons(newCount);
}
private void UpdateBoosterButtons(int boosterCount)
{
if (boosterPackButtons == null || boosterPackButtons.Length == 0) return;
int visibleCount = Mathf.Min(boosterCount, boosterPackButtons.Length);
for (int i = 0; i < boosterPackButtons.Length; i++)
{
if (boosterPackButtons[i] != null)
{
boosterPackButtons[i].SetActive(i < visibleCount);
}
}
}
private void OnBoosterButtonClicked()
{
if (boosterOpeningPage != null && UIPageController.Instance != null)
{
2025-11-06 11:11:15 +01:00
// Pass current booster count to the opening page
int boosterCount = CardSystemManager.Instance?.GetBoosterPackCount() ?? 0;
boosterOpeningPage.SetAvailableBoosterCount(boosterCount);
2025-11-06 10:10:54 +01:00
UIPageController.Instance.PushPage(boosterOpeningPage);
}
}
2025-11-06 13:18:39 +01:00
public override void TransitionIn()
{
// Only store and switch input mode if this is the first time entering
if (Input.InputManager.Instance != null)
2025-11-06 13:18:39 +01:00
{
// Store the current input mode before switching
_previousInputMode = Input.InputMode.GameAndUI;
Input.InputManager.Instance.SetInputMode(Input.InputMode.UI);
2025-11-10 13:03:36 +01:00
Logging.Debug("[AlbumViewPage] Switched to UI-only input mode on first entry");
2025-11-06 13:18:39 +01:00
}
2025-11-10 12:41:28 +01:00
// Only spawn pending cards if we're already on an album page (not the menu)
if (IsInAlbumProper())
{
2025-11-10 13:03:36 +01:00
Logging.Debug("[AlbumViewPage] Opening directly to album page - spawning cards immediately");
2025-11-17 10:59:59 +01:00
SpawnPendingCornerCards();
2025-11-10 12:41:28 +01:00
}
else
{
2025-11-10 13:03:36 +01:00
Logging.Debug("[AlbumViewPage] Opening to menu page - cards will spawn when entering album");
2025-11-10 12:41:28 +01:00
}
2025-11-06 13:18:39 +01:00
base.TransitionIn();
}
public override void TransitionOut()
{
// Clean up active pending cards to prevent duplicates on next opening
2025-11-17 10:59:59 +01:00
CleanupPendingCornerCards();
2025-11-06 13:18:39 +01:00
// Don't restore input mode here - only restore when actually exiting (in OnExitButtonClicked)
base.TransitionOut();
}
protected override void DoTransitionIn(System.Action onComplete)
{
2025-10-20 13:45:56 +02:00
// Simple fade in animation
2025-10-15 09:22:13 +02:00
if (canvasGroup != null)
{
2025-10-15 09:22:13 +02:00
canvasGroup.alpha = 0f;
2025-10-27 15:21:23 +01:00
Tween.Value(0f, 1f, (value) => canvasGroup.alpha = value, transitionDuration, 0f, Tween.EaseInOut, Tween.LoopType.None, null, onComplete, obeyTimescale: false);
}
else
{
2025-10-20 13:45:56 +02:00
// Fallback if no CanvasGroup
onComplete?.Invoke();
}
}
protected override void DoTransitionOut(System.Action onComplete)
{
// Clean up any enlarged card state before closing
CleanupEnlargedCardState();
2025-10-20 13:45:56 +02:00
// Simple fade out animation
2025-10-15 09:22:13 +02:00
if (canvasGroup != null)
{
2025-10-27 15:21:23 +01:00
Tween.Value(canvasGroup.alpha, 0f, (value) => canvasGroup.alpha = value, transitionDuration, 0f, Tween.EaseInOut, Tween.LoopType.None, null, onComplete, obeyTimescale: false);
}
else
{
2025-10-20 13:45:56 +02:00
// Fallback if no CanvasGroup
onComplete?.Invoke();
2025-10-15 09:22:13 +02:00
}
}
/// <summary>
/// Clean up enlarged card state when closing the album
/// </summary>
private void CleanupEnlargedCardState()
{
// Hide backdrop if visible
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);
2025-11-16 20:35:54 +01:00
var card = cardTransform.GetComponent<StateMachine.Card>();
var state = cardTransform.GetComponentInChildren<StateMachine.States.CardAlbumEnlargedState>(true);
if (card != null && state != null)
{
2025-11-16 20:35:54 +01:00
Transform originalParent = state.GetOriginalParent();
if (originalParent != null)
{
cardTransform.SetParent(originalParent, true);
2025-11-16 20:35:54 +01:00
cardTransform.localPosition = state.GetOriginalLocalPosition();
cardTransform.localRotation = state.GetOriginalLocalRotation();
}
}
}
}
}
2025-11-10 12:41:28 +01:00
/// <summary>
/// Check if we're currently viewing the album proper (not the menu page)
/// </summary>
private bool IsInAlbumProper()
{
if (book == null)
{
2025-11-10 13:03:36 +01:00
Logging.Warning("[AlbumViewPage] Book reference is null in IsInAlbumProper check");
2025-11-10 12:41:28 +01:00
return false;
}
// Page 1 is the menu/cover, page 2+ are album pages with card slots
bool inAlbum = book.CurrentPaper > 1;
return inAlbum;
}
/// <summary>
/// Called when book page flips - show/hide pending cards based on whether we're in the album proper
/// </summary>
private void OnPageFlipped()
{
bool isInAlbum = IsInAlbumProper();
2025-11-17 10:59:59 +01:00
if (isInAlbum && _pendingCornerCards.Count == 0)
2025-11-10 12:41:28 +01:00
{
// Entering album proper and no cards spawned yet - spawn them with animation
2025-11-10 13:03:36 +01:00
Logging.Debug("[AlbumViewPage] Entering album proper - spawning pending cards with animation");
2025-11-17 10:59:59 +01:00
SpawnPendingCornerCards();
2025-11-10 12:41:28 +01:00
}
2025-11-17 10:59:59 +01:00
else if (!isInAlbum && _pendingCornerCards.Count > 0)
2025-11-10 12:41:28 +01:00
{
// Returning to menu page - cleanup cards
2025-11-10 13:03:36 +01:00
Logging.Debug("[AlbumViewPage] Returning to menu page - cleaning up pending cards");
2025-11-17 10:59:59 +01:00
CleanupPendingCornerCards();
2025-11-10 12:41:28 +01:00
}
else
{
2025-11-10 13:03:36 +01:00
Logging.Debug($"[AlbumViewPage] Page flipped but no card state change needed (already in correct state)");
2025-11-10 12:41:28 +01:00
}
}
2025-11-16 20:35:54 +01:00
#region Card Enlarge System (Album Slots)
/// <summary>
2025-11-16 20:35:54 +01:00
/// Subscribe to a placed card's enlarged state events to manage backdrop and reparenting.
/// Called by AlbumCardSlot when it spawns an owned card in a slot.
/// </summary>
2025-11-16 20:35:54 +01:00
public void RegisterCardInAlbum(StateMachine.Card card)
{
2025-11-16 20:35:54 +01:00
if (card == null) return;
var enlargeState = card.GetStateComponent<StateMachine.States.CardAlbumEnlargedState>("AlbumEnlargedState");
if (enlargeState != null)
{
enlargeState.OnEnlargeRequested += OnCardEnlargeRequested;
enlargeState.OnShrinkRequested += OnCardShrinkRequested;
}
}
2025-11-16 20:35:54 +01:00
public void UnregisterCardInAlbum(StateMachine.Card card)
{
2025-11-16 20:35:54 +01:00
if (card == null) return;
var enlargeState = card.GetStateComponent<StateMachine.States.CardAlbumEnlargedState>("AlbumEnlargedState");
if (enlargeState != null)
{
enlargeState.OnEnlargeRequested -= OnCardEnlargeRequested;
enlargeState.OnShrinkRequested -= OnCardShrinkRequested;
}
}
2025-11-16 20:35:54 +01:00
private void OnCardEnlargeRequested(StateMachine.States.CardAlbumEnlargedState state)
{
2025-11-16 20:35:54 +01:00
if (state == null) return;
// Show backdrop
if (cardEnlargedBackdrop != null)
{
cardEnlargedBackdrop.SetActive(true);
}
2025-11-16 20:35:54 +01:00
// Reparent card root to enlarged container preserving world transform
if (cardEnlargedContainer != null)
{
2025-11-16 20:35:54 +01:00
var ctx = state.GetComponentInParent<StateMachine.CardContext>();
if (ctx != null)
{
ctx.RootTransform.SetParent(cardEnlargedContainer, true);
ctx.RootTransform.SetAsLastSibling();
}
}
}
2025-11-16 20:35:54 +01:00
private void OnCardShrinkRequested(StateMachine.States.CardAlbumEnlargedState state)
{
2025-11-16 20:35:54 +01:00
if (state == null) return;
2025-11-17 23:49:37 +01:00
// Hide backdrop; state will animate back to slot and reparent on completion
if (cardEnlargedBackdrop != null)
{
cardEnlargedBackdrop.SetActive(false);
}
2025-11-17 23:49:37 +01:00
// Do not reparent here; reverse animation is orchestrated by the state
}
2025-11-16 20:35:54 +01:00
#endregion
2025-11-17 17:10:24 +01:00
/// <summary>
2025-11-16 20:35:54 +01:00
/// Find a slot by its SlotIndex property
/// </summary>
2025-11-16 20:35:54 +01:00
private DraggableSlot FindSlotByIndex(int slotIndex)
{
2025-11-16 20:35:54 +01:00
if (bottomRightSlots == null)
return null;
2025-11-16 20:35:54 +01:00
foreach (var slot in bottomRightSlots.Slots)
{
2025-11-16 20:35:54 +01:00
if (slot.SlotIndex == slotIndex)
{
return slot;
}
}
2025-11-16 20:35:54 +01:00
return null;
}
2025-11-17 08:39:41 +01:00
public void SpawnPendingCornerCards()
2025-11-17 17:10:24 +01:00
{
RebuildCornerCards();
}
/// <summary>
2025-11-17 23:38:34 +01:00
/// 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
2025-11-17 17:10:24 +01:00
/// Called on initial spawn and after card is removed.
/// </summary>
private void RebuildCornerCards()
2025-11-17 08:39:41 +01:00
{
if (cardPrefab == null || bottomRightSlots == null) return;
2025-11-17 17:10:24 +01:00
2025-11-17 23:38:34 +01:00
// Step 1: Determine how many cards should be displayed
2025-11-17 14:30:07 +01:00
var uniquePending = GetUniquePendingCards();
2025-11-17 17:10:24 +01:00
int totalPendingCards = uniquePending.Count;
int cardsToDisplay = Mathf.Min(totalPendingCards, MaxPendingCorner);
2025-11-17 14:30:07 +01:00
2025-11-17 23:38:34 +01:00
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)
2025-11-17 14:30:07 +01:00
{
2025-11-17 23:38:34 +01:00
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}");
2025-11-17 14:30:07 +01:00
}
2025-11-17 23:38:34 +01:00
// Step 3: Shuffle remaining cards to occupy first slots (0, 1, 2)
ShuffleCardsToFrontSlots();
2025-11-17 14:30:07 +01:00
2025-11-17 23:38:34 +01:00
// Step 4: Spawn new cards in remaining slots if needed
while (_pendingCornerCards.Count < cardsToDisplay)
2025-11-17 08:39:41 +01:00
{
2025-11-17 23:38:34 +01:00
int slotIndex = _pendingCornerCards.Count; // Next available slot
2025-11-17 17:10:24 +01:00
var slot = FindSlotByIndex(slotIndex);
2025-11-17 23:38:34 +01:00
2025-11-17 17:10:24 +01:00
if (slot == null)
2025-11-17 08:39:41 +01:00
{
2025-11-17 17:10:24 +01:00
Logging.Warning($"[AlbumViewPage] Slot {slotIndex} not found, stopping spawn");
break;
2025-11-17 08:39:41 +01:00
}
2025-11-17 17:10:24 +01:00
SpawnCardInSlot(slot);
2025-11-17 23:38:34 +01:00
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}");
2025-11-17 08:39:41 +01:00
}
}
2025-11-17 17:10:24 +01:00
/// <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;
}
2025-11-17 23:38:34 +01:00
// IMPORTANT: Assign to slot FIRST (establishes correct transform)
2025-11-17 17:10:24 +01:00
card.AssignToSlot(slot, false); // false = instant, no animation
2025-11-17 23:38:34 +01:00
// THEN setup card for pending state (transitions to PendingFaceDownState)
// This ensures OriginalScale is captured AFTER slot assignment
card.SetupForAlbumPending();
2025-11-17 17:10:24 +01:00
// 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
}
2025-11-17 14:30:07 +01:00
/// <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);
}
2025-11-17 08:39:41 +01:00
private void CleanupPendingCornerCards()
{
2025-11-17 17:10:24 +01:00
// First, unsubscribe and destroy tracked cards
2025-11-17 08:39:41 +01:00
foreach (var c in _pendingCornerCards)
{
2025-11-17 10:59:59 +01:00
if (c != null)
{
if (c.Context != null)
{
c.Context.OnDragStarted -= OnCardDragStarted;
}
Destroy(c.gameObject);
}
2025-11-17 08:39:41 +01:00
}
_pendingCornerCards.Clear();
2025-11-17 10:59:59 +01:00
2025-11-17 17:10:24 +01:00
// 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)
2025-11-17 14:30:07 +01:00
{
2025-11-17 17:10:24 +01:00
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}");
}
}
}
2025-11-17 14:30:07 +01:00
}
}
2025-11-17 17:10:24 +01:00
#region Query Methods for Card States (Data Providers)
/// <summary>
/// Query method: Get card data for a pending slot.
/// Called by PendingFaceDownState when drag starts.
/// IMPORTANT: This removes the card from pending list immediately, then rebuilds corner.
/// </summary>
public CardData GetCardForPendingSlot()
2025-11-17 14:30:07 +01:00
{
2025-11-17 17:10:24 +01:00
if (CardSystemManager.Instance == null) return null;
var pending = CardSystemManager.Instance.GetPendingRevealCards();
if (pending.Count == 0) return null;
2025-11-17 14:30:07 +01:00
2025-11-17 17:10:24 +01:00
// Try current page match
var pageDefs = GetDefinitionsOnCurrentPage();
var match = pending.Find(c => pageDefs.Contains(c.DefinitionId));
2025-11-17 14:30:07 +01:00
2025-11-17 17:10:24 +01:00
// If no match, use random
if (match == null)
{
int idx = Random.Range(0, pending.Count);
match = pending[idx];
}
2025-11-17 14:30:07 +01:00
2025-11-17 17:10:24 +01:00
// IMPORTANT: Remove from pending list immediately
// Card is now in "reveal flow" and will be added to collection when placed
if (match != null)
2025-11-17 14:30:07 +01:00
{
2025-11-17 17:10:24 +01:00
// 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");
2025-11-17 14:30:07 +01:00
2025-11-17 17:10:24 +01:00
// Rebuild corner cards AFTER removing from pending list
// This ensures the removed card doesn't get re-spawned
RebuildCornerCards();
2025-11-17 14:30:07 +01:00
}
2025-11-17 17:10:24 +01:00
return match;
2025-11-17 14:30:07 +01:00
}
/// <summary>
2025-11-17 17:10:24 +01:00
/// Query method: Get target slot for a card.
/// Called by PendingFaceDownState to find where card should go.
2025-11-17 14:30:07 +01:00
/// </summary>
2025-11-17 17:10:24 +01:00
public AlbumCardSlot GetTargetSlotForCard(CardData cardData)
2025-11-17 14:30:07 +01:00
{
if (cardData == null) return null;
var allSlots = FindObjectsByType<AlbumCardSlot>(FindObjectsSortMode.None);
foreach (var slot in allSlots)
{
if (slot.TargetCardDefinition != null &&
slot.TargetCardDefinition.Id == cardData.DefinitionId)
{
return slot;
}
2025-11-17 08:39:41 +01:00
}
2025-11-17 10:59:59 +01:00
2025-11-17 14:30:07 +01:00
return null;
}
2025-11-17 17:10:24 +01:00
/// <summary>
/// Service method: Navigate to the page for a specific card.
/// Called by PendingFaceDownState to flip book to correct zone page.
/// </summary>
public void NavigateToCardPage(CardData cardData, System.Action onComplete)
2025-11-17 14:30:07 +01:00
{
2025-11-17 17:10:24 +01:00
if (cardData == null || book == null)
{
onComplete?.Invoke();
return;
}
2025-11-17 14:30:07 +01:00
2025-11-17 17:10:24 +01:00
// Find target page based on card's zone
int targetPage = FindPageForZone(cardData.Zone);
2025-11-17 14:30:07 +01:00
2025-11-17 17:10:24 +01:00
if (targetPage < 0)
{
Logging.Warning($"[AlbumViewPage] No page found for zone {cardData.Zone}");
onComplete?.Invoke();
return;
}
2025-11-17 14:30:07 +01:00
2025-11-17 17:10:24 +01:00
// Mark as flipping
_isPageFlipping = true;
Logging.Debug($"[AlbumViewPage] Starting page flip to page {targetPage}");
2025-11-17 14:30:07 +01:00
2025-11-17 17:10:24 +01:00
// Get or add AutoFlip component
BookCurlPro.AutoFlip autoFlip = book.GetComponent<BookCurlPro.AutoFlip>();
if (autoFlip == null)
{
autoFlip = book.gameObject.AddComponent<BookCurlPro.AutoFlip>();
}
2025-11-17 14:30:07 +01:00
2025-11-17 17:10:24 +01:00
// 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();
});
2025-11-17 14:30:07 +01:00
}
2025-11-17 17:10:24 +01:00
/// <summary>
/// Notify that a card has been placed (for cleanup).
/// Called by PlacedInSlotState after placement is complete.
/// </summary>
public void NotifyCardPlaced(StateMachine.Card card)
2025-11-17 14:30:07 +01:00
{
2025-11-17 17:10:24 +01:00
if (card != null)
{
// Remove from tracking list
_pendingCornerCards.Remove(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}");
}
2025-11-17 08:39:41 +01:00
}
2025-11-17 17:10:24 +01:00
#endregion
#region Helper Methods
2025-11-17 08:39:41 +01:00
2025-11-17 14:30:07 +01:00
/// <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("[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;
}
2025-11-17 08:39:41 +01:00
private List<string> GetDefinitionsOnCurrentPage()
{
2025-11-17 10:59:59 +01:00
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;
2025-11-17 08:39:41 +01:00
}
2025-11-17 14:30:07 +01:00
#endregion
}
}