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:
2025-11-18 08:40:59 +00:00
parent 06cc3bde3b
commit 235fa04eba
161 changed files with 18057 additions and 5743 deletions

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 27f0915ceda44ccf9415c3f344645b38
timeCreated: 1759923720

View File

@@ -1,193 +0,0 @@
using System;
using AppleHills.Data.CardSystem;
using Core;
using Pixelplacement;
using UnityEngine;
using UnityEngine.EventSystems;
namespace UI.CardSystem
{
/// <summary>
/// Album card component that wraps CardDisplay.
/// Handles tap-to-enlarge and tap-to-shrink interactions for cards placed in album slots.
///
/// TODO: Consider refactoring to state machine pattern (PendingReveal, PlacedInSlot, Enlarged)
/// This would eliminate the need for separate AlbumPlacementCard wrapper and simplify the hierarchy.
/// See design discussion with state transitions for cleaner architecture.
/// </summary>
public class AlbumCard : MonoBehaviour, IPointerClickHandler
{
[Header("References")]
[SerializeField] private CardDisplay cardDisplay;
[Header("Enlarge Settings")]
[SerializeField] private float enlargedScale = 2.5f;
[SerializeField] private float scaleDuration = 0.3f;
// Events for AlbumViewPage to manage backdrop and reparenting
public event Action<AlbumCard> OnEnlargeRequested;
public event Action<AlbumCard> OnShrinkRequested;
private AlbumCardSlot _parentSlot;
private CardData _cardData;
private bool _isEnlarged;
private Vector3 _originalScale;
private Transform _originalParent;
private Vector3 _originalLocalPosition;
private Quaternion _originalLocalRotation;
private void Awake()
{
// Auto-find CardDisplay if not assigned
if (cardDisplay == null)
{
cardDisplay = GetComponentInChildren<CardDisplay>();
}
// Store original scale
_originalScale = transform.localScale;
}
/// <summary>
/// Setup card with data
/// </summary>
public void SetupCard(CardData data)
{
_cardData = data;
if (cardDisplay != null)
{
cardDisplay.SetupCard(data);
}
}
/// <summary>
/// Set the parent slot this card belongs to
/// </summary>
public void SetParentSlot(AlbumCardSlot slot)
{
_parentSlot = slot;
}
/// <summary>
/// Get the card data
/// </summary>
public CardData GetCardData()
{
return _cardData;
}
/// <summary>
/// Handle tap on card - request enlarge/shrink from parent page
/// Only process clicks when card is placed in a slot (not during reveal flow)
/// </summary>
public void OnPointerClick(PointerEventData eventData)
{
Logging.Debug($"[CLICK-TRACE-ALBUMCARD] OnPointerClick on {name}, _parentSlot={((_parentSlot != null) ? _parentSlot.name : "NULL")}, _isEnlarged={_isEnlarged}, position={eventData.position}");
// During reveal flow (before placed in slot), forward clicks to parent FlippableCard
if (_parentSlot == null)
{
Logging.Debug($"[CLICK-TRACE-ALBUMCARD] {name} - No parent slot, forwarding click to parent FlippableCard");
// Find parent FlippableCard and forward the click
FlippableCard parentFlippable = GetComponentInParent<FlippableCard>();
if (parentFlippable != null)
{
Logging.Debug($"[CLICK-TRACE-ALBUMCARD] {name} - Found parent FlippableCard, calling OnPointerClick");
parentFlippable.OnPointerClick(eventData);
}
else
{
Logging.Warning($"[CLICK-TRACE-ALBUMCARD] {name} - No parent FlippableCard found!");
}
return;
}
Logging.Debug($"[CLICK-TRACE-ALBUMCARD] {name} - Has parent slot, processing click");
if (_isEnlarged)
{
Logging.Debug($"[CLICK-TRACE-ALBUMCARD] {name} - Is enlarged, requesting shrink");
OnShrinkRequested?.Invoke(this);
}
else
{
Logging.Debug($"[CLICK-TRACE-ALBUMCARD] {name} - Is normal size, requesting enlarge");
OnEnlargeRequested?.Invoke(this);
}
}
/// <summary>
/// Enlarge card (called by AlbumViewPage after reparenting)
/// </summary>
public void EnlargeCard()
{
if (_isEnlarged) return;
_isEnlarged = true;
// Store original transform info for restoration
_originalParent = transform.parent;
_originalLocalPosition = transform.localPosition;
_originalLocalRotation = transform.localRotation;
// Scale up with snappy tween
Tween.LocalScale(transform, _originalScale * enlargedScale, scaleDuration, 0f, Tween.EaseOutBack);
}
/// <summary>
/// Shrink card back to original size (called by AlbumViewPage before reparenting back)
/// </summary>
/// <param name="onComplete">Optional callback to invoke when shrink animation completes</param>
public void ShrinkCard(System.Action onComplete = null)
{
if (!_isEnlarged) return;
_isEnlarged = false;
// Scale back down with snappy tween, invoke callback when done
Tween.LocalScale(transform, _originalScale, scaleDuration, 0f, Tween.EaseInBack,
completeCallback: () => onComplete?.Invoke());
}
/// <summary>
/// Get original parent for restoration
/// </summary>
public Transform GetOriginalParent()
{
return _originalParent;
}
/// <summary>
/// Get original local position for restoration
/// </summary>
public Vector3 GetOriginalLocalPosition()
{
return _originalLocalPosition;
}
/// <summary>
/// Get original local rotation for restoration
/// </summary>
public Quaternion GetOriginalLocalRotation()
{
return _originalLocalRotation;
}
/// <summary>
/// Check if card is currently enlarged
/// </summary>
public bool IsEnlarged => _isEnlarged;
/// <summary>
/// Force reset enlarged state (for cleanup scenarios like page closing)
/// </summary>
public void ForceResetEnlargedState()
{
_isEnlarged = false;
transform.localScale = _originalScale;
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 258a530448814715b5ec19737df2a658
timeCreated: 1762505823

View File

@@ -1,849 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using AppleHills.Data.CardSystem;
using Core;
using Data.CardSystem;
using Pixelplacement;
using UI.Core;
using UI.DragAndDrop.Core;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
namespace UI.CardSystem
{
/// <summary>
/// UI page for viewing the player's card collection in an album.
/// Manages booster pack button visibility and opening flow.
/// </summary>
public class AlbumViewPage : UIPage
{
[Header("UI References")]
[SerializeField] private CanvasGroup canvasGroup;
[SerializeField] private Button exitButton;
[SerializeField] private BookCurlPro.BookPro book;
[Header("Zone Navigation")]
[SerializeField] private Transform tabContainer; // Container holding all BookTabButton children
private BookTabButton[] zoneTabs; // Discovered zone tab buttons
[Header("Album Card Reveal")]
[SerializeField] private SlotContainer bottomRightSlots;
[SerializeField] private GameObject albumCardPlacementPrefab; // The wrapper prefab with flip/drag (AlbumPlacementCard)
[Header("Card Enlarge System")]
[SerializeField] private GameObject cardEnlargedBackdrop; // Backdrop to block interactions
[SerializeField] private Transform cardEnlargedContainer; // Container for enlarged cards (sits above backdrop)
[Header("Booster Pack UI")]
[SerializeField] private GameObject[] boosterPackButtons;
[SerializeField] private BoosterOpeningPage boosterOpeningPage;
private Input.InputMode _previousInputMode;
private List<AlbumCardPlacementDraggable> _activeCards = new List<AlbumCardPlacementDraggable>();
private const int MAX_VISIBLE_CARDS = 3;
internal override void OnManagedStart()
{
// Discover zone tabs from container
DiscoverZoneTabs();
// Make sure we have a CanvasGroup for transitions
if (canvasGroup == null)
canvasGroup = GetComponent<CanvasGroup>();
if (canvasGroup == null)
canvasGroup = gameObject.AddComponent<CanvasGroup>();
// Hide backdrop initially
if (cardEnlargedBackdrop != null)
{
cardEnlargedBackdrop.SetActive(false);
}
// Set up exit button
if (exitButton != null)
{
exitButton.onClick.AddListener(OnExitButtonClicked);
}
// Set up booster pack button listeners
SetupBoosterButtonListeners();
// Subscribe to book page flip events
if (book != null)
{
book.OnFlip.AddListener(OnPageFlipped);
Logging.Debug("[AlbumViewPage] Subscribed to book.OnFlip event");
}
else
{
Logging.Warning("[AlbumViewPage] Book reference is null, cannot subscribe to OnFlip event!");
}
// Subscribe to CardSystemManager events (managers are guaranteed to be initialized)
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
// Update initial button visibility
int initialCount = CardSystemManager.Instance.GetBoosterPackCount();
UpdateBoosterButtons(initialCount);
}
// UI pages should start disabled
gameObject.SetActive(false);
}
/// <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.");
zoneTabs = new BookTabButton[0];
return;
}
// Get all BookTabButton components from children
zoneTabs = tabContainer.GetComponentsInChildren<BookTabButton>(includeInactive: false);
if (zoneTabs == null || zoneTabs.Length == 0)
{
Logging.Warning($"[AlbumViewPage] No BookTabButton components found in tab container '{tabContainer.name}'!");
zoneTabs = new BookTabButton[0];
}
else
{
Logging.Debug($"[AlbumViewPage] Discovered {zoneTabs.Length} zone tabs from container '{tabContainer.name}'");
foreach (var tab in zoneTabs)
{
Logging.Debug($" - Tab: {tab.name}, Zone: {tab.Zone}, TargetPage: {tab.TargetPage}");
}
}
}
private void SetupBoosterButtonListeners()
{
if (boosterPackButtons == null) return;
for (int i = 0; i < boosterPackButtons.Length; i++)
{
if (boosterPackButtons[i] == null) continue;
// Unsubscribe from book events
if (book != null)
{
book.OnFlip.RemoveListener(OnPageFlipped);
}
Button button = boosterPackButtons[i].GetComponent<Button>();
if (button != null)
{
button.onClick.AddListener(OnBoosterButtonClicked);
}
}
}
internal override void OnManagedDestroy()
{
// Unsubscribe from CardSystemManager
if (CardSystemManager.Instance != null)
{
CardSystemManager.Instance.OnBoosterCountChanged -= OnBoosterCountChanged;
// NOTE: OnPendingCardAdded is unsubscribed in TransitionOut
}
// Clean up exit button
if (exitButton != null)
{
exitButton.onClick.RemoveListener(OnExitButtonClicked);
}
// 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);
}
}
}
// Clean up active cards
CleanupActiveCards();
}
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)
{
autoFlip = book.gameObject.AddComponent<BookCurlPro.AutoFlip>();
}
autoFlip.enabled = true;
autoFlip.StartFlipping(1);
}
else
{
// Already on page 0 or no book reference, exit
// Restore input mode before popping
if (Input.InputManager.Instance != null)
{
Input.InputManager.Instance.SetInputMode(_previousInputMode);
Logging.Debug($"[AlbumViewPage] Restored input mode to {_previousInputMode} on exit");
}
if (UIPageController.Instance != null)
{
UIPageController.Instance.PopPage();
}
}
}
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)
{
// Pass current booster count to the opening page
int boosterCount = CardSystemManager.Instance?.GetBoosterPackCount() ?? 0;
boosterOpeningPage.SetAvailableBoosterCount(boosterCount);
UIPageController.Instance.PushPage(boosterOpeningPage);
}
}
public override void TransitionIn()
{
// Only store and switch input mode if this is the first time entering
// (when _previousInputMode hasn't been set yet)
if (Input.InputManager.Instance != null)
{
// Store the current input mode before switching
_previousInputMode = Input.InputMode.GameAndUI;
Input.InputManager.Instance.SetInputMode(Input.InputMode.UI);
Logging.Debug("[AlbumViewPage] Switched to UI-only input mode on first entry");
}
// Subscribe to pending card events while page is active
if (CardSystemManager.Instance != null)
{
CardSystemManager.Instance.OnPendingCardAdded += OnPendingCardAdded;
}
// Only spawn pending cards if we're already on an album page (not the menu)
if (IsInAlbumProper())
{
Logging.Debug("[AlbumViewPage] Opening directly to album page - spawning cards immediately");
SpawnPendingCards();
}
else
{
Logging.Debug("[AlbumViewPage] Opening to menu page - cards will spawn when entering album");
}
base.TransitionIn();
}
public override void TransitionOut()
{
// Unsubscribe from pending card events when page closes
if (CardSystemManager.Instance != null)
{
CardSystemManager.Instance.OnPendingCardAdded -= OnPendingCardAdded;
}
// Clean up active pending cards to prevent duplicates on next opening
CleanupActiveCards();
// Don't restore input mode here - only restore when actually exiting (in OnExitButtonClicked)
base.TransitionOut();
}
protected override void DoTransitionIn(System.Action onComplete)
{
// Simple fade in animation
if (canvasGroup != null)
{
canvasGroup.alpha = 0f;
Tween.Value(0f, 1f, (value) => canvasGroup.alpha = value, transitionDuration, 0f, Tween.EaseInOut, Tween.LoopType.None, null, onComplete, obeyTimescale: false);
}
else
{
// Fallback if no CanvasGroup
onComplete?.Invoke();
}
}
protected override void DoTransitionOut(System.Action onComplete)
{
// Clean up any enlarged card state before closing
CleanupEnlargedCardState();
// Simple fade out animation
if (canvasGroup != null)
{
Tween.Value(canvasGroup.alpha, 0f, (value) => canvasGroup.alpha = value, transitionDuration, 0f, Tween.EaseInOut, Tween.LoopType.None, null, onComplete, obeyTimescale: false);
}
else
{
// Fallback if no CanvasGroup
onComplete?.Invoke();
}
}
/// <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)
{
// Get all enlarged cards (should only be one, but just in case)
for (int i = cardEnlargedContainer.childCount - 1; i >= 0; i--)
{
Transform cardTransform = cardEnlargedContainer.GetChild(i);
AlbumCard albumCard = cardTransform.GetComponent<AlbumCard>();
if (albumCard != null && albumCard.IsEnlarged)
{
// Force reset state and return to slot
Transform originalParent = albumCard.GetOriginalParent();
if (originalParent != null)
{
cardTransform.SetParent(originalParent, true);
cardTransform.localPosition = albumCard.GetOriginalLocalPosition();
cardTransform.localRotation = albumCard.GetOriginalLocalRotation();
}
albumCard.ForceResetEnlargedState();
}
}
}
}
/// <summary>
/// Check if we're currently viewing the album proper (not the menu page)
/// </summary>
private bool IsInAlbumProper()
{
if (book == null)
{
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>
/// 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();
if (isInAlbum && _activeCards.Count == 0)
{
// Entering album proper and no cards spawned yet - spawn them with animation
Logging.Debug("[AlbumViewPage] Entering album proper - spawning pending cards with animation");
SpawnPendingCards();
}
else if (!isInAlbum && _activeCards.Count > 0)
{
// Returning to menu page - cleanup cards
Logging.Debug("[AlbumViewPage] Returning to menu page - cleaning up pending cards");
CleanupActiveCards();
}
else
{
Logging.Debug($"[AlbumViewPage] Page flipped but no card state change needed (already in correct state)");
}
}
#region Album Card Reveal System
/// <summary>
/// Spawn pending cards from CardSystemManager
/// Only spawns unique cards (one per definition+rarity, not one per copy)
/// </summary>
private void SpawnPendingCards()
{
if (CardSystemManager.Instance == null || bottomRightSlots == null || albumCardPlacementPrefab == null)
return;
var pending = CardSystemManager.Instance.GetPendingRevealCards();
// Get unique cards only (by DefinitionId + Rarity)
// Filter out cards with CopiesOwned = 0 (shouldn't happen but guard against it)
var uniquePending = pending
.Where(c => c.CopiesOwned > 0) // Guard: exclude zero-count cards
.GroupBy(c => new { c.DefinitionId, c.Rarity })
.Select(g => g.First()) // Take first instance of each unique card
.ToList();
int spawnCount = Mathf.Min(uniquePending.Count, MAX_VISIBLE_CARDS);
Logging.Debug($"[AlbumViewPage] Spawning {spawnCount} unique pending cards (total pending: {pending.Count})");
for (int i = 0; i < spawnCount; i++)
{
SpawnCardInSlot(i, uniquePending[i]);
}
}
/// <summary>
/// Spawn a card in a specific slot
/// </summary>
private void SpawnCardInSlot(int slotIndex, CardData cardData)
{
// Guard: Don't spawn cards with zero copies
if (cardData.CopiesOwned <= 0)
{
Logging.Warning($"[AlbumViewPage] Skipping spawn of card '{cardData.Name}' with {cardData.CopiesOwned} copies");
return;
}
DraggableSlot slot = FindSlotByIndex(slotIndex);
if (slot == null)
{
Logging.Warning($"[AlbumViewPage] Could not find slot with SlotIndex {slotIndex}");
return;
}
// Instantiate card directly as child of the slot container (not the slot itself, not canvas root)
// This keeps it in the correct UI hierarchy
GameObject cardObj = Instantiate(albumCardPlacementPrefab, bottomRightSlots.transform);
AlbumCardPlacementDraggable cardPlacement = cardObj.GetComponent<AlbumCardPlacementDraggable>();
if (cardPlacement != null)
{
// Setup card data
cardPlacement.SetupCard(cardData);
// Subscribe to events
cardPlacement.OnCardRevealed += OnCardRevealed;
cardPlacement.OnCardPlacedInAlbum += OnCardPlacedInAlbum;
// NOW assign to slot - this will:
// 1. Reparent to slot
// 2. Apply slot's occupantSizeMode scaling
// 3. Animate to slot position
cardPlacement.AssignToSlot(slot, true);
// Track it
_activeCards.Add(cardPlacement);
Logging.Debug($"[AlbumViewPage] Spawned card '{cardData.Name}' (CopiesOwned: {cardData.CopiesOwned}) in slot {slotIndex}");
}
else
{
Logging.Warning($"[AlbumViewPage] Spawned card has no AlbumCardDraggable component!");
Destroy(cardObj);
}
}
/// <summary>
/// Handle when a new card is added to pending queue
/// Only spawn if this unique card isn't already visualized
/// </summary>
private void OnPendingCardAdded(CardData card)
{
// Guard: Don't spawn cards with zero copies
if (card.CopiesOwned <= 0)
{
Logging.Warning($"[AlbumViewPage] Ignoring pending card '{card.Name}' with {card.CopiesOwned} copies");
return;
}
// Check if we already have a card with this definition + rarity spawned
bool alreadySpawned = _activeCards.Any(c =>
c.CardData.DefinitionId == card.DefinitionId &&
c.CardData.Rarity == card.Rarity);
if (alreadySpawned)
{
Logging.Debug($"[AlbumViewPage] Card '{card.Name}' already spawned, skipping duplicate spawn");
return; // Don't spawn duplicates
}
// Try to spawn if we have space
if (_activeCards.Count < MAX_VISIBLE_CARDS)
{
int nextSlotIndex = _activeCards.Count;
SpawnCardInSlot(nextSlotIndex, card);
}
}
/// <summary>
/// Handle when a card is revealed (flipped)
/// </summary>
private void OnCardRevealed(AlbumCardPlacementDraggable cardPlacement, CardData cardData)
{
Logging.Debug($"[AlbumViewPage] Card revealed: {cardData.Name} (Zone: {cardData.Zone}, CopiesOwned: {cardData.CopiesOwned})");
// IMMEDIATELY move card from pending to inventory upon reveal
if (CardSystemManager.Instance != null)
{
CardSystemManager.Instance.MarkCardAsPlaced(cardData);
Logging.Debug($"[AlbumViewPage] Moved card '{cardData.Name}' from pending to inventory on reveal");
}
// Remove this card from active cards list
_activeCards.Remove(cardPlacement);
// Check if we're currently viewing the correct zone for this card
CardZone currentZone = GetCurrentZone();
if (currentZone != cardData.Zone)
{
// Card is from a different zone - navigate to its zone
Logging.Debug($"[AlbumViewPage] Card zone ({cardData.Zone}) doesn't match current zone ({currentZone}). Navigating to card's zone...");
NavigateToZone(cardData.Zone);
}
else
{
Logging.Debug($"[AlbumViewPage] Card zone ({cardData.Zone}) matches current zone - no navigation needed.");
}
// Shuffle remaining cards to front and spawn next unique card
ShuffleCardsToFront();
TrySpawnNextCard();
}
/// <summary>
/// Handle when a card is placed in the album (from AlbumCardDraggable)
/// Card data already moved to inventory in OnCardRevealed
/// This just handles cleanup
/// </summary>
private void OnCardPlacedInAlbum(AlbumCardPlacementDraggable cardPlacement, CardData cardData)
{
Logging.Debug($"[AlbumViewPage] Card placed in album slot: {cardData.Name}");
// Unsubscribe from events (card is now static in album)
cardPlacement.OnCardRevealed -= OnCardRevealed;
cardPlacement.OnCardPlacedInAlbum -= OnCardPlacedInAlbum;
// Note: Card already removed from _activeCards in OnCardRevealed
// Note: Shuffle and spawn already done in OnCardRevealed
}
/// <summary>
/// Shuffle active cards to occupy front slots
/// </summary>
private void ShuffleCardsToFront()
{
if (bottomRightSlots == null || _activeCards.Count == 0)
return;
// Convert to base DraggableObject list for helper method
List<DraggableObject> draggableList = _activeCards.Cast<DraggableObject>().ToList();
SlotContainerHelper.ShuffleToFront(bottomRightSlots, draggableList, animate: true);
}
/// <summary>
/// Try to spawn the next pending card
/// Only spawns unique cards (not duplicates)
/// </summary>
private void TrySpawnNextCard()
{
if (CardSystemManager.Instance == null)
return;
if (_activeCards.Count >= MAX_VISIBLE_CARDS)
return; // Already at max
var pending = CardSystemManager.Instance.GetPendingRevealCards();
// Get unique pending cards, excluding zero-count cards
var uniquePending = pending
.Where(c => c.CopiesOwned > 0) // Guard: exclude zero-count cards
.GroupBy(c => new { c.DefinitionId, c.Rarity })
.Select(g => g.First())
.ToList();
// Find first unique card that's not already spawned
foreach (var cardData in uniquePending)
{
bool alreadySpawned = _activeCards.Any(c =>
c.CardData.DefinitionId == cardData.DefinitionId &&
c.CardData.Rarity == cardData.Rarity);
if (!alreadySpawned)
{
int nextSlotIndex = _activeCards.Count;
SpawnCardInSlot(nextSlotIndex, cardData);
break;
}
}
}
/// <summary>
/// Find a slot by its SlotIndex property
/// </summary>
private DraggableSlot FindSlotByIndex(int slotIndex)
{
if (bottomRightSlots == null)
return null;
foreach (var slot in bottomRightSlots.Slots)
{
if (slot.SlotIndex == slotIndex)
{
return slot;
}
}
return null;
}
/// <summary>
/// Get the current zone based on book page
/// </summary>
public CardZone GetCurrentZone()
{
if (book == null || zoneTabs == null || zoneTabs.Length == 0)
{
return CardZone.AppleHills; // Default
}
int currentPage = book.CurrentPaper;
// Find tab with matching target page
foreach (var tab in zoneTabs)
{
if (tab.TargetPage == currentPage)
{
return tab.Zone;
}
}
// Fallback to first zone
return CardZone.NotApplicable;
}
/// <summary>
/// Get tab for a specific zone
/// </summary>
public BookTabButton GetTabForZone(CardZone zone)
{
if (zoneTabs == null)
{
return null;
}
foreach (var tab in zoneTabs)
{
if (tab.Zone == zone)
{
return tab;
}
}
return null;
}
/// <summary>
/// Navigate to a specific zone
/// </summary>
public void NavigateToZone(CardZone zone)
{
BookTabButton tab = GetTabForZone(zone);
if (tab != null)
{
tab.ActivateTab();
}
}
/// <summary>
/// Clean up all active cards
/// </summary>
private void CleanupActiveCards()
{
foreach (var card in _activeCards)
{
if (card != null && card.gameObject != null)
{
card.OnCardRevealed -= OnCardRevealed;
card.OnCardPlacedInAlbum -= OnCardPlacedInAlbum;
Destroy(card.gameObject);
}
}
_activeCards.Clear();
}
#endregion
#region Card Enlarge System
/// <summary>
/// Subscribe to album card events when a card is spawned in a slot
/// Call this when AlbumCardSlot spawns a card
/// </summary>
public void RegisterAlbumCard(AlbumCard albumCard)
{
if (albumCard == null) return;
albumCard.OnEnlargeRequested += OnCardEnlargeRequested;
albumCard.OnShrinkRequested += OnCardShrinkRequested;
}
/// <summary>
/// Unsubscribe from album card events
/// </summary>
public void UnregisterAlbumCard(AlbumCard albumCard)
{
if (albumCard == null) return;
albumCard.OnEnlargeRequested -= OnCardEnlargeRequested;
albumCard.OnShrinkRequested -= OnCardShrinkRequested;
}
/// <summary>
/// Handle card enlarge request - show backdrop and reparent card
/// </summary>
private void OnCardEnlargeRequested(AlbumCard card)
{
if (card == null) return;
Logging.Debug($"[AlbumViewPage] OnCardEnlargeRequested called for card: {card.name}, current parent: {card.transform.parent.name}");
// IMPORTANT: Call EnlargeCard FIRST to store original parent (the slot)
// BEFORE reparenting to the enlarged container
card.EnlargeCard();
// Show backdrop
if (cardEnlargedBackdrop != null)
{
cardEnlargedBackdrop.SetActive(true);
Logging.Debug($"[AlbumViewPage] Backdrop shown");
}
// NOW reparent card to enlarged container (above backdrop)
if (cardEnlargedContainer != null)
{
card.transform.SetParent(cardEnlargedContainer, true);
card.transform.SetAsLastSibling(); // Ensure on top
Logging.Debug($"[AlbumViewPage] Card reparented to enlarged container");
}
Logging.Debug($"[AlbumViewPage] Card enlarged: {card.GetCardData()?.Name}");
}
/// <summary>
/// Handle card shrink request - hide backdrop and reparent card back to slot
/// </summary>
private void OnCardShrinkRequested(AlbumCard card)
{
if (card == null) return;
// Trigger shrink animation
card.ShrinkCard();
// Hide backdrop
if (cardEnlargedBackdrop != null)
{
cardEnlargedBackdrop.SetActive(false);
}
// Reparent back to original parent (the slot)
Transform originalParent = card.GetOriginalParent();
if (originalParent != null)
{
card.transform.SetParent(originalParent, true);
card.transform.localPosition = card.GetOriginalLocalPosition();
card.transform.localRotation = card.GetOriginalLocalRotation();
}
Logging.Debug($"[AlbumViewPage] Card shrunk: {card.GetCardData()?.Name}");
}
/// <summary>
/// Show backdrop and reparent slot preview card for enlargement
/// </summary>
public void ShowSlotPreview(AlbumCardSlot slot, Transform previewCardTransform)
{
if (previewCardTransform == null)
return;
Logging.Debug($"[AlbumViewPage] ShowSlotPreview called for slot: {slot.name}");
// Show backdrop
if (cardEnlargedBackdrop != null)
{
cardEnlargedBackdrop.SetActive(true);
}
// Reparent preview card to enlarged container (above backdrop)
if (cardEnlargedContainer != null)
{
previewCardTransform.SetParent(cardEnlargedContainer, true);
previewCardTransform.SetAsLastSibling();
}
}
/// <summary>
/// Hide backdrop and trigger shrink animation for slot preview
/// </summary>
public void HideSlotPreview(AlbumCardSlot slot, Transform previewCardTransform, System.Action onComplete)
{
if (previewCardTransform == null)
return;
Logging.Debug($"[AlbumViewPage] HideSlotPreview called for slot: {slot.name}");
// Hide backdrop
if (cardEnlargedBackdrop != null)
{
cardEnlargedBackdrop.SetActive(false);
}
// Shrink preview card
Vector3 originalScale = previewCardTransform.localScale / 2.5f; // Assuming 2.5x is enlarged scale
Pixelplacement.Tween.LocalScale(previewCardTransform, originalScale, 0.3f, 0f, Pixelplacement.Tween.EaseInBack,
completeCallback: () => onComplete?.Invoke());
}
#endregion
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 59ff936424a34ce3937299c66232bf7a
timeCreated: 1759923921

View File

@@ -1,135 +0,0 @@
using System;
using AppleHills.Data.CardSystem;
using BookCurlPro;
using Core;
using UnityEngine;
using UnityEngine.UI;
using Tween = Pixelplacement.Tween;
namespace UI.CardSystem
{
/// <summary>
/// Tab button for navigating to specific pages in the card album book.
/// Coordinates with other tabs via static events for visual feedback.
/// </summary>
[RequireComponent(typeof(Button))]
public class BookTabButton : MonoBehaviour
{
[Header("Book Reference")]
[SerializeField] private BookPro book;
[Header("Tab Configuration")]
[SerializeField] private int targetPage;
[SerializeField] private CardZone zone;
[Header("Visual Settings")]
[SerializeField] private bool enableScaling = true;
[SerializeField] private float selectedScale = 2.0f;
[SerializeField] private float normalScale = 1.0f;
[SerializeField] private float scaleTransitionDuration = 0.2f;
private Button button;
private RectTransform rectTransform;
private Vector2 originalSize;
// Static dispatcher for coordinating all tabs
private static event Action<BookTabButton> OnTabClicked;
// Public properties to access this tab's configuration
public CardZone Zone => zone;
public int TargetPage => targetPage;
private void Awake()
{
// Get required components
button = GetComponent<Button>();
rectTransform = GetComponent<RectTransform>();
// Cache original size
originalSize = rectTransform.sizeDelta;
// Register button click
button.onClick.AddListener(OnButtonClicked);
// Subscribe to static tab event
OnTabClicked += OnAnyTabClicked;
}
private void OnDestroy()
{
// Cleanup listeners
if (button != null)
{
button.onClick.RemoveListener(OnButtonClicked);
}
OnTabClicked -= OnAnyTabClicked;
}
private void OnButtonClicked()
{
if (book == null)
{
Logging.Warning($"[BookTabButton] No BookPro reference assigned on {gameObject.name}");
return;
}
// Notify all tabs that this one was clicked
OnTabClicked?.Invoke(this);
// Flip to target page using AutoFlip
BookCurlPro.AutoFlip autoFlip = book.GetComponent<BookCurlPro.AutoFlip>();
if (autoFlip == null)
{
autoFlip = book.gameObject.AddComponent<BookCurlPro.AutoFlip>();
}
autoFlip.enabled = true;
autoFlip.StartFlipping(targetPage);
}
private void OnAnyTabClicked(BookTabButton clickedTab)
{
// Skip scaling if disabled
if (!enableScaling) return;
// Scale this tab based on whether it was clicked
if (clickedTab == this)
{
SetScale(selectedScale);
}
else
{
SetScale(normalScale);
}
}
private void SetScale(float targetScale)
{
Vector2 targetSize = originalSize * targetScale;
// Use Pixelplacement Tween for smooth size change
Tween.Value(rectTransform.sizeDelta, targetSize,
(Vector2 value) => rectTransform.sizeDelta = value,
scaleTransitionDuration, 0f, Tween.EaseInOut);
}
// Public method to programmatically trigger this tab
public void ActivateTab()
{
OnButtonClicked();
}
#if UNITY_EDITOR
private void OnValidate()
{
// Ensure target page is non-negative
if (targetPage < 0)
{
targetPage = 0;
}
}
#endif
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: ff50caabb55742bc8d24a6ddffeda815
timeCreated: 1762385754

View File

@@ -1,219 +0,0 @@
using Core.Lifecycle;
using Data.CardSystem;
using Pixelplacement;
using Pixelplacement.TweenSystem;
using TMPro;
using UnityEngine;
namespace UI.CardSystem
{
/// <summary>
/// Manages a notification dot that displays a count (e.g., booster packs)
/// Can be reused across different UI elements that need to show numeric notifications
/// Automatically syncs with CardSystemManager to display booster pack count
/// </summary>
public class BoosterNotificationDot : ManagedBehaviour
{
[Header("UI References")]
[SerializeField] private GameObject dotBackground;
[SerializeField] private TextMeshProUGUI countText;
[Header("Settings")]
[SerializeField] private bool hideWhenZero = true;
[SerializeField] private bool useAnimation = false;
[SerializeField] private string textPrefix = "";
[SerializeField] private string textSuffix = "";
[SerializeField] private Color textColor = Color.white;
[Header("Animation")]
[SerializeField] private bool useTween = true;
[SerializeField] private float pulseDuration = 0.3f;
[SerializeField] private float pulseScale = 1.2f;
// Optional animator reference
[SerializeField] private Animator animator;
[SerializeField] private string animationTrigger = "Update";
// Current count value
private int _currentCount;
private Vector3 _originalScale;
private TweenBase _activeTween;
internal override void OnManagedStart()
{
// Store original scale for pulse animation
if (dotBackground != null)
{
_originalScale = dotBackground.transform.localScale;
}
// Apply text color
if (countText != null)
{
countText.color = textColor;
}
// Subscribe to CardSystemManager events (managers are guaranteed to be initialized)
if (CardSystemManager.Instance != null)
{
CardSystemManager.Instance.OnBoosterCountChanged += OnBoosterCountChanged;
// Poll initial count and display it
int initialCount = CardSystemManager.Instance.GetBoosterPackCount();
SetCount(initialCount);
}
else
{
// If CardSystemManager isn't available yet, set to default count
SetCount(_currentCount);
}
}
internal override void OnManagedDestroy()
{
// Unsubscribe from CardSystemManager events to prevent memory leaks
if (CardSystemManager.Instance != null)
{
CardSystemManager.Instance.OnBoosterCountChanged -= OnBoosterCountChanged;
}
}
/// <summary>
/// Callback when booster count changes in CardSystemManager
/// </summary>
private void OnBoosterCountChanged(int newCount)
{
SetCount(newCount);
}
/// <summary>
/// Sets the count displayed on the notification dot
/// Also handles visibility based on settings
/// </summary>
public void SetCount(int count)
{
bool countChanged = count != _currentCount;
_currentCount = count;
// Update text
if (countText != null)
{
countText.text = textPrefix + count.ToString() + textSuffix;
}
// Handle visibility
if (hideWhenZero)
{
SetVisibility(count > 0);
}
// Play animation if value changed and animation is enabled
if (countChanged && count > 0)
{
if (useAnimation)
{
Animate();
}
}
}
/// <summary>
/// Gets the current count value
/// </summary>
public int GetCount()
{
return _currentCount;
}
/// <summary>
/// Set text formatting options
/// </summary>
public void SetFormatting(string prefix, string suffix, Color color)
{
textPrefix = prefix;
textSuffix = suffix;
textColor = color;
if (countText != null)
{
countText.color = color;
// Update text with new formatting
countText.text = textPrefix + _currentCount.ToString() + textSuffix;
}
}
/// <summary>
/// Explicitly control the notification dot visibility
/// </summary>
public void SetVisibility(bool isVisible)
{
if (dotBackground != null)
{
dotBackground.SetActive(isVisible);
}
if (countText != null)
{
countText.gameObject.SetActive(isVisible);
}
}
/// <summary>
/// Show the notification dot
/// </summary>
public void Show()
{
SetVisibility(true);
}
/// <summary>
/// Hide the notification dot
/// </summary>
public void Hide()
{
SetVisibility(false);
}
/// <summary>
/// Play animation manually - either using Animator or Tween
/// </summary>
public void Animate()
{
if (useAnimation)
{
if (animator != null)
{
animator.SetTrigger(animationTrigger);
}
else if (useTween && dotBackground != null)
{
// Cancel any existing tweens on this transform
if(_activeTween != null)
_activeTween.Cancel();
// Reset to original scale
dotBackground.transform.localScale = _originalScale;
// Pulse animation using Tween
_activeTween = Tween.LocalScale(dotBackground.transform,
_originalScale * pulseScale,
pulseDuration/2,
0,
Tween.EaseOut,
Tween.LoopType.None,
null,
() => {
// Scale back to original size
Tween.LocalScale(dotBackground.transform,
_originalScale,
pulseDuration/2,
0,
Tween.EaseIn);
},
obeyTimescale: false);
}
}
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 5845ed3764635fe429b6f1063effdd8a

View File

@@ -1,938 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using AppleHills.Data.CardSystem;
using Core;
using Data.CardSystem;
using Pixelplacement;
using UI.Core;
using UI.CardSystem.DragDrop;
using UI.DragAndDrop.Core;
using Unity.Cinemachine;
using UnityEngine;
using UnityEngine.UI;
namespace UI.CardSystem
{
/// <summary>
/// UI page for opening booster packs and displaying the cards received.
/// Manages the entire booster opening flow with drag-and-drop interaction.
/// </summary>
public class BoosterOpeningPage : UIPage
{
[Header("UI References")]
[SerializeField] private CanvasGroup canvasGroup;
[Header("Booster Management")]
[SerializeField] private GameObject boosterPackPrefab; // Prefab to instantiate new boosters
[SerializeField] private SlotContainer bottomRightSlots; // Holds waiting boosters (max 3)
[SerializeField] private DraggableSlot centerOpeningSlot; // Where booster goes to open
[Header("Card Display")]
[SerializeField] private Transform cardDisplayContainer;
[SerializeField] private GameObject flippableCardPrefab; // Placeholder for card backs
[SerializeField] private float cardSpacing = 150f;
[Header("Settings")]
[SerializeField] private float boosterDisappearDuration = 0.5f;
[SerializeField] private CinemachineImpulseSource impulseSource;
[SerializeField] private ParticleSystem openingParticleSystem;
[SerializeField] private GameObject albumIcon; // Target for card fly-away animation and dismiss button
private Button _dismissButton; // Button to close/dismiss the booster opening page
private int _availableBoosterCount;
private BoosterPackDraggable _currentBoosterInCenter;
private List<BoosterPackDraggable> _activeBoostersInSlots = new List<BoosterPackDraggable>();
private List<GameObject> _currentRevealedCards = new List<GameObject>();
private CardData[] _currentCardData;
private int _revealedCardCount;
private int _cardsCompletedInteraction; // Track how many cards finished their new/repeat interaction
private bool _isProcessingOpening;
private const int MAX_VISIBLE_BOOSTERS = 3;
private FlippableCard _currentActiveCard; // The card currently awaiting interaction
private void Awake()
{
// Make sure we have a CanvasGroup for transitions
if (canvasGroup == null)
canvasGroup = GetComponent<CanvasGroup>();
if (canvasGroup == null)
canvasGroup = gameObject.AddComponent<CanvasGroup>();
// Get dismiss button from albumIcon GameObject
if (albumIcon != null)
{
_dismissButton = albumIcon.GetComponent<Button>();
if (_dismissButton != null)
{
_dismissButton.onClick.AddListener(OnDismissButtonClicked);
}
else
{
Logging.Warning("[BoosterOpeningPage] albumIcon does not have a Button component!");
}
}
// UI pages should start disabled
gameObject.SetActive(false);
}
internal override void OnManagedDestroy()
{
// Unsubscribe from dismiss button
if (_dismissButton != null)
{
_dismissButton.onClick.RemoveListener(OnDismissButtonClicked);
}
// Unsubscribe from slot events
if (centerOpeningSlot != null)
{
centerOpeningSlot.OnOccupied -= OnBoosterPlacedInCenter;
centerOpeningSlot.OnVacated -= OnBoosterRemovedFromCenter;
}
// Unsubscribe from booster events
UnsubscribeFromAllBoosters();
}
/// <summary>
/// Set the number of available booster packs before showing the page
/// </summary>
public void SetAvailableBoosterCount(int count)
{
_availableBoosterCount = count;
}
/// <summary>
/// Called when the dismiss button (albumIcon) is clicked
/// </summary>
private void OnDismissButtonClicked()
{
if (UIPageController.Instance != null)
{
UIPageController.Instance.PopPage();
Logging.Debug("[BoosterOpeningPage] Dismiss button clicked, popping page from stack");
}
}
public override void TransitionIn()
{
base.TransitionIn();
// Ensure album icon is visible when page opens
if (albumIcon != null)
{
albumIcon.SetActive(true);
}
InitializeBoosterDisplay();
}
public override void TransitionOut()
{
CleanupPage();
base.TransitionOut();
}
/// <summary>
/// Initialize the booster pack display based on available count
/// </summary>
private void InitializeBoosterDisplay()
{
Logging.Debug($"[BoosterOpeningPage] InitializeBoosterDisplay called with {_availableBoosterCount} boosters available");
if (boosterPackPrefab == null)
{
Logging.Warning("BoosterOpeningPage: No booster pack prefab assigned!");
return;
}
if (bottomRightSlots == null || bottomRightSlots.SlotCount == 0)
{
Logging.Warning("BoosterOpeningPage: No slots available!");
return;
}
// Clear any existing boosters
_activeBoostersInSlots.Clear();
// Calculate how many boosters to show (max 3, or available count, whichever is lower)
int visibleCount = Mathf.Min(_availableBoosterCount, MAX_VISIBLE_BOOSTERS);
Logging.Debug($"[BoosterOpeningPage] Will spawn {visibleCount} boosters");
// Spawn boosters and assign to slots
for (int i = 0; i < visibleCount; i++)
{
SpawnBoosterInSlot(i);
}
// Subscribe to center slot events
if (centerOpeningSlot != null)
{
centerOpeningSlot.OnOccupied += OnBoosterPlacedInCenter;
centerOpeningSlot.OnVacated += OnBoosterRemovedFromCenter;
Logging.Debug($"[BoosterOpeningPage] Subscribed to center slot events");
}
else
{
Logging.Warning("[BoosterOpeningPage] centerOpeningSlot is null!");
}
}
/// <summary>
/// Spawn a new booster and place it in the specified slot index
/// </summary>
private void SpawnBoosterInSlot(int slotIndex)
{
DraggableSlot slot = FindSlotByIndex(slotIndex);
if (slot == null)
{
Logging.Warning($"[BoosterOpeningPage] Could not find slot with SlotIndex {slotIndex}!");
return;
}
// Instantiate booster
GameObject boosterObj = Instantiate(boosterPackPrefab, slot.transform);
BoosterPackDraggable booster = boosterObj.GetComponent<BoosterPackDraggable>();
if (booster != null)
{
// Reset state
booster.ResetTapCount();
booster.SetTapToOpenEnabled(false);
// Subscribe to events
booster.OnReadyToOpen += OnBoosterReadyToOpen;
// Assign to slot with animation
booster.AssignToSlot(slot, true);
// Track it
_activeBoostersInSlots.Add(booster);
Logging.Debug($"[BoosterOpeningPage] Spawned booster in slot with SlotIndex {slotIndex}");
}
else
{
Logging.Warning($"[BoosterOpeningPage] Spawned booster has no BoosterPackDraggable component!");
Destroy(boosterObj);
}
}
/// <summary>
/// Remove and destroy the booster from the specified slot
/// </summary>
private void RemoveBoosterFromSlot(int slotIndex)
{
if (slotIndex >= _activeBoostersInSlots.Count)
return;
BoosterPackDraggable booster = _activeBoostersInSlots[slotIndex];
if (booster != null)
{
// Unsubscribe from events
booster.OnReadyToOpen -= OnBoosterReadyToOpen;
// Animate out and destroy
Transform boosterTransform = booster.transform;
Tween.LocalScale(boosterTransform, Vector3.zero, 0.3f, 0f, Tween.EaseInBack,
completeCallback: () =>
{
if (booster != null && booster.gameObject != null)
{
Destroy(booster.gameObject);
}
});
// Remove from slot
if (booster.CurrentSlot != null)
{
booster.CurrentSlot.Vacate();
}
Logging.Debug($"[BoosterOpeningPage] Removed booster from slot {slotIndex}");
}
_activeBoostersInSlots.RemoveAt(slotIndex);
}
/// <summary>
/// Update visible boosters based on available count
/// </summary>
private void UpdateVisibleBoosters()
{
int targetCount = Mathf.Min(_availableBoosterCount, MAX_VISIBLE_BOOSTERS);
// Remove excess boosters (from the end)
while (_activeBoostersInSlots.Count > targetCount)
{
int lastIndex = _activeBoostersInSlots.Count - 1;
RemoveBoosterFromSlot(lastIndex);
}
Logging.Debug($"[BoosterOpeningPage] Updated visible boosters: {_activeBoostersInSlots.Count}/{targetCount}");
}
/// <summary>
/// Shuffle boosters so they always occupy the first available slots
/// </summary>
private void ShuffleBoostersToFront()
{
if (_activeBoostersInSlots.Count == 0) return;
Logging.Debug($"[BoosterOpeningPage] Shuffling {_activeBoostersInSlots.Count} boosters to front slots");
// Unassign all boosters from their current slots
foreach (var booster in _activeBoostersInSlots)
{
if (booster.CurrentSlot != null)
{
booster.CurrentSlot.Vacate();
}
}
// Reassign boosters to first N slots starting from slot with SlotIndex 0
for (int i = 0; i < _activeBoostersInSlots.Count; i++)
{
// Find slot by its actual SlotIndex property
DraggableSlot targetSlot = FindSlotByIndex(i);
BoosterPackDraggable booster = _activeBoostersInSlots[i];
if (targetSlot != null)
{
Logging.Debug($"[BoosterOpeningPage] Assigning booster to slot with SlotIndex {i} {targetSlot.name}");
booster.AssignToSlot(targetSlot, true); // Animate the move
}
else
{
Logging.Warning($"[BoosterOpeningPage] Could not find slot with SlotIndex {i} {targetSlot.name}");
}
}
}
/// <summary>
/// Find a slot by its SlotIndex property (not list position)
/// </summary>
private DraggableSlot FindSlotByIndex(int slotIndex)
{
foreach (var slot in bottomRightSlots.Slots)
{
if (slot.SlotIndex == slotIndex)
{
return slot;
}
}
return null;
}
/// <summary>
/// Try to spawn a new booster to maintain up to 3 visible
/// Pass decrementCount=true when called after placing a booster in center slot
/// (accounts for the booster that will be consumed)
/// </summary>
private void TrySpawnNewBooster(bool decrementCount = false)
{
// Can we spawn more boosters?
if (_activeBoostersInSlots.Count >= MAX_VISIBLE_BOOSTERS)
return; // Already at max
// Use decremented count if this is called after placing in center
// (the booster in center will be consumed, so we check against count - 1)
int effectiveCount = decrementCount ? _availableBoosterCount - 1 : _availableBoosterCount;
if (_activeBoostersInSlots.Count >= effectiveCount)
return; // No more boosters available
// Find first available slot by SlotIndex (0, 1, 2)
for (int i = 0; i < MAX_VISIBLE_BOOSTERS; i++)
{
DraggableSlot slot = FindSlotByIndex(i);
if (slot != null && !slot.IsOccupied)
{
SpawnBoosterInSlot(i);
Logging.Debug($"[BoosterOpeningPage] Spawned new booster in slot with SlotIndex {i}");
break;
}
}
}
/// <summary>
/// Handle when a booster is placed in the center opening slot
/// </summary>
private void OnBoosterPlacedInCenter(DraggableObject draggable)
{
BoosterPackDraggable booster = draggable as BoosterPackDraggable;
if (booster == null) return;
_currentBoosterInCenter = booster;
// Remove from active slots list
_activeBoostersInSlots.Remove(booster);
// Hide album icon when booster is placed in center
if (albumIcon != null)
{
albumIcon.SetActive(false);
Logging.Debug($"[BoosterOpeningPage] Album icon hidden");
}
// Lock the slot so it can't be dragged out
Logging.Debug($"[BoosterOpeningPage] Locking center slot. IsLocked before: {centerOpeningSlot.IsLocked}");
centerOpeningSlot.SetLocked(true);
Logging.Debug($"[BoosterOpeningPage] IsLocked after: {centerOpeningSlot.IsLocked}");
// Configure booster for opening (disables drag, enables tapping, resets tap count)
Logging.Debug($"[BoosterOpeningPage] Calling SetInOpeningSlot(true) on booster");
booster.SetInOpeningSlot(true);
// Subscribe to tap events for visual feedback
booster.OnTapped += OnBoosterTapped;
booster.OnReadyToOpen += OnBoosterReadyToOpen;
booster.OnBoosterOpened += OnBoosterOpened;
// Try to spawn a new booster to maintain 3 visible
// Use decrementCount=true because this booster will be consumed
TrySpawnNewBooster(decrementCount: true);
// Shuffle remaining boosters to occupy the first slots
ShuffleBoostersToFront();
Logging.Debug($"[BoosterOpeningPage] Booster placed in center, ready for taps. Active boosters in slots: {_activeBoostersInSlots.Count}");
}
/// <summary>
/// Handle when a booster is removed from the center opening slot
/// </summary>
private void OnBoosterRemovedFromCenter(DraggableObject draggable)
{
BoosterPackDraggable booster = draggable as BoosterPackDraggable;
if (booster == null) return;
// If it's being removed back to a corner slot, add it back to tracking
if (booster.CurrentSlot != null && bottomRightSlots.HasSlot(booster.CurrentSlot))
{
_activeBoostersInSlots.Add(booster);
booster.SetInOpeningSlot(false);
}
_currentBoosterInCenter = null;
Logging.Debug($"[BoosterOpeningPage] Booster removed from center");
}
private void OnBoosterTapped(BoosterPackDraggable booster, int currentTaps, int maxTaps)
{
Logging.Debug($"[BoosterOpeningPage] Booster tapped: {currentTaps}/{maxTaps}");
// Fire Cinemachine impulse with random velocity (excluding Z)
if (impulseSource != null)
{
// Generate random velocity vector (X and Y only, Z = 0)
Vector3 randomVelocity = new Vector3(
Random.Range(-1f, 1f),
Random.Range(-1f, 1f),
0f
);
// Normalize to ensure consistent strength
randomVelocity.Normalize();
// Generate the impulse with strength 1 and random velocity
impulseSource.GenerateImpulse(randomVelocity);
}
}
/// <summary>
/// Handle when booster is opened - play particle effects
/// </summary>
private void OnBoosterOpened(BoosterPackDraggable booster)
{
Logging.Debug($"[BoosterOpeningPage] Booster opened, playing particle effect");
// Reset and play particle system
if (openingParticleSystem != null)
{
// Stop any existing playback
if (openingParticleSystem.isPlaying)
{
openingParticleSystem.Stop();
}
// Clear existing particles
openingParticleSystem.Clear();
// Play the particle system
openingParticleSystem.Play();
}
}
/// <summary>
/// Handle tap-to-place: When player taps a booster in bottom slots, move it to center
/// </summary>
public void OnBoosterTappedInBottomSlot(BoosterPackDraggable booster)
{
if (_currentBoosterInCenter != null || centerOpeningSlot == null)
return; // Center slot already occupied
// Move booster to center slot
booster.AssignToSlot(centerOpeningSlot, true);
}
/// <summary>
/// Handle when booster is ready to open (after max taps)
/// </summary>
private void OnBoosterReadyToOpen(BoosterPackDraggable booster)
{
if (_isProcessingOpening) return;
Logging.Debug($"[BoosterOpeningPage] Booster ready to open!");
// Trigger the actual opening sequence
booster.TriggerOpen();
StartCoroutine(ProcessBoosterOpening(booster));
}
/// <summary>
/// Process the booster opening sequence
/// </summary>
private IEnumerator ProcessBoosterOpening(BoosterPackDraggable booster)
{
_isProcessingOpening = true;
// Call CardSystemManager to open the pack
if (CardSystemManager.Instance != null)
{
List<CardData> revealedCardsList = CardSystemManager.Instance.OpenBoosterPack();
_currentCardData = revealedCardsList.ToArray();
// Animate booster disappearing
yield return StartCoroutine(AnimateBoosterDisappear(booster));
// Decrement available count
_availableBoosterCount--;
// Update visible boosters (remove from end if we drop below thresholds)
UpdateVisibleBoosters();
// Show card backs
SpawnCardBacks(_currentCardData.Length);
// Wait for player to reveal all cards
bool isLastBooster = _availableBoosterCount <= 0;
yield return StartCoroutine(WaitForCardReveals());
// Check if this was the last booster pack
if (isLastBooster)
{
// Wait for all card animations to complete before transitioning
// WaitForCardReveals already includes: 0.5s wait + (cardCount * 0.5s stagger) + 0.5s animation + 0.5s final
// Total is: 1.5s + (cardCount * 0.5s)
// For 5 cards that's 4 seconds total, which should be enough
Logging.Debug("[BoosterOpeningPage] Last booster opened, auto-transitioning to album main page");
if (UIPageController.Instance != null)
{
UIPageController.Instance.PopPage();
}
}
}
_isProcessingOpening = false;
}
/// <summary>
/// Animate the booster pack disappearing
/// </summary>
private IEnumerator AnimateBoosterDisappear(BoosterPackDraggable booster)
{
if (booster == null) yield break;
// Scale down and fade out
Transform boosterTransform = booster.transform;
Tween.LocalScale(boosterTransform, Vector3.zero, boosterDisappearDuration, 0f, Tween.EaseInBack);
// Also fade the visual if it has a CanvasGroup
CanvasGroup boosterCg = booster.GetComponentInChildren<CanvasGroup>();
if (boosterCg != null)
{
Tween.Value(1f, 0f, (val) => boosterCg.alpha = val, boosterDisappearDuration, 0f);
}
yield return new WaitForSeconds(boosterDisappearDuration);
// Destroy the booster
Destroy(booster.gameObject);
_currentBoosterInCenter = null;
// Unlock center slot
centerOpeningSlot.SetLocked(false);
}
/// <summary>
/// Spawn card back placeholders for revealing
/// </summary>
private void SpawnCardBacks(int count)
{
if (flippableCardPrefab == null || cardDisplayContainer == null)
{
Logging.Warning("BoosterOpeningPage: Missing card prefab or container!");
return;
}
_currentRevealedCards.Clear();
_revealedCardCount = 0;
_cardsCompletedInteraction = 0; // Reset interaction count
// Calculate positions
float totalWidth = (count - 1) * cardSpacing;
float startX = -totalWidth / 2f;
for (int i = 0; i < count; i++)
{
GameObject cardObj = Instantiate(flippableCardPrefab, cardDisplayContainer);
RectTransform cardRect = cardObj.GetComponent<RectTransform>();
if (cardRect != null)
{
cardRect.anchoredPosition = new Vector2(startX + (i * cardSpacing), 0);
}
// Get FlippableCard component and setup the card data
FlippableCard flippableCard = cardObj.GetComponent<FlippableCard>();
if (flippableCard != null)
{
// Setup the card data (stored but not revealed yet)
flippableCard.SetupCard(_currentCardData[i]);
// Subscribe to flip started event (to disable other cards IMMEDIATELY)
int cardIndex = i; // Capture for closure
flippableCard.OnFlipStarted += OnCardFlipStarted;
// Subscribe to reveal event to track when flipped
flippableCard.OnCardRevealed += (card, data) => OnCardRevealed(cardIndex);
// Subscribe to inactive click event (for jiggle effect)
flippableCard.OnClickedWhileInactive += OnCardClickedWhileInactive;
// Initially, all cards are clickable (for flipping)
flippableCard.SetClickable(true);
}
else
{
Logging.Warning($"[BoosterOpeningPage] FlippableCard component not found on card {i}!");
}
_currentRevealedCards.Add(cardObj);
// Animate cards flying in
cardRect.localScale = Vector3.zero;
Tween.LocalScale(cardRect, Vector3.one, 0.3f, i * 0.1f, Tween.EaseOutBack);
}
}
/// <summary>
/// Handle when a card flip starts (disable all other cards IMMEDIATELY)
/// </summary>
private void OnCardFlipStarted(FlippableCard flippingCard)
{
Logging.Debug($"[BoosterOpeningPage] Card flip started, disabling all other cards.");
// Disable ALL cards immediately to prevent multi-flip
foreach (GameObject cardObj in _currentRevealedCards)
{
FlippableCard card = cardObj.GetComponent<FlippableCard>();
if (card != null)
{
card.SetClickable(false);
}
}
}
/// <summary>
/// Handle card reveal (when flipped)
/// </summary>
private void OnCardRevealed(int cardIndex)
{
Logging.Debug($"[BoosterOpeningPage] Card {cardIndex} revealed!");
_revealedCardCount++;
// Get the flippable card and card data
FlippableCard flippableCard = _currentRevealedCards[cardIndex].GetComponent<FlippableCard>();
if (flippableCard == null)
{
Logging.Warning($"[BoosterOpeningPage] FlippableCard not found for card {cardIndex}!");
return;
}
CardData cardData = flippableCard.CardData;
// Check if this is a new card using CardSystemManager
bool isNew = Data.CardSystem.CardSystemManager.Instance.IsCardNew(cardData, out CardData existingCard);
if (isNew)
{
Logging.Debug($"[BoosterOpeningPage] Card '{cardData.Name}' is NEW!");
flippableCard.ShowAsNew();
}
else
{
// Check if card is already Legendary - if so, skip progress bar and auto-progress
if (existingCard.Rarity == AppleHills.Data.CardSystem.CardRarity.Legendary)
{
Logging.Debug($"[BoosterOpeningPage] Card '{cardData.Name}' is LEGENDARY - auto-progressing!");
// Add to inventory immediately and move to next card
Data.CardSystem.CardSystemManager.Instance.AddCardToInventoryDelayed(cardData);
_cardsCompletedInteraction++;
_revealedCardCount++; // This was already incremented earlier, but we need to track completion
EnableUnrevealedCards();
return; // Skip showing the card enlarged
}
int ownedCount = existingCard.CopiesOwned;
Logging.Debug($"[BoosterOpeningPage] Card '{cardData.Name}' is a REPEAT! Owned: {ownedCount}");
// Check if this card will trigger an upgrade (ownedCount + 1 >= threshold)
bool willUpgrade = (ownedCount + 1) >= flippableCard.CardsToUpgrade && existingCard.Rarity < AppleHills.Data.CardSystem.CardRarity.Legendary;
if (willUpgrade)
{
Logging.Debug($"[BoosterOpeningPage] This card will trigger upgrade! ({ownedCount + 1}/{flippableCard.CardsToUpgrade})");
// Show as repeat - progress bar will fill and auto-trigger upgrade
flippableCard.ShowAsRepeatWithUpgrade(ownedCount, existingCard);
}
else
{
// Normal repeat, no upgrade
flippableCard.ShowAsRepeat(ownedCount);
}
}
// Set this card as the active one (only this card is clickable now)
SetActiveCard(flippableCard);
// Subscribe to tap event to know when interaction is complete
flippableCard.OnCardTappedAfterReveal += (card) => OnCardCompletedInteraction(card, cardIndex);
}
/// <summary>
/// Handle when a card's interaction is complete (tapped after reveal)
/// </summary>
private void OnCardCompletedInteraction(FlippableCard card, int cardIndex)
{
Logging.Debug($"[BoosterOpeningPage] Card {cardIndex} interaction complete!");
// Add card to inventory NOW (after player saw it)
Data.CardSystem.CardSystemManager.Instance.AddCardToInventoryDelayed(card.CardData);
// Return card to normal size
card.ReturnToNormalSize();
// Increment completed interaction count
_cardsCompletedInteraction++;
// Clear active card
_currentActiveCard = null;
// Re-enable all unrevealed cards (they can be flipped now)
EnableUnrevealedCards();
Logging.Debug($"[BoosterOpeningPage] Cards completed interaction: {_cardsCompletedInteraction}/{_currentCardData.Length}");
}
/// <summary>
/// Set which card is currently active (only this card can be clicked)
/// </summary>
private void SetActiveCard(FlippableCard activeCard)
{
_currentActiveCard = activeCard;
// Disable all other cards
foreach (GameObject cardObj in _currentRevealedCards)
{
FlippableCard card = cardObj.GetComponent<FlippableCard>();
if (card != null)
{
// Only the active card is clickable
card.SetClickable(card == activeCard);
}
}
Logging.Debug($"[BoosterOpeningPage] Set active card. Only one card is now clickable.");
}
/// <summary>
/// Re-enable all unrevealed cards (allow them to be flipped)
/// </summary>
private void EnableUnrevealedCards()
{
foreach (GameObject cardObj in _currentRevealedCards)
{
FlippableCard card = cardObj.GetComponent<FlippableCard>();
if (card != null && !card.IsFlipped)
{
card.SetClickable(true);
}
}
Logging.Debug($"[BoosterOpeningPage] Re-enabled unrevealed cards for flipping.");
}
/// <summary>
/// Handle when a card is clicked while not active (jiggle the active card)
/// </summary>
private void OnCardClickedWhileInactive(FlippableCard inactiveCard)
{
Logging.Debug($"[BoosterOpeningPage] Inactive card clicked, jiggling active card.");
if (_currentActiveCard != null)
{
_currentActiveCard.Jiggle();
}
}
/// <summary>
/// Wait until all cards are revealed AND all interactions are complete
/// </summary>
private IEnumerator WaitForCardReveals()
{
// Wait until all cards are flipped
while (_revealedCardCount < _currentCardData.Length)
{
yield return null;
}
Logging.Debug($"[BoosterOpeningPage] All cards revealed! Waiting for interactions...");
// Wait until all cards have completed their new/repeat interaction
while (_cardsCompletedInteraction < _currentCardData.Length)
{
yield return null;
}
Logging.Debug($"[BoosterOpeningPage] All interactions complete! Animating cards to album...");
// All cards revealed and interacted with, wait a moment
yield return new WaitForSeconds(0.5f);
// Show album icon before cards start tweening to it
if (albumIcon != null)
{
albumIcon.SetActive(true);
Logging.Debug($"[BoosterOpeningPage] Album icon shown for card tween target");
}
// Animate cards to album icon (or center if no icon assigned) with staggered delays
Vector3 targetPosition = albumIcon != null ? albumIcon.transform.position : Vector3.zero;
int cardIndex = 0;
foreach (GameObject cardObj in _currentRevealedCards)
{
if (cardObj != null)
{
// Stagger each card with 0.5s delay
float delay = cardIndex * 0.5f;
// Animate to album icon position, then destroy
Tween.Position(cardObj.transform, targetPosition, 0.5f, delay, Tween.EaseInBack);
Tween.LocalScale(cardObj.transform, Vector3.zero, 0.5f, delay, Tween.EaseInBack,
completeCallback: () => Destroy(cardObj));
cardIndex++;
}
}
// Wait for all animations to complete
// Last card starts at: (cardCount - 1) * 0.5s delay
// Last card finishes at: (cardCount - 1) * 0.5s + 0.5s animation duration = cardCount * 0.5s
float totalAnimationTime = _currentCardData.Length * 0.5f;
_currentRevealedCards.Clear();
yield return new WaitForSeconds(totalAnimationTime);
// Album icon stays visible for next booster (will be hidden when next booster is placed)
}
/// <summary>
/// Clean up the page when hidden
/// </summary>
private void CleanupPage()
{
UnsubscribeFromAllBoosters();
// Destroy all active boosters
foreach (var booster in _activeBoostersInSlots.ToList())
{
if (booster != null && booster.gameObject != null)
Destroy(booster.gameObject);
}
_activeBoostersInSlots.Clear();
// Clear any remaining cards
foreach (GameObject card in _currentRevealedCards)
{
if (card != null)
Destroy(card);
}
_currentRevealedCards.Clear();
_currentBoosterInCenter = null;
_isProcessingOpening = false;
}
/// <summary>
/// Unsubscribe from all booster events
/// </summary>
private void UnsubscribeFromAllBoosters()
{
// Unsubscribe from active boosters in slots
foreach (var booster in _activeBoostersInSlots)
{
if (booster != null)
{
booster.OnReadyToOpen -= OnBoosterReadyToOpen;
booster.OnTapped -= OnBoosterTapped;
booster.OnBoosterOpened -= OnBoosterOpened;
}
}
// Unsubscribe from center booster
if (_currentBoosterInCenter != null)
{
_currentBoosterInCenter.OnReadyToOpen -= OnBoosterReadyToOpen;
_currentBoosterInCenter.OnTapped -= OnBoosterTapped;
_currentBoosterInCenter.OnBoosterOpened -= OnBoosterOpened;
}
}
protected override void DoTransitionIn(System.Action onComplete)
{
// Simple fade in animation
if (canvasGroup != null)
{
canvasGroup.alpha = 0f;
Tween.Value(0f, 1f, (value) => canvasGroup.alpha = value, transitionDuration, 0f, Tween.EaseInOut, Tween.LoopType.None, null, onComplete, obeyTimescale: false);
}
else
{
// Fallback if no CanvasGroup
onComplete?.Invoke();
}
}
protected override void DoTransitionOut(System.Action onComplete)
{
// Simple fade out animation
if (canvasGroup != null)
{
Tween.Value(canvasGroup.alpha, 0f, (value) => canvasGroup.alpha = value, transitionDuration, 0f, Tween.EaseInOut, Tween.LoopType.None, null, onComplete, obeyTimescale: false);
}
else
{
// Fallback if no CanvasGroup
onComplete?.Invoke();
}
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 91691a5efb1346b5b34482dd8200c868
timeCreated: 1762418615

View File

@@ -1,220 +0,0 @@
using System.Collections;
using Core;
using Data.CardSystem;
using UnityEngine;
using UnityEngine.Events;
namespace UI.CardSystem
{
/// <summary>
/// One-off helper to visually grant a booster pack.
/// Place this on a UI GameObject with two Image children (a background "glow" and a booster pack image).
/// Access via BoosterPackGiver.Instance and call GiveBoosterPack().
/// The sequence:
/// 1) Shows the object (enables children)
/// 2) Pulses the glow scale for a fixed duration
/// 3) Hides the glow, tweens the booster image towards the backpack icon and scales to zero
/// 4) Invokes OnCompleted
/// </summary>
public class BoosterPackGiver : MonoBehaviour
{
public static BoosterPackGiver Instance { get; private set; }
[Header("References")]
[Tooltip("Canvas that contains these UI elements. If null, will search up the hierarchy.")]
[SerializeField] private Canvas canvas;
[Tooltip("Background glow RectTransform (child image)")]
[SerializeField] private RectTransform backgroundGlow;
[Tooltip("Booster pack image RectTransform (child image)")]
[SerializeField] private RectTransform boosterImage;
[Tooltip("Target RectTransform for the backpack icon (where the booster flies to)")]
[SerializeField] private RectTransform targetBackpackIcon;
[Header("Timing")]
[Tooltip("How long the glow should pulse before the booster flies to the backpack")]
[SerializeField] private float pulseDuration = 2.0f;
[Tooltip("Duration of the flight/scale-down animation")]
[SerializeField] private float moveDuration = 0.6f;
[Header("Glow Pulse")]
[Tooltip("Minimum scale during pulse")] [SerializeField] private float glowScaleMin = 0.9f;
[Tooltip("Maximum scale during pulse")] [SerializeField] private float glowScaleMax = 1.1f;
[Tooltip("Pulse speed in cycles per second")] [SerializeField] private float glowPulseSpeed = 2.0f;
[Header("Move/Scale Easing")]
[SerializeField] private AnimationCurve moveCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
[SerializeField] private AnimationCurve scaleCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
[Header("Behaviour")]
[Tooltip("Hide visuals when the sequence completes")] [SerializeField] private bool hideOnComplete = true;
[Header("Events")]
public UnityEvent OnCompleted;
private Coroutine _sequenceCoroutine;
private Vector3 _boosterInitialScale;
private Vector2 _boosterInitialAnchoredPos;
private IEnumerator Start()
{
if (Instance != null && Instance != this)
{
Logging.Warning("[BoosterPackGiver] Duplicate instance detected. Destroying this component.");
Destroy(this);
yield break;
}
Instance = this;
if (canvas == null)
{
canvas = GetComponentInParent<Canvas>();
}
CacheInitialBoosterState();
// Start hidden (keep GameObject active so the singleton remains accessible)
SetVisualsActive(false);
// yield return new WaitForSeconds(1f);
// GiveBoosterPack();
}
private void OnDestroy()
{
if (Instance == this)
Instance = null;
}
private void CacheInitialBoosterState()
{
if (boosterImage != null)
{
_boosterInitialScale = boosterImage.localScale;
_boosterInitialAnchoredPos = boosterImage.anchoredPosition;
}
}
/// <summary>
/// Public entry point: run the grant animation.
/// </summary>
public void GiveBoosterPack()
{
if (backgroundGlow == null || boosterImage == null)
{
Debug.LogError("[BoosterPackGiver] Missing references. Assign Background Glow and Booster Image in the inspector.");
return;
}
// Reset and start fresh
if (_sequenceCoroutine != null)
{
StopCoroutine(_sequenceCoroutine);
_sequenceCoroutine = null;
}
// Ensure canvas reference
if (canvas == null)
{
canvas = GetComponentInParent<Canvas>();
}
// Reset booster transform
boosterImage.localScale = _boosterInitialScale;
boosterImage.anchoredPosition = _boosterInitialAnchoredPos;
// Show visuals
SetVisualsActive(true);
_sequenceCoroutine = StartCoroutine(RunSequence());
}
private IEnumerator RunSequence()
{
// 1) Pulse the glow
float elapsed = 0f;
Vector3 baseGlowScale = backgroundGlow.localScale;
while (elapsed < pulseDuration)
{
elapsed += Time.unscaledDeltaTime;
float t = Mathf.Sin(elapsed * Mathf.PI * 2f * glowPulseSpeed) * 0.5f + 0.5f; // 0..1
float s = Mathf.Lerp(glowScaleMin, glowScaleMax, t);
backgroundGlow.localScale = baseGlowScale * s;
yield return null;
}
// 2) Hide glow
backgroundGlow.gameObject.SetActive(false);
// 3) Move booster to backpack icon and scale to zero
Vector2 startPos = boosterImage.anchoredPosition;
Vector2 targetPos = startPos;
// Convert target to booster parent space if available
if (targetBackpackIcon != null)
{
var parentRect = boosterImage.parent as RectTransform;
if (parentRect != null)
{
Vector2 localPoint;
Vector2 screenPoint = RectTransformUtility.WorldToScreenPoint(canvas != null ? canvas.worldCamera : null, targetBackpackIcon.position);
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(parentRect, screenPoint, canvas != null ? canvas.worldCamera : null, out localPoint))
{
targetPos = localPoint;
}
}
}
elapsed = 0f;
while (elapsed < moveDuration)
{
elapsed += Time.unscaledDeltaTime;
float t = Mathf.Clamp01(elapsed / moveDuration);
float mt = moveCurve != null ? moveCurve.Evaluate(t) : t;
float st = scaleCurve != null ? scaleCurve.Evaluate(t) : t;
boosterImage.anchoredPosition = Vector2.LerpUnclamped(startPos, targetPos, mt);
boosterImage.localScale = Vector3.LerpUnclamped(_boosterInitialScale, Vector3.zero, st);
yield return null;
}
// Ensure final state
boosterImage.anchoredPosition = targetPos;
boosterImage.localScale = Vector3.zero;
if (hideOnComplete)
{
SetVisualsActive(false);
// Restore booster for the next run
boosterImage.localScale = _boosterInitialScale;
boosterImage.anchoredPosition = _boosterInitialAnchoredPos;
backgroundGlow.localScale = Vector3.one; // reset pulse scaling
}
_sequenceCoroutine = null;
OnCompleted?.Invoke();
CardSystemManager.Instance.AddBoosterPack(1);
}
private void SetVisualsActive(bool active)
{
if (backgroundGlow != null) backgroundGlow.gameObject.SetActive(active);
if (boosterImage != null) boosterImage.gameObject.SetActive(active);
}
// Optional: quick editor hookup to validate references
#if UNITY_EDITOR
private void OnValidate()
{
if (canvas == null)
{
canvas = GetComponentInParent<Canvas>();
}
if (boosterImage != null && _boosterInitialScale == Vector3.zero)
{
_boosterInitialScale = boosterImage.localScale;
_boosterInitialAnchoredPos = boosterImage.anchoredPosition;
}
}
#endif
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: e805057df6a34bd4b881031b5f460fe5
timeCreated: 1761053022

View File

@@ -1,92 +0,0 @@
using Core;
using UI.Core;
using UnityEngine;
using UnityEngine.UI;
namespace UI.CardSystem
{
/// <summary>
/// Opens the card album view when the button is pressed.
/// Attach this to a top-level GameObject in the scene.
/// </summary>
public class CardAlbumOpener : MonoBehaviour
{
[Header("References")]
[SerializeField] private Button openAlbumButton;
[SerializeField] private AlbumViewPage albumViewPage;
[SerializeField] private BoosterOpeningPage boosterOpeningPage;
private void Awake()
{
if (openAlbumButton != null)
{
openAlbumButton.onClick.AddListener(OnOpenAlbumClicked);
}
}
private void Start()
{
if (UIPageController.Instance != null)
{
UIPageController.Instance.OnPageChanged += OnPageChanged;
}
}
private void OnDisable()
{
if (UIPageController.Instance != null)
{
UIPageController.Instance.OnPageChanged -= OnPageChanged;
}
}
private void OnDestroy()
{
if (openAlbumButton != null)
{
openAlbumButton.onClick.RemoveListener(OnOpenAlbumClicked);
}
Logging.Debug("ALBUM: CardAlbumDestroyed");
}
private void OnOpenAlbumClicked()
{
if (UIPageController.Instance == null) return;
// Check if we're currently on the booster opening page
if (UIPageController.Instance.CurrentPage == boosterOpeningPage)
{
// We're in booster opening page, pop back to album main page
UIPageController.Instance.PopPage();
}
else if (UIPageController.Instance.CurrentPage != albumViewPage)
{
// We're not in the album at all, open it
if (openAlbumButton != null)
{
openAlbumButton.gameObject.SetActive(false);
}
if (albumViewPage != null)
{
UIPageController.Instance.PushPage(albumViewPage);
}
}
}
private void OnPageChanged(UIPage currentPage)
{
if (openAlbumButton == null) return;
// Show the button when:
// 1. We're on the booster opening page (acts as "back to album" button)
// 2. We're NOT on the album main page (acts as "open album" button)
// Hide the button only when we're on the album main page
bool shouldShowButton = currentPage == boosterOpeningPage || currentPage != albumViewPage;
openAlbumButton.gameObject.SetActive(shouldShowButton);
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 4b3b63118ebc48d6b8f28cd69d96191e
timeCreated: 1762384087

View File

@@ -1,392 +0,0 @@
using System;
using AppleHills.Data.CardSystem;
using Core;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace UI.CardSystem
{
/// <summary>
/// Displays a single card using the new visual system with frames, overlays, backgrounds, and zone shapes.
/// </summary>
public class CardDisplay : MonoBehaviour, IPointerClickHandler
{
[Header("UI Elements")]
[SerializeField] private TextMeshProUGUI cardNameText;
[SerializeField] private Image cardImage;
[SerializeField] private Image frameImage;
[SerializeField] private Image overlayImage;
[SerializeField] private Image backgroundImage;
[SerializeField] private Image zoneShapeImage;
[Header("Card Data")]
[SerializeField] private CardData cardData;
[Header("Visual Settings")]
[SerializeField] private CardVisualConfig visualConfig;
#if UNITY_EDITOR
[Header("Editor Tools")]
[SerializeField] private CardDefinition editorCardDefinition;
#endif
// Events
public event Action<CardDisplay> OnCardClicked;
// Preview mode tracking for click forwarding
private bool _isPreviewMode;
private AlbumCardSlot _previewSlot;
/// <summary>
/// Sets up the card display with the given card data
/// </summary>
public void SetupCard(CardData data)
{
if (data == null)
{
Logging.Warning("[CardDisplay] Attempted to setup card with null data");
return;
}
cardData = data;
UpdateCardVisuals();
}
/// <summary>
/// Updates all visual elements based on current card data
/// </summary>
private void UpdateCardVisuals()
{
if (cardData == null)
{
ClearCard();
return;
}
if (visualConfig == null)
{
Logging.Warning("[CardDisplay] No CardVisualConfig assigned. Visuals may not display correctly.");
}
// Update text
UpdateCardName();
// Update main card image
UpdateCardImage();
// Update rarity-based visuals (frame and overlay)
UpdateRarityVisuals();
// Update zone-based visuals (background and shape)
UpdateZoneVisuals();
Logging.Debug($"[CardDisplay] Updated visuals for card: {cardData.Name} (Rarity: {cardData.Rarity}, Zone: {cardData.Zone})");
}
/// <summary>
/// Apply preview visuals - black tint to card image and question marks for name
/// Used for empty slot previews to show locked/unknown cards
/// </summary>
public void SetPreviewVisuals()
{
// Set card name to question marks
if (cardNameText != null)
{
cardNameText.text = "??????";
}
// Apply black non-opaque tint to card image
if (cardImage != null)
{
cardImage.color = Color.black;
}
Logging.Debug($"[CardDisplay] Applied preview visuals (black tint and ?????? name)");
}
/// <summary>
/// Reset preview visuals back to normal
/// </summary>
public void ClearPreviewVisuals()
{
// Restore normal card name
if (cardData != null && cardNameText != null)
{
cardNameText.text = cardData.Name ?? "Unknown Card";
}
// Reset card image color to white (normal)
if (cardImage != null)
{
cardImage.color = Color.white;
}
Logging.Debug($"[CardDisplay] Cleared preview visuals");
}
/// <summary>
/// Updates the card name text
/// </summary>
private void UpdateCardName()
{
if (cardNameText != null)
{
cardNameText.text = cardData.Name ?? "Unknown Card";
}
}
/// <summary>
/// Updates the main card image
/// </summary>
private void UpdateCardImage()
{
if (cardImage != null)
{
if (cardData.CardImage != null)
{
cardImage.sprite = cardData.CardImage;
cardImage.enabled = true;
}
else
{
cardImage.sprite = null;
cardImage.enabled = false;
}
}
}
/// <summary>
/// Updates frame and overlay based on card rarity
/// </summary>
private void UpdateRarityVisuals()
{
if (visualConfig == null) return;
// Update frame
if (frameImage != null)
{
Sprite frameSprite = visualConfig.GetRarityFrame(cardData.Rarity);
if (frameSprite != null)
{
frameImage.sprite = frameSprite;
frameImage.enabled = true;
}
else
{
frameImage.enabled = false;
}
}
// Update overlay (can be null for some rarities)
if (overlayImage != null)
{
Sprite overlaySprite = visualConfig.GetRarityOverlay(cardData.Rarity);
if (overlaySprite != null)
{
overlayImage.sprite = overlaySprite;
overlayImage.enabled = true;
}
else
{
overlayImage.enabled = false;
}
}
}
/// <summary>
/// Updates background and zone shape based on card zone and rarity
/// Special handling for Legendary cards
/// </summary>
private void UpdateZoneVisuals()
{
if (visualConfig == null) return;
// Update background
// Legendary cards get special background, others get zone background
if (backgroundImage != null)
{
Sprite bgSprite = visualConfig.GetBackground(cardData.Zone, cardData.Rarity);
if (bgSprite != null)
{
backgroundImage.sprite = bgSprite;
backgroundImage.enabled = true;
}
else
{
backgroundImage.enabled = false;
}
}
// Update zone shape
// Legendary cards don't show shapes
if (zoneShapeImage != null)
{
Sprite shapeSprite = visualConfig.GetZoneShape(cardData.Zone, cardData.Rarity);
if (shapeSprite != null)
{
zoneShapeImage.sprite = shapeSprite;
zoneShapeImage.enabled = true;
}
else
{
zoneShapeImage.enabled = false;
}
}
}
/// <summary>
/// Clears all visual elements
/// </summary>
private void ClearCard()
{
if (cardNameText != null) cardNameText.text = "";
if (cardImage != null) cardImage.enabled = false;
if (frameImage != null) frameImage.enabled = false;
if (overlayImage != null) overlayImage.enabled = false;
if (backgroundImage != null) backgroundImage.enabled = false;
if (zoneShapeImage != null) zoneShapeImage.enabled = false;
}
/// <summary>
/// Called when the card is clicked
/// </summary>
public void OnClick()
{
OnCardClicked?.Invoke(this);
}
/// <summary>
/// Returns the card data associated with this display
/// </summary>
public CardData GetCardData()
{
return cardData;
}
/// <summary>
/// Updates the visual config reference
/// </summary>
public void SetVisualConfig(CardVisualConfig config)
{
visualConfig = config;
if (cardData != null)
{
UpdateCardVisuals();
}
}
/// <summary>
/// Enable preview mode - when clicked, forwards click to the associated slot
/// </summary>
public void SetPreviewMode(bool isEnabled, AlbumCardSlot slot = null)
{
_isPreviewMode = isEnabled;
_previewSlot = slot;
// Enable raycast targets on images so this CardDisplay can receive clicks
if (cardImage != null) cardImage.raycastTarget = isEnabled;
if (frameImage != null) frameImage.raycastTarget = isEnabled;
if (overlayImage != null) overlayImage.raycastTarget = isEnabled;
if (backgroundImage != null) backgroundImage.raycastTarget = isEnabled;
if (zoneShapeImage != null) zoneShapeImage.raycastTarget = isEnabled;
Logging.Debug($"[CardDisplay] Preview mode {(isEnabled ? "enabled" : "disabled")}, slot: {(slot != null ? slot.name : "NULL")}");
}
/// <summary>
/// Handle click on CardDisplay - forward to preview slot if in preview mode
/// </summary>
public void OnPointerClick(PointerEventData eventData)
{
Logging.Debug($"[CLICK-TRACE-CARDDISPLAY] OnPointerClick on {name}, _isPreviewMode={_isPreviewMode}, _previewSlot={((_previewSlot != null) ? _previewSlot.name : "NULL")}");
if (_isPreviewMode && _previewSlot != null)
{
Logging.Debug($"[CLICK-TRACE-CARDDISPLAY] {name} - In preview mode, calling DismissPreview on slot: {_previewSlot.name}");
_previewSlot.DismissPreview();
}
else
{
// Not in preview mode - forward click to parent AlbumCard (if it exists)
AlbumCard parentAlbumCard = GetComponentInParent<AlbumCard>();
if (parentAlbumCard != null)
{
Logging.Debug($"[CLICK-TRACE-CARDDISPLAY] {name} - Forwarding click to parent AlbumCard");
parentAlbumCard.OnPointerClick(eventData);
}
else
{
Logging.Debug($"[CLICK-TRACE-CARDDISPLAY] {name} - No parent AlbumCard, firing OnCardClicked event");
OnCardClicked?.Invoke(this);
}
}
}
#if UNITY_EDITOR
/// <summary>
/// Editor-only: Preview a card from the assigned definition
/// </summary>
public void PreviewFromDefinition()
{
if (editorCardDefinition == null)
{
Debug.LogWarning("[CardDisplay] No Card Definition assigned in Editor Tools.");
return;
}
CardData previewData = editorCardDefinition.CreateCardData();
SetupCard(previewData);
}
/// <summary>
/// Editor-only: Clear the preview
/// </summary>
public void ClearPreview()
{
cardData = null;
ClearCard();
}
#endif
}
#if UNITY_EDITOR
[CustomEditor(typeof(CardDisplay))]
public class CardDisplayEditor : Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
CardDisplay display = (CardDisplay)target;
EditorGUILayout.Space();
EditorGUILayout.LabelField("Editor Preview Tools", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Preview Card"))
{
display.PreviewFromDefinition();
EditorUtility.SetDirty(display);
}
if (GUILayout.Button("Clear Preview"))
{
display.ClearPreview();
EditorUtility.SetDirty(display);
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
EditorGUILayout.HelpBox(
"Assign a Card Definition in 'Editor Tools' section, then click 'Preview Card' to see how it will look at runtime.",
MessageType.Info
);
}
}
#endif
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 72cb26621865420aa763a66c06eb7f6d
timeCreated: 1762380447

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 053a2ff2538541699b134b07a07edecb
timeCreated: 1762420654

View File

@@ -1,252 +0,0 @@
using System;
using System.Collections;
using AppleHills.Data.CardSystem;
using Core;
using Data.CardSystem;
using Pixelplacement;
using UI.DragAndDrop.Core;
using UnityEngine;
namespace UI.CardSystem
{
/// <summary>
/// Draggable card for album reveal system.
/// Handles both tap and drag-hold interactions for revealing cards.
/// Auto-snaps to matching album slot on release/tap.
/// </summary>
public class AlbumCardPlacementDraggable : DraggableObject
{
[Header("Album Card Settings")]
[SerializeField] private FlippableCard flippableCard;
[SerializeField] private float holdRevealDelay = 0.1f;
private CardData _cardData;
private bool _isRevealed = false;
private bool _isDragRevealing = false;
private bool _waitingForPlacementTap = false;
private Coroutine _holdRevealCoroutine;
private bool _isHolding = false; // Track if pointer is currently down
// Events
public event Action<AlbumCardPlacementDraggable, CardData> OnCardRevealed;
public event Action<AlbumCardPlacementDraggable, CardData> OnCardPlacedInAlbum;
public CardData CardData => _cardData;
public bool IsRevealed => _isRevealed;
public CardZone Zone => _cardData?.Zone ?? CardZone.AppleHills;
protected override void Initialize()
{
base.Initialize();
// Auto-find FlippableCard if not assigned
if (flippableCard == null)
{
flippableCard = GetComponent<FlippableCard>();
}
}
/// <summary>
/// Setup the card data (stores it but doesn't reveal until tapped/dragged)
/// </summary>
public void SetupCard(CardData data)
{
_cardData = data;
if (flippableCard != null)
{
flippableCard.SetupCard(data);
}
}
/// <summary>
/// Reveal the card (flip to show front)
/// </summary>
public void RevealCard()
{
if (_isRevealed)
{
return;
}
_isRevealed = true;
if (flippableCard != null)
{
flippableCard.FlipToReveal();
}
OnCardRevealed?.Invoke(this, _cardData);
}
/// <summary>
/// Snap to the matching album slot
/// </summary>
public void SnapToAlbumSlot()
{
if (_cardData == null)
{
Logging.Warning("[AlbumCardPlacementDraggable] Cannot snap to slot - no card data assigned.");
return;
}
// Find all album card slots in the scene
AlbumCardSlot[] allSlots = FindObjectsByType<AlbumCardSlot>(FindObjectsSortMode.None);
AlbumCardSlot matchingSlot = null;
foreach (var slot in allSlots)
{
if (slot.CanAcceptCard(_cardData))
{
matchingSlot = slot;
break;
}
}
if (matchingSlot != null)
{
SetDraggingEnabled(false);
// NEW FLOW: Extract AlbumCard FIRST, then tween it
if (flippableCard != null)
{
AlbumCard extractedCard = flippableCard.ExtractAlbumCard(matchingSlot.transform);
if (extractedCard != null)
{
// Notify slot that card was placed
matchingSlot.OnCardPlaced(extractedCard);
// NOW tween the extracted AlbumCard into position
TweenExtractedCardToSlot(extractedCard, () =>
{
// After animation completes
Logging.Debug($"[AlbumCardPlacementDraggable] Card placement animation complete for {_cardData.Name}");
// Notify that card was placed
OnCardPlacedInAlbum?.Invoke(this, _cardData);
// Destroy this wrapper (the AlbumPlacementCard)
Destroy(gameObject);
});
}
else
{
Logging.Warning("[AlbumCardPlacementDraggable] Failed to extract AlbumCard from wrapper!");
}
}
}
else
{
Logging.Warning($"[AlbumCardPlacementDraggable] Could not find matching slot for card '{_cardData.Name}' (Zone: {_cardData.Zone}, Index: {_cardData.CollectionIndex})");
}
}
/// <summary>
/// Tween the extracted AlbumCard into its slot position
/// Tweens from current size to slot size - AspectRatioFitter handles width
/// </summary>
private void TweenExtractedCardToSlot(AlbumCard card, System.Action onComplete)
{
Transform cardTransform = card.transform;
RectTransform cardRect = cardTransform as RectTransform;
if (cardRect != null)
{
// Get target height from slot
RectTransform slotRect = cardTransform.parent as RectTransform;
float targetHeight = slotRect != null ? slotRect.rect.height : cardRect.sizeDelta.y;
// Tween from current size to target size (AspectRatioFitter will adjust width)
Vector2 targetSize = new Vector2(cardRect.sizeDelta.x, targetHeight);
Tween.Size(cardRect, targetSize, snapDuration, 0f, Tween.EaseOutBack);
// Tween position and rotation to slot center
Tween.LocalPosition(cardRect, Vector3.zero, snapDuration, 0f, Tween.EaseOutBack);
Tween.LocalRotation(cardTransform, Quaternion.identity, snapDuration, 0f, Tween.EaseOutBack,
completeCallback: () =>
{
Logging.Debug($"[AlbumCardPlacementDraggable] Tween complete for extracted card {card.name}, final height: {cardRect.sizeDelta.y}");
onComplete?.Invoke();
});
}
else
{
// No RectTransform, just reset and call callback
cardTransform.localPosition = Vector3.zero;
cardTransform.localRotation = Quaternion.identity;
onComplete?.Invoke();
}
}
protected override void OnPointerDownHook()
{
base.OnPointerDownHook();
_isHolding = true;
// Start hold-reveal timer if card not yet revealed
if (!_isRevealed && _holdRevealCoroutine == null)
{
_holdRevealCoroutine = StartCoroutine(HoldRevealTimer());
}
}
protected override void OnPointerUpHook(bool longPress)
{
base.OnPointerUpHook(longPress);
_isHolding = false;
// Cancel hold timer if running
if (_holdRevealCoroutine != null)
{
StopCoroutine(_holdRevealCoroutine);
_holdRevealCoroutine = null;
}
else
{
}
// Handle tap (not dragged)
if (!_wasDragged)
{
if (!_isRevealed)
{
// First tap: reveal the card
RevealCard();
_waitingForPlacementTap = true;
}
else if (_waitingForPlacementTap)
{
// Second tap: snap to slot
_waitingForPlacementTap = false;
SnapToAlbumSlot();
}
else
{
}
}
else if (_isDragRevealing)
{
// Was drag-revealed, auto-snap on release
_isDragRevealing = false;
SnapToAlbumSlot();
}
}
/// <summary>
/// Coroutine to reveal card after holding for specified duration
/// </summary>
private IEnumerator HoldRevealTimer()
{
yield return new WaitForSeconds(holdRevealDelay);
// If still holding after delay, reveal the card
if (!_isRevealed && _isHolding)
{
RevealCard();
_isDragRevealing = true;
}
_holdRevealCoroutine = null;
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 706803638ea24880bae19c87d3851ce6
timeCreated: 1762470947

View File

@@ -1,366 +0,0 @@
using AppleHills.Data.CardSystem;
using Core;
using Data.CardSystem;
using UI.DragAndDrop.Core;
using UnityEngine;
using UnityEngine.EventSystems;
namespace UI.CardSystem
{
/// <summary>
/// Specialized slot for album pages that only accepts a specific card.
/// Validates cards based on their CardDefinition.
/// Self-populates with owned cards when enabled.
/// Shows preview of target card when empty slot is tapped.
/// </summary>
public class AlbumCardSlot : DraggableSlot, IPointerClickHandler
{
[Header("Album Slot Configuration")]
[SerializeField] private CardDefinition targetCardDefinition; // Which card this slot accepts
[SerializeField] private GameObject albumCardPrefab; // Prefab to spawn when card is owned
[Header("Preview Card (for empty slots)")]
[SerializeField] private CardDisplay previewCardDisplay; // Nested CardDisplay showing greyed-out preview
[SerializeField] private float previewEnlargedScale = 2.5f;
[SerializeField] private float previewScaleDuration = 0.3f;
private bool _isOccupiedPermanently = false;
private AlbumCard _placedCard;
private bool _isPreviewShowing = false;
private Vector3 _previewOriginalScale;
private void Awake()
{
// Store original scale of preview card
if (previewCardDisplay != null)
{
_previewOriginalScale = previewCardDisplay.transform.localScale;
// Hide preview card by default
previewCardDisplay.gameObject.SetActive(false);
}
}
/// <summary>
/// Set the target card this slot should accept
/// </summary>
public void SetTargetCard(CardDefinition definition)
{
targetCardDefinition = definition;
}
/// <summary>
/// Check if this slot can accept a specific card
/// </summary>
public bool CanAcceptCard(CardData cardData)
{
if (cardData == null || targetCardDefinition == null) return false;
if (_isOccupiedPermanently) return false;
// Card must match this slot's target definition
return cardData.DefinitionId == targetCardDefinition.Id;
}
/// <summary>
/// Called when a card is successfully placed in this slot
/// </summary>
public void OnCardPlaced(AlbumCard albumCard = null)
{
_isOccupiedPermanently = true;
if (albumCard != null)
{
_placedCard = albumCard;
albumCard.SetParentSlot(this);
// Register with AlbumViewPage for enlarge/shrink handling
AlbumViewPage albumPage = FindFirstObjectByType<AlbumViewPage>();
if (albumPage != null)
{
albumPage.RegisterAlbumCard(albumCard);
}
}
}
/// <summary>
/// Check if this slot has a placed card
/// </summary>
public bool HasPlacedCard()
{
return _placedCard != null;
}
/// <summary>
/// Get the placed card (if any)
/// </summary>
public AlbumCard GetPlacedCard()
{
return _placedCard;
}
private void OnEnable()
{
// Check if we should spawn a card for this slot
CheckAndSpawnOwnedCard();
// Setup preview card display if slot is empty
SetupPreviewCard();
}
/// <summary>
/// Setup the preview card display to show target card with preview visuals
/// Preview stays hidden until user taps to show it
/// </summary>
private void SetupPreviewCard()
{
if (previewCardDisplay == null || targetCardDefinition == null)
return;
// Only setup preview if slot is empty
if (_isOccupiedPermanently || _placedCard != null)
{
// Hide preview if slot is occupied
previewCardDisplay.gameObject.SetActive(false);
return;
}
// Setup preview card data
CardData previewData = targetCardDefinition.CreateCardData();
previewData.Rarity = CardRarity.Normal; // Show as normal rarity
previewCardDisplay.SetupCard(previewData);
// Apply preview visuals (black tint and ?????? name)
previewCardDisplay.SetPreviewVisuals();
// Keep preview hidden - it'll show when user taps to enlarge
previewCardDisplay.gameObject.SetActive(false);
Logging.Debug($"[AlbumCardSlot] Setup preview card for {targetCardDefinition.Name} (hidden until tap)");
}
/// <summary>
/// Check if player owns the card for this slot and spawn it if so
/// </summary>
private void CheckAndSpawnOwnedCard()
{
// Guard: need CardSystemManager and target definition
if (CardSystemManager.Instance == null || targetCardDefinition == null)
return;
// Guard: don't spawn if already occupied
if (_isOccupiedPermanently || _placedCard != null)
return;
// Guard: need prefab to spawn
if (albumCardPrefab == null)
{
Logging.Warning($"[AlbumCardSlot] No albumCardPrefab assigned for slot targeting {targetCardDefinition.name}");
return;
}
// Check if player owns this card at ANY rarity (prioritize highest rarity)
CardData ownedCard = null;
// Check in order: Legendary > Rare > Normal
foreach (CardRarity rarity in new[] { CardRarity.Legendary, CardRarity.Rare, CardRarity.Normal })
{
CardData card = CardSystemManager.Instance.GetCardInventory().GetCard(targetCardDefinition.Id, rarity);
if (card != null)
{
ownedCard = card;
break; // Found highest rarity owned
}
}
// Spawn card if owned
if (ownedCard != null)
{
SpawnAlbumCard(ownedCard);
}
}
/// <summary>
/// Spawn an AlbumCard in this slot
/// </summary>
private void SpawnAlbumCard(CardData cardData)
{
GameObject cardObj = Instantiate(albumCardPrefab, transform);
AlbumCard albumCard = cardObj.GetComponent<AlbumCard>();
if (albumCard != null)
{
albumCard.SetupCard(cardData);
albumCard.SetParentSlot(this);
_placedCard = albumCard;
_isOccupiedPermanently = true;
// Resize the card to match the slot size (same as placed cards)
RectTransform cardRect = albumCard.transform as RectTransform;
RectTransform slotRect = transform as RectTransform;
if (cardRect != null && slotRect != null)
{
// Set height to match slot height (AspectRatioFitter will handle width)
float targetHeight = slotRect.rect.height;
cardRect.sizeDelta = new Vector2(cardRect.sizeDelta.x, targetHeight);
// Ensure position and rotation are centered
cardRect.localPosition = Vector3.zero;
cardRect.localRotation = Quaternion.identity;
}
// Register with AlbumViewPage for enlarge/shrink handling
AlbumViewPage albumPage = FindFirstObjectByType<AlbumViewPage>();
if (albumPage != null)
{
albumPage.RegisterAlbumCard(albumCard);
}
Logging.Debug($"[AlbumCardSlot] Spawned owned card '{cardData.Name}' ({cardData.Rarity}) in slot");
}
else
{
Logging.Warning($"[AlbumCardSlot] Spawned prefab has no AlbumCard component!");
Destroy(cardObj);
}
}
/// <summary>
/// Get the target card definition for this slot
/// </summary>
public CardDefinition GetTargetCardDefinition()
{
return targetCardDefinition;
}
/// <summary>
/// Handle click on slot - show/hide preview if empty
/// </summary>
public void OnPointerClick(PointerEventData eventData)
{
Logging.Debug($"[CLICK-TRACE-SLOT] OnPointerClick on {name}, _isOccupiedPermanently={_isOccupiedPermanently}, _placedCard={((_placedCard != null) ? _placedCard.name : "NULL")}, _isPreviewShowing={_isPreviewShowing}, position={eventData.position}");
// Only handle clicks if slot is empty
if (_isOccupiedPermanently || _placedCard != null)
{
Logging.Debug($"[CLICK-TRACE-SLOT] {name} - Slot is occupied, ignoring");
return;
}
// Only handle if we have a preview card setup
if (previewCardDisplay == null || targetCardDefinition == null)
{
Logging.Debug($"[CLICK-TRACE-SLOT] {name} - No preview setup, ignoring");
return;
}
if (_isPreviewShowing)
{
Logging.Debug($"[CLICK-TRACE-SLOT] {name} - Preview is showing, hiding it");
HidePreview();
}
else
{
Logging.Debug($"[CLICK-TRACE-SLOT] {name} - Preview is hidden, showing it");
ShowPreview();
}
}
/// <summary>
/// Show enlarged preview of target card
/// </summary>
private void ShowPreview()
{
if (_isPreviewShowing || previewCardDisplay == null)
return;
_isPreviewShowing = true;
// Show the preview card (already has preview visuals applied)
previewCardDisplay.gameObject.SetActive(true);
// Enable preview mode so clicks on CardDisplay forward to this slot
previewCardDisplay.SetPreviewMode(true, this);
// Reset to normal scale before enlarging
previewCardDisplay.transform.localScale = _previewOriginalScale;
// Get AlbumViewPage to show backdrop and reparent
AlbumViewPage albumPage = FindFirstObjectByType<AlbumViewPage>();
if (albumPage != null)
{
albumPage.ShowSlotPreview(this, previewCardDisplay.transform);
}
// Scale up preview card
Pixelplacement.Tween.LocalScale(previewCardDisplay.transform, _previewOriginalScale * previewEnlargedScale,
previewScaleDuration, 0f, Pixelplacement.Tween.EaseOutBack);
Logging.Debug($"[AlbumCardSlot] Showing preview for {targetCardDefinition.Name}");
}
/// <summary>
/// Hide preview and return to normal
/// </summary>
private void HidePreview()
{
if (!_isPreviewShowing || previewCardDisplay == null)
return;
_isPreviewShowing = false;
// Disable preview mode on CardDisplay
previewCardDisplay.SetPreviewMode(false, null);
// Get AlbumViewPage to hide backdrop
AlbumViewPage albumPage = FindFirstObjectByType<AlbumViewPage>();
if (albumPage != null)
{
albumPage.HideSlotPreview(this, previewCardDisplay.transform, () =>
{
// After shrink completes, reparent back to slot
previewCardDisplay.transform.SetParent(transform, false);
// Reset RectTransform properties
RectTransform previewRect = previewCardDisplay.transform as RectTransform;
if (previewRect != null)
{
// Set anchors to stretch in all directions (matching original setup)
previewRect.anchorMin = Vector2.zero; // (0, 0)
previewRect.anchorMax = Vector2.one; // (1, 1)
// Reset offsets to zero (left, right, top, bottom all = 0)
previewRect.offsetMin = Vector2.zero; // Sets left and bottom to 0
previewRect.offsetMax = Vector2.zero; // Sets right and top to 0 (note: these are negative values internally)
previewRect.pivot = new Vector2(0.5f, 0.5f);
}
previewCardDisplay.transform.localPosition = Vector3.zero;
previewCardDisplay.transform.localRotation = Quaternion.identity;
previewCardDisplay.transform.localScale = _previewOriginalScale;
// Hide the preview card after returning to slot
previewCardDisplay.gameObject.SetActive(false);
Logging.Debug($"[AlbumCardSlot] Preview hidden and reset for {targetCardDefinition.Name}");
});
}
Logging.Debug($"[AlbumCardSlot] Hiding preview for {targetCardDefinition.Name}");
}
/// <summary>
/// Public method to dismiss preview - can be called by CardDisplay when clicked
/// </summary>
public void DismissPreview()
{
Logging.Debug($"[CLICK-TRACE-SLOT] DismissPreview called on {name}");
HidePreview();
}
/// <summary>
/// Get the target card definition for this slot
/// </summary>
public CardDefinition TargetCardDefinition => targetCardDefinition;
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 514a349ba18d4842bc4292cb034f0d76
timeCreated: 1762470924

View File

@@ -1,192 +0,0 @@
using Core;
using UI.DragAndDrop.Core;
using UnityEngine;
namespace UI.CardSystem.DragDrop
{
/// <summary>
/// Booster pack specific implementation of DraggableObject.
/// Manages booster pack behavior and opening logic.
/// </summary>
public class BoosterPackDraggable : DraggableObject
{
[Header("Booster Pack Settings")]
[SerializeField] private bool canOpenOnDrop = true;
[SerializeField] private bool canOpenOnDoubleClick = true;
[Header("Tap to Open")]
[SerializeField] private bool canTapToOpen = true;
[SerializeField] private int maxTapsToOpen = 3;
[SerializeField] private float tapPulseScale = 1.15f;
[SerializeField] private float tapPulseDuration = 0.2f;
// ...existing code...
public event System.Action<BoosterPackDraggable> OnBoosterOpened;
public event System.Action<BoosterPackDraggable, int, int> OnTapped; // (booster, currentTap, maxTaps)
public event System.Action<BoosterPackDraggable> OnReadyToOpen; // Final tap reached
private bool _isOpening;
private float _lastClickTime;
private int _currentTapCount;
public bool IsOpening => _isOpening;
public int CurrentTapCount => _currentTapCount;
protected override void OnPointerUpHook(bool longPress)
{
base.OnPointerUpHook(longPress);
// Handle tap-to-open logic (only when in slot and not a long press)
if (canTapToOpen && !longPress && CurrentSlot != null)
{
_currentTapCount++;
// Pulse effect on tap (scales visual up and back down)
if (Visual != null)
{
// Calculate pulse intensity based on tap progress
float tapProgress = _currentTapCount / (float)maxTapsToOpen;
float currentPulseScale = 1f + (tapPulseScale - 1f) * (0.5f + tapProgress * 0.5f); // Increases from 1.075 to 1.15
// Save the current scale before pulsing
Vector3 baseScale = Visual.transform.localScale;
Pixelplacement.Tween.Cancel(Visual.transform.GetInstanceID());
Pixelplacement.Tween.LocalScale(Visual.transform, baseScale * currentPulseScale, tapPulseDuration * 0.5f, 0f,
Pixelplacement.Tween.EaseOutBack, completeCallback: () =>
{
// Return to the base scale we had before pulsing
Pixelplacement.Tween.LocalScale(Visual.transform, baseScale, tapPulseDuration * 0.5f, 0f, Pixelplacement.Tween.EaseInBack);
});
}
OnTapped?.Invoke(this, _currentTapCount, maxTapsToOpen);
if (_currentTapCount >= maxTapsToOpen)
{
OnReadyToOpen?.Invoke(this);
}
return; // Don't process double-click if tap-to-open is active
}
// ...existing code...
if (canOpenOnDoubleClick && !longPress)
{
float timeSinceLastClick = Time.time - _lastClickTime;
if (timeSinceLastClick < 0.3f) // Double click threshold
{
TriggerOpen();
}
_lastClickTime = Time.time;
}
}
protected override void OnDragEndedHook()
{
base.OnDragEndedHook();
// Optionally trigger open when dropped in specific zones
if (canOpenOnDrop)
{
// Could check if dropped in an "opening zone"
// For now, just a placeholder
}
}
/// <summary>
/// Trigger the booster pack opening animation and logic
/// </summary>
public void TriggerOpen()
{
if (_isOpening)
return;
_isOpening = true;
OnBoosterOpened?.Invoke(this);
// The actual opening logic (calling CardSystemManager) should be handled
// by the UI page or controller that manages this booster pack
// Visual feedback would be handled by the BoosterPackVisual
}
/// <summary>
/// Reset the opening state
/// </summary>
public void ResetOpeningState()
{
_isOpening = false;
_currentTapCount = 0;
}
/// <summary>
/// Set whether this booster is in the opening slot (disables dragging, enables tapping)
/// </summary>
public void SetInOpeningSlot(bool inSlot)
{
Logging.Debug($"[BoosterPackDraggable] SetInOpeningSlot({inSlot}) called on {name}");
SetDraggingEnabled(!inSlot); // Disable dragging when in opening slot
Logging.Debug($"[BoosterPackDraggable] SetDraggingEnabled({!inSlot}) called");
canTapToOpen = inSlot; // Enable tap-to-open when in opening slot
if (inSlot)
{
_currentTapCount = 0; // Reset tap counter when placed
// Suppress visual effects (idle animations, glow, etc.) when in opening slot
// But allow slot tween and tap pulse to still work
if (Visual != null)
{
Visual.SuppressEffects();
// Play one-time placement tween to animate into slot
// The visual will follow the parent to its slot position, then lock in place
// Get target scale from current slot if it has scale mode
Vector3 targetScale = Vector3.one;
if (CurrentSlot != null && CurrentSlot.GetComponent<DraggableSlot>() != null)
{
// Access the slot's occupant scale if it's in Scale mode
// For now, use Vector3.one as default
targetScale = Vector3.one;
}
Visual.PlayPlacementTween(0.3f, targetScale);
}
}
else
{
ResetOpeningState(); // Reset completely when removed
// Resume visual effects when removed from opening slot
if (Visual != null)
{
Visual.StopPlacementTween(); // Stop any ongoing placement tween
Visual.ResumeEffects();
}
}
}
/// <summary>
/// Reset tap count (useful when starting a new opening sequence)
/// </summary>
public void ResetTapCount()
{
_currentTapCount = 0;
}
/// <summary>
/// Enable or disable tap-to-open functionality at runtime
/// </summary>
public void SetTapToOpenEnabled(bool enabled)
{
canTapToOpen = enabled;
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: f95c1542aaa549d1867b43f6dc21e90f
timeCreated: 1762420681

View File

@@ -1,322 +0,0 @@
using Pixelplacement;
using Pixelplacement.TweenSystem;
using UI.DragAndDrop.Core;
using UnityEngine;
using UnityEngine.UI;
namespace UI.CardSystem.DragDrop
{
/// <summary>
/// Visual representation for BoosterPackDraggable.
/// Displays the booster pack sprite and handles opening animations.
/// </summary>
public class BoosterPackVisual : DraggableVisual
{
[Header("Booster Pack Visual")]
[SerializeField] private Image packImage;
[SerializeField] private Sprite packSprite;
[SerializeField] private ParticleSystem glowEffect;
[SerializeField] private Transform glowTransform;
[Header("Opening Animation")]
[SerializeField] private float openingScalePunch = 0.5f;
[SerializeField] private float openingRotationPunch = 360f;
[SerializeField] private float openingDuration = 0.5f;
private BoosterPackDraggable _boosterDraggable;
// Effect tracking
private int _glowRotationTweenId = -1;
private float _defaultGlowRate = 10f;
public override void Initialize(DraggableObject parent)
{
base.Initialize(parent);
_boosterDraggable = parent as BoosterPackDraggable;
// Get pack image if not assigned
if (packImage == null)
{
packImage = GetComponentInChildren<Image>();
}
// Set initial sprite
if (packImage != null && packSprite != null)
{
packImage.sprite = packSprite;
}
// Subscribe to booster events
if (_boosterDraggable != null)
{
_boosterDraggable.OnBoosterOpened += HandleBoosterOpened;
_boosterDraggable.OnTapped += HandleTapped;
}
// Start glow effect if available
if (glowEffect != null && !glowEffect.isPlaying)
{
glowEffect.Play();
}
}
protected override void UpdateVisualContent()
{
// Update glow rotation for visual interest (skip if effects suppressed)
if (glowTransform != null && !_effectsSuppressed)
{
glowTransform.Rotate(Vector3.forward * 30f * Time.deltaTime);
}
}
private void HandleBoosterOpened(BoosterPackDraggable booster)
{
PlayOpeningAnimation();
}
private void HandleTapped(BoosterPackDraggable booster, int currentTap, int maxTaps)
{
PlayShakeAnimation(currentTap, maxTaps);
}
/// <summary>
/// Play progressive shake animation based on tap intensity
/// This is always allowed, even when effects are suppressed
/// </summary>
public void PlayShakeAnimation(int intensity, int maxIntensity)
{
float normalizedIntensity = (float)intensity / maxIntensity;
float shakeAmount = Mathf.Lerp(5f, 30f, normalizedIntensity);
float shakeDuration = 0.15f;
// Shake rotation
Vector3 shakeRotation = new Vector3(
Random.Range(-shakeAmount, shakeAmount),
Random.Range(-shakeAmount, shakeAmount),
Random.Range(-shakeAmount, shakeAmount)
);
Tween.Rotation(transform, transform.eulerAngles + shakeRotation,
shakeDuration, 0f, Tween.EaseOutBack,
completeCallback: () => {
Tween.Rotation(transform, Vector3.zero,
shakeDuration, 0f, Tween.EaseInBack);
});
// NOTE: Scale pulse is handled by BoosterPackDraggable.OnPointerUpHook()
// We don't need a duplicate scale tween here - it would conflict!
// Extra glow burst on final tap (only if effects not suppressed)
if (intensity == maxIntensity && glowEffect != null && !_effectsSuppressed)
{
var emission = glowEffect.emission;
emission.rateOverTimeMultiplier = 50f;
}
}
/// <summary>
/// Play the booster pack opening animation
/// </summary>
public void PlayOpeningAnimation()
{
// Scale punch
Vector3 targetScale = transform.localScale * (1f + openingScalePunch);
Tween.LocalScale(transform, targetScale, openingDuration / 2f, 0f, Tween.EaseOutBack,
completeCallback: () => {
Tween.LocalScale(transform, Vector3.one, openingDuration / 2f, 0f, Tween.EaseInBack);
});
// Rotation
Tween.Rotation(transform, transform.eulerAngles + Vector3.forward * openingRotationPunch,
openingDuration, 0f, Tween.EaseOutBack);
// Glow burst
if (glowEffect != null)
{
glowEffect.Stop();
glowEffect.Play();
}
}
/// <summary>
/// Set the booster pack sprite
/// </summary>
public void SetPackSprite(Sprite sprite)
{
packSprite = sprite;
if (packImage != null)
{
packImage.sprite = packSprite;
}
}
protected override void OnPointerEnterVisual()
{
base.OnPointerEnterVisual();
// Extra glow when hovering (skip if effects suppressed)
if (glowEffect != null && !_effectsSuppressed)
{
var emission = glowEffect.emission;
emission.rateOverTimeMultiplier = 20f;
}
}
protected override void OnPointerExitVisual()
{
base.OnPointerExitVisual();
// Restore normal glow (skip if effects suppressed)
if (glowEffect != null && !_effectsSuppressed)
{
var emission = glowEffect.emission;
emission.rateOverTimeMultiplier = _defaultGlowRate;
}
}
#region Effect Management Overrides
protected override void OnEffectsSuppressed()
{
base.OnEffectsSuppressed();
// Stop glow effect
if (glowEffect != null)
{
glowEffect.Stop();
}
// Cancel glow rotation tween if tracked
if (_glowRotationTweenId != -1)
{
Tween.Stop(_glowRotationTweenId);
_glowRotationTweenId = -1;
}
// Reset glow rotation to zero
if (glowTransform != null)
{
glowTransform.localRotation = Quaternion.identity;
}
}
protected override void OnEffectsResumed()
{
base.OnEffectsResumed();
// Resume glow effect
if (glowEffect != null && !glowEffect.isPlaying)
{
glowEffect.Play();
}
}
protected override void OnEffectsReset()
{
base.OnEffectsReset();
// Stop glow effect
if (glowEffect != null)
{
glowEffect.Stop();
}
// Reset glow rotation
if (glowTransform != null)
{
glowTransform.localRotation = Quaternion.identity;
}
// NOTE: Tap pulse scale is handled by BoosterPackDraggable, not here
}
#endregion
#region Event Handler Overrides (Block when in Opening Slot)
protected override void HandleDragStarted(DraggableObject draggable)
{
// Don't call base if effects are suppressed (in opening slot)
if (_effectsSuppressed)
return;
base.HandleDragStarted(draggable);
}
protected override void HandleDragEnded(DraggableObject draggable)
{
// ALWAYS reset canvas sorting, even when effects are suppressed
if (canvas != null)
{
canvas.overrideSorting = false;
}
// Skip other drag end effects if suppressed (in opening slot)
if (_effectsSuppressed)
return;
base.HandleDragEnded(draggable);
}
protected override void HandlePointerEnter(DraggableObject draggable)
{
// Don't call base if effects are suppressed (in opening slot)
if (_effectsSuppressed)
return;
base.HandlePointerEnter(draggable);
}
protected override void HandlePointerExit(DraggableObject draggable)
{
// Don't call base if effects are suppressed (in opening slot)
if (_effectsSuppressed)
return;
base.HandlePointerExit(draggable);
}
protected override void HandlePointerDown(DraggableObject draggable)
{
// Don't call base if effects are suppressed (in opening slot)
// This allows the BoosterPackDraggable to handle tap pulse itself
if (_effectsSuppressed)
return;
base.HandlePointerDown(draggable);
}
protected override void HandlePointerUp(DraggableObject draggable, bool longPress)
{
// Don't call base if effects are suppressed (in opening slot)
// This allows the BoosterPackDraggable to handle tap pulse itself
if (_effectsSuppressed)
return;
base.HandlePointerUp(draggable, longPress);
}
protected override void HandleSelection(DraggableObject draggable, bool selected)
{
// Don't call base if effects are suppressed (in opening slot)
if (_effectsSuppressed)
return;
base.HandleSelection(draggable, selected);
}
#endregion
protected override void OnDestroy()
{
base.OnDestroy();
if (_boosterDraggable != null)
{
_boosterDraggable.OnBoosterOpened -= HandleBoosterOpened;
}
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: a7d9474ece3b4d2ebad19ae178b22f4d
timeCreated: 1762420699

View File

@@ -1,62 +0,0 @@
using AppleHills.Data.CardSystem;
using UI.DragAndDrop.Core;
using UnityEngine;
namespace UI.CardSystem.DragDrop
{
/// <summary>
/// Card-specific implementation of DraggableObject.
/// Manages card data and card-specific drag behavior.
/// </summary>
public class CardDraggable : DraggableObject
{
[Header("Card Data")]
[SerializeField] private CardData cardData;
// Events
public event System.Action<CardDraggable, CardData> OnCardDataChanged;
public CardData CardData => cardData;
/// <summary>
/// Set the card data for this draggable card
/// </summary>
public void SetCardData(CardData data)
{
cardData = data;
OnCardDataChanged?.Invoke(this, cardData);
// Update visual if it exists
if (_visualInstance != null && _visualInstance is CardDraggableVisual cardVisual)
{
cardVisual.RefreshCardDisplay();
}
}
protected override void OnDragStartedHook()
{
base.OnDragStartedHook();
// Card-specific drag started behavior
}
protected override void OnDragEndedHook()
{
base.OnDragEndedHook();
// Card-specific drag ended behavior
}
protected override void OnSelectionChangedHook(bool selected)
{
base.OnSelectionChangedHook(selected);
// Card-specific selection behavior
}
protected override void OnSlotChangedHook(DraggableSlot previousSlot, DraggableSlot newSlot)
{
base.OnSlotChangedHook(previousSlot, newSlot);
// Card-specific slot changed behavior
// Could trigger events for card collection reordering, etc.
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 5a2741bb7299441b9f9bd44d746ebb4b
timeCreated: 1762420654

View File

@@ -1,121 +0,0 @@
using AppleHills.Data.CardSystem;
using UI.DragAndDrop.Core;
using UnityEngine;
namespace UI.CardSystem.DragDrop
{
/// <summary>
/// Visual representation for CardDraggable.
/// Uses the existing CardDisplay component to render the card.
/// </summary>
public class CardDraggableVisual : DraggableVisual
{
[Header("Card Visual Components")]
[SerializeField] private CardDisplay cardDisplay;
[SerializeField] private Transform shadowTransform;
[SerializeField] private float shadowOffset = 20f;
private Vector3 _shadowInitialPosition;
private CardDraggable _cardDraggable;
public CardDisplay CardDisplay => cardDisplay;
public override void Initialize(DraggableObject parent)
{
base.Initialize(parent);
_cardDraggable = parent as CardDraggable;
// Get CardDisplay component if not assigned
if (cardDisplay == null)
{
cardDisplay = GetComponentInChildren<CardDisplay>();
}
// Initialize shadow
if (shadowTransform != null)
{
_shadowInitialPosition = shadowTransform.localPosition;
}
// Subscribe to card data changes
if (_cardDraggable != null)
{
_cardDraggable.OnCardDataChanged += HandleCardDataChanged;
// Initial card setup
if (_cardDraggable.CardData != null && cardDisplay != null)
{
cardDisplay.SetupCard(_cardDraggable.CardData);
}
}
}
protected override void UpdateVisualContent()
{
// CardDisplay handles its own rendering, no need to update every frame
// This is called every frame but we only update when card data changes
}
/// <summary>
/// Refresh the card display with current data
/// </summary>
public void RefreshCardDisplay()
{
if (cardDisplay != null && _cardDraggable != null && _cardDraggable.CardData != null)
{
cardDisplay.SetupCard(_cardDraggable.CardData);
}
}
private void HandleCardDataChanged(CardDraggable draggable, CardData data)
{
RefreshCardDisplay();
}
protected override void OnPointerDownVisual()
{
base.OnPointerDownVisual();
// Move shadow down when pressed
if (shadowTransform != null)
{
shadowTransform.localPosition = _shadowInitialPosition + (-Vector3.up * shadowOffset);
}
}
protected override void OnPointerUpVisual(bool longPress)
{
base.OnPointerUpVisual(longPress);
// Restore shadow position
if (shadowTransform != null)
{
shadowTransform.localPosition = _shadowInitialPosition;
}
}
protected override void OnDragStartedVisual()
{
base.OnDragStartedVisual();
// Card-specific visual effects when dragging starts
}
protected override void OnDragEndedVisual()
{
base.OnDragEndedVisual();
// Card-specific visual effects when dragging ends
}
protected override void OnDestroy()
{
base.OnDestroy();
if (_cardDraggable != null)
{
_cardDraggable.OnCardDataChanged -= HandleCardDataChanged;
}
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 2a4c3884410d44f98182cd8119a972a4
timeCreated: 1762420668

View File

@@ -1,673 +0,0 @@
using System;
using AppleHills.Data.CardSystem;
using Core;
using Pixelplacement;
using Pixelplacement.TweenSystem;
using UnityEngine;
using UnityEngine.EventSystems;
namespace UI.CardSystem
{
/// <summary>
/// Flippable card wrapper that shows a card back, then flips to reveal the CardDisplay front.
/// This component nests an existing CardDisplay prefab to reuse card visuals everywhere.
/// </summary>
public class FlippableCard : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler
{
[Header("Card References")]
[SerializeField] private GameObject cardBackObject; // The card back visual
[SerializeField] private GameObject cardFrontObject; // Your CardDisplay prefab instance
[SerializeField] private CardDisplay cardDisplay; // Reference to CardDisplay component
[SerializeField] private AlbumCard albumCard; // Reference to nested AlbumCard (for album placement flow)
[Header("Idle Hover Animation")]
[SerializeField] private bool enableIdleHover = true;
[SerializeField] private float idleHoverHeight = 10f;
[SerializeField] private float idleHoverDuration = 1.5f;
[SerializeField] private float hoverScaleMultiplier = 1.05f;
[Header("Flip Animation")]
[SerializeField] private float flipDuration = 0.6f;
[SerializeField] private float flipScalePunch = 1.1f;
[Header("New/Repeat Card Display")]
[SerializeField] private GameObject newCardText;
[SerializeField] private GameObject newCardIdleText;
[SerializeField] private GameObject repeatText;
[SerializeField] private GameObject progressBarContainer;
[SerializeField] private int cardsToUpgrade = 5;
[SerializeField] private float enlargedScale = 1.5f;
// State
private bool _isFlipped = false;
private bool _isFlipping = false;
private TweenBase _idleHoverTween;
private CardData _cardData;
private Vector2 _originalPosition; // Track original spawn position
private bool _isWaitingForTap = false; // Waiting for tap after reveal
private bool _isNew = false; // Is this a new card
private int _ownedCount = 0; // Owned count for repeat cards
private bool _isClickable = true; // Can this card be clicked
// Events
public event Action<FlippableCard, CardData> OnCardRevealed;
public event Action<FlippableCard> OnCardTappedAfterReveal;
public event Action<FlippableCard> OnClickedWhileInactive; // Fired when clicked but not clickable
public event Action<FlippableCard> OnFlipStarted; // Fired when flip animation begins
public bool IsFlipped => _isFlipped;
public CardData CardData => _cardData;
public int CardsToUpgrade => cardsToUpgrade; // Expose upgrade threshold
private void Awake()
{
// Auto-find CardDisplay if not assigned
if (cardDisplay == null && cardFrontObject != null)
{
cardDisplay = cardFrontObject.GetComponent<CardDisplay>();
}
// Auto-find AlbumCard if not assigned
if (albumCard == null)
{
albumCard = GetComponentInChildren<AlbumCard>();
}
// Card back: starts at 0° rotation (normal, facing camera, clickable)
// Card front: starts at 180° rotation (flipped away, will rotate to 0° when revealed)
if (cardBackObject != null)
{
cardBackObject.transform.localRotation = Quaternion.Euler(0, 0, 0);
cardBackObject.SetActive(true);
}
if (cardFrontObject != null)
{
cardFrontObject.transform.localRotation = Quaternion.Euler(0, 180, 0);
cardFrontObject.SetActive(false);
}
// Hide all new/repeat UI elements initially
if (newCardText != null)
newCardText.SetActive(false);
if (newCardIdleText != null)
newCardIdleText.SetActive(false);
if (repeatText != null)
repeatText.SetActive(false);
if (progressBarContainer != null)
progressBarContainer.SetActive(false);
}
private void Start()
{
// Save the original position so we can return to it after hover
RectTransform rectTransform = GetComponent<RectTransform>();
if (rectTransform != null)
{
_originalPosition = rectTransform.anchoredPosition;
}
// Start idle hover animation
if (enableIdleHover && !_isFlipped)
{
StartIdleHover();
}
}
/// <summary>
/// Setup the card data (stores it but doesn't reveal until flipped)
/// </summary>
public void SetupCard(CardData data)
{
_cardData = data;
// Setup the CardDisplay but keep it hidden
if (cardDisplay != null)
{
cardDisplay.SetupCard(data);
}
}
/// <summary>
/// Flip the card to reveal the front
/// </summary>
public void FlipToReveal()
{
if (_isFlipped || _isFlipping)
return;
_isFlipping = true;
// Fire flip started event IMMEDIATELY (before animations)
OnFlipStarted?.Invoke(this);
// Stop idle hover
StopIdleHover();
// Flip animation: Rotate the visual children (back from 0→90, front from 180→0)
// ...existing code...
// Card back: 0° → 90° (rotates away)
// Card front: 180° → 90° → 0° (rotates into view)
// Phase 1: Rotate both to 90 degrees (edge view)
if (cardBackObject != null)
{
Tween.LocalRotation(cardBackObject.transform, Quaternion.Euler(0, 90, 0), flipDuration * 0.5f, 0f, Tween.EaseInOut);
}
if (cardFrontObject != null)
{
Tween.LocalRotation(cardFrontObject.transform, Quaternion.Euler(0, 90, 0), flipDuration * 0.5f, 0f, Tween.EaseInOut,
completeCallback: () =>
{
// At edge (90°), switch visibility
if (cardBackObject != null)
cardBackObject.SetActive(false);
if (cardFrontObject != null)
cardFrontObject.SetActive(true);
// Phase 2: Rotate front from 90 to 0 (show at correct orientation)
Tween.LocalRotation(cardFrontObject.transform, Quaternion.Euler(0, 0, 0), flipDuration * 0.5f, 0f, Tween.EaseInOut,
completeCallback: () =>
{
_isFlipped = true;
_isFlipping = false;
// Fire revealed event
OnCardRevealed?.Invoke(this, _cardData);
});
});
}
// Scale punch during flip for extra juice
Vector3 originalScale = transform.localScale;
Tween.LocalScale(transform, originalScale * flipScalePunch, flipDuration * 0.5f, 0f, Tween.EaseOutBack,
completeCallback: () =>
{
Tween.LocalScale(transform, originalScale, flipDuration * 0.5f, 0f, Tween.EaseInBack);
});
}
/// <summary>
/// Start idle hover animation (gentle bobbing)
/// </summary>
private void StartIdleHover()
{
if (_idleHoverTween != null)
return;
RectTransform rectTransform = GetComponent<RectTransform>();
if (rectTransform == null)
return;
Vector2 originalPos = rectTransform.anchoredPosition;
Vector2 targetPos = originalPos + Vector2.up * idleHoverHeight;
_idleHoverTween = Tween.Value(0f, 1f,
(val) =>
{
if (rectTransform != null)
{
float t = Mathf.Sin(val * Mathf.PI * 2f) * 0.5f + 0.5f; // Smooth sine wave
rectTransform.anchoredPosition = Vector2.Lerp(originalPos, targetPos, t);
}
},
idleHoverDuration, 0f, Tween.EaseInOut, Tween.LoopType.Loop);
}
/// <summary>
/// Stop idle hover animation
/// </summary>
private void StopIdleHover()
{
if (_idleHoverTween != null)
{
_idleHoverTween.Stop();
_idleHoverTween = null;
// Reset to ORIGINAL position (not Vector2.zero!)
RectTransform rectTransform = GetComponent<RectTransform>();
if (rectTransform != null)
{
Tween.AnchoredPosition(rectTransform, _originalPosition, 0.3f, 0f, Tween.EaseOutBack);
}
}
}
#region Pointer Event Handlers
public void OnPointerEnter(PointerEventData eventData)
{
if (_isFlipped || _isFlipping)
return;
// Scale up slightly on hover
Tween.LocalScale(transform, Vector3.one * hoverScaleMultiplier, 0.2f, 0f, Tween.EaseOutBack);
}
public void OnPointerExit(PointerEventData eventData)
{
if (_isFlipped || _isFlipping)
return;
// Scale back to normal
Tween.LocalScale(transform, Vector3.one, 0.2f, 0f, Tween.EaseOutBack);
}
public void OnPointerClick(PointerEventData eventData)
{
Logging.Debug($"[CLICK-TRACE-FLIPPABLE] OnPointerClick on {name}, _isClickable={_isClickable}, _isWaitingForTap={_isWaitingForTap}, _isFlipped={_isFlipped}, position={eventData.position}");
// If not clickable, notify and return
if (!_isClickable)
{
Logging.Debug($"[CLICK-TRACE-FLIPPABLE] {name} - Not clickable, firing OnClickedWhileInactive");
OnClickedWhileInactive?.Invoke(this);
return;
}
// If waiting for tap after reveal, handle that
if (_isWaitingForTap)
{
Logging.Debug($"[CLICK-TRACE-FLIPPABLE] {name} - Waiting for tap, dismissing enlarged state");
OnCardTappedAfterReveal?.Invoke(this);
_isWaitingForTap = false;
return;
}
if (_isFlipped || _isFlipping)
{
Logging.Debug($"[CLICK-TRACE-FLIPPABLE] {name} - Ignoring click (flipped={_isFlipped}, flipping={_isFlipping})");
return;
}
Logging.Debug($"[CLICK-TRACE-FLIPPABLE] {name} - Processing click, starting flip");
// Flip on click
FlipToReveal();
}
#endregion
#region New/Repeat Card Display
/// <summary>
/// Show this card as a new card (enlarge, show "NEW CARD" text, wait for tap)
/// </summary>
public void ShowAsNew()
{
_isNew = true;
_isWaitingForTap = true;
// Show new card text
if (newCardText != null)
newCardText.SetActive(true);
// Enlarge the card
EnlargeCard();
}
/// <summary>
/// Show this card as a repeat that will trigger an upgrade (enlarge, show progress, auto-transition to upgrade)
/// </summary>
/// <param name="ownedCount">Number of copies owned BEFORE this one</param>
/// <param name="lowerRarityCard">The existing card data at lower rarity (for upgrade reference)</param>
public void ShowAsRepeatWithUpgrade(int ownedCount, AppleHills.Data.CardSystem.CardData lowerRarityCard)
{
_isNew = false;
_ownedCount = ownedCount;
_isWaitingForTap = false; // Don't wait yet - upgrade will happen automatically
// Show repeat text
if (repeatText != null)
repeatText.SetActive(true);
// Enlarge the card
EnlargeCard();
// Show progress bar with owned count, then auto-trigger upgrade
ShowProgressBar(ownedCount, () =>
{
// Progress animation complete - trigger upgrade!
TriggerUpgradeTransition(lowerRarityCard);
});
}
/// <summary>
/// Trigger the upgrade transition (called after progress bar fills)
/// </summary>
private void TriggerUpgradeTransition(AppleHills.Data.CardSystem.CardData lowerRarityCard)
{
Logging.Debug($"[FlippableCard] Triggering upgrade transition from {lowerRarityCard.Rarity}!");
AppleHills.Data.CardSystem.CardRarity oldRarity = lowerRarityCard.Rarity;
AppleHills.Data.CardSystem.CardRarity newRarity = oldRarity + 1;
// Reset the lower rarity count to 0
lowerRarityCard.CopiesOwned = 0;
// Create upgraded card data
AppleHills.Data.CardSystem.CardData upgradedCardData = new AppleHills.Data.CardSystem.CardData(_cardData);
upgradedCardData.Rarity = newRarity;
upgradedCardData.CopiesOwned = 1;
// Check if we already have this card at the higher rarity
bool isNewAtHigherRarity = Data.CardSystem.CardSystemManager.Instance.IsCardNew(upgradedCardData, out AppleHills.Data.CardSystem.CardData existingHigherRarity);
// Add the higher rarity card to inventory
Data.CardSystem.CardSystemManager.Instance.GetCardInventory().AddCard(upgradedCardData);
// Update our displayed card data
_cardData.Rarity = newRarity;
// Transition to appropriate display
if (isNewAtHigherRarity || newRarity == AppleHills.Data.CardSystem.CardRarity.Legendary)
{
// Show as NEW at higher rarity
TransitionToNewCardView(newRarity);
}
else
{
// Show progress for higher rarity, then transition to NEW
int ownedAtHigherRarity = existingHigherRarity.CopiesOwned;
ShowProgressBar(ownedAtHigherRarity, () =>
{
TransitionToNewCardView(newRarity);
});
}
}
/// <summary>
/// Show this card as a repeat (enlarge, show progress bar, wait for tap)
/// </summary>
/// <param name="ownedCount">Number of copies owned BEFORE this one</param>
public void ShowAsRepeat(int ownedCount)
{
_isNew = false;
_ownedCount = ownedCount;
_isWaitingForTap = true;
// Show repeat text
if (repeatText != null)
repeatText.SetActive(true);
// Enlarge the card
EnlargeCard();
// Show progress bar with owned count, then blink new element
ShowProgressBar(ownedCount, () =>
{
// Progress animation complete
});
}
/// <summary>
/// Show this card as upgraded (hide progress bar, show as new with upgraded rarity)
/// </summary>
public void ShowAsUpgraded(AppleHills.Data.CardSystem.CardRarity oldRarity, AppleHills.Data.CardSystem.CardRarity newRarity)
{
_isNew = true;
_isWaitingForTap = true;
// Update the CardDisplay to show new rarity
if (cardDisplay != null && _cardData != null)
{
_cardData.Rarity = newRarity;
cardDisplay.SetupCard(_cardData);
}
// Hide progress bar and repeat text
if (progressBarContainer != null)
progressBarContainer.SetActive(false);
if (repeatText != null)
repeatText.SetActive(false);
// Show new card text (it's now a "new" card at the higher rarity)
if (newCardText != null)
newCardText.SetActive(true);
Logging.Debug($"[FlippableCard] Card upgraded from {oldRarity} to {newRarity}! Showing as NEW.");
// Card is already enlarged from the repeat display, so no need to enlarge again
}
/// <summary>
/// Show this card as upgraded with progress bar (already have copies at higher rarity)
/// </summary>
public void ShowAsUpgradedWithProgress(AppleHills.Data.CardSystem.CardRarity oldRarity, AppleHills.Data.CardSystem.CardRarity newRarity, int ownedAtNewRarity)
{
_isNew = false;
_isWaitingForTap = false; // Don't wait for tap yet, progress bar will complete first
// Hide new card text
if (newCardText != null)
newCardText.SetActive(false);
// Show repeat text (it's a repeat at the new rarity)
if (repeatText != null)
repeatText.SetActive(true);
// Show progress bar for the new rarity
ShowProgressBar(ownedAtNewRarity, () =>
{
// Progress animation complete - now transition to "NEW CARD" view
TransitionToNewCardView(newRarity);
});
Logging.Debug($"[FlippableCard] Card upgraded from {oldRarity} to {newRarity}! Showing progress {ownedAtNewRarity}/5");
}
/// <summary>
/// Transition to "NEW CARD" view after upgrade progress completes
/// </summary>
private void TransitionToNewCardView(AppleHills.Data.CardSystem.CardRarity newRarity)
{
Logging.Debug($"[FlippableCard] Transitioning to NEW CARD view at {newRarity} rarity");
// Update the CardDisplay to show new rarity
if (cardDisplay != null && _cardData != null)
{
_cardData.Rarity = newRarity;
cardDisplay.SetupCard(_cardData);
}
// Hide progress bar and repeat text
if (progressBarContainer != null)
progressBarContainer.SetActive(false);
if (repeatText != null)
repeatText.SetActive(false);
// Show "NEW CARD" text
if (newCardText != null)
newCardText.SetActive(true);
// Now wait for tap
_isNew = true;
_isWaitingForTap = true;
Logging.Debug($"[FlippableCard] Now showing as NEW CARD at {newRarity}, waiting for tap");
}
/// <summary>
/// Enlarge the card
/// </summary>
private void EnlargeCard()
{
Tween.LocalScale(transform, Vector3.one * enlargedScale, 0.3f, 0f, Tween.EaseOutBack);
}
/// <summary>
/// Return card to normal size
/// </summary>
public void ReturnToNormalSize()
{
Tween.LocalScale(transform, Vector3.one, 0.3f, 0f, Tween.EaseOutBack, completeCallback: () =>
{
// After returning to normal, hide new card text, show idle text
if (_isNew)
{
if (newCardText != null)
newCardText.SetActive(false);
if (newCardIdleText != null)
newCardIdleText.SetActive(true);
}
// Keep repeat text visible
});
}
/// <summary>
/// Show progress bar with owned count, then blink the new element
/// </summary>
private void ShowProgressBar(int ownedCount, System.Action onComplete)
{
if (progressBarContainer == null)
{
onComplete?.Invoke();
return;
}
progressBarContainer.SetActive(true);
// Get all child Image components
UnityEngine.UI.Image[] progressElements = progressBarContainer.GetComponentsInChildren<UnityEngine.UI.Image>(true);
// Check if we have the required number of elements (should match cardsToUpgrade)
if (progressElements.Length < cardsToUpgrade)
{
Logging.Warning($"[FlippableCard] Not enough Image components in progress bar! Expected {cardsToUpgrade}, found {progressElements.Length}");
onComplete?.Invoke();
return;
}
// Disable all elements first
foreach (var img in progressElements)
{
img.enabled = false;
}
// Show owned count (from the END, going backwards)
// E.g., if owned 3 cards, enable elements at index [4], [3], [2] (last 3 elements)
int startIndex = Mathf.Max(0, cardsToUpgrade - ownedCount);
for (int i = startIndex; i < cardsToUpgrade && i < progressElements.Length; i++)
{
progressElements[i].enabled = true;
}
// Wait a moment, then blink the new element
// New element is at index (cardsToUpgrade - ownedCount - 1)
int newElementIndex = Mathf.Max(0, cardsToUpgrade - ownedCount - 1);
if (newElementIndex >= 0 && newElementIndex < progressElements.Length)
{
Tween.Value(0f, 1f, (val) => { }, 0.3f, 0f, completeCallback: () =>
{
BlinkProgressElement(newElementIndex, progressElements, onComplete);
});
}
else
{
onComplete?.Invoke();
}
}
/// <summary>
/// Blink a progress element (enable/disable rapidly)
/// </summary>
private void BlinkProgressElement(int index, UnityEngine.UI.Image[] elements, System.Action onComplete)
{
if (index < 0 || index >= elements.Length)
{
onComplete?.Invoke();
return;
}
UnityEngine.UI.Image element = elements[index];
int blinkCount = 0;
const int maxBlinks = 3;
void Blink()
{
element.enabled = !element.enabled;
blinkCount++;
if (blinkCount >= maxBlinks * 2)
{
element.enabled = true; // End on enabled
onComplete?.Invoke();
}
else
{
Tween.Value(0f, 1f, (val) => { }, 0.15f, 0f, completeCallback: Blink);
}
}
Blink();
}
/// <summary>
/// Enable or disable clickability of this card
/// </summary>
public void SetClickable(bool clickable)
{
_isClickable = clickable;
}
/// <summary>
/// Jiggle the card (shake animation)
/// </summary>
public void Jiggle()
{
// Quick shake animation - rotate left, then right, then center
Transform cardTransform = transform;
Quaternion originalRotation = cardTransform.localRotation;
// Shake sequence: 0 -> -5 -> +5 -> 0
Tween.LocalRotation(cardTransform, Quaternion.Euler(0, 0, -5), 0.05f, 0f, Tween.EaseInOut,
completeCallback: () =>
{
Tween.LocalRotation(cardTransform, Quaternion.Euler(0, 0, 5), 0.1f, 0f, Tween.EaseInOut,
completeCallback: () =>
{
Tween.LocalRotation(cardTransform, originalRotation, 0.05f, 0f, Tween.EaseInOut);
});
});
}
/// <summary>
/// Extract the nested AlbumCard and reparent it to a new parent
/// Used when placing card in album slot - extracts the AlbumCard from this wrapper
/// The caller is responsible for tweening it to the final position
/// </summary>
/// <param name="newParent">The transform to reparent the AlbumCard to (typically the AlbumCardSlot)</param>
/// <returns>The extracted AlbumCard component, or null if not found</returns>
public AlbumCard ExtractAlbumCard(Transform newParent)
{
if (albumCard == null)
{
Logging.Warning("[FlippableCard] Cannot extract AlbumCard - none found!");
return null;
}
// Reparent AlbumCard to new parent (maintain world position temporarily)
// The caller will tween it to the final position
albumCard.transform.SetParent(newParent, true);
// Setup the card data on the AlbumCard
if (_cardData != null)
{
albumCard.SetupCard(_cardData);
}
Logging.Debug($"[FlippableCard] Extracted AlbumCard '{_cardData?.Name}' to {newParent.name} - ready for tween");
return albumCard;
}
#endregion
private void OnDestroy()
{
StopIdleHover();
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: ffa05ec4ecbd4cc485e2127683c29f09
timeCreated: 1762454507

View File

@@ -1,300 +0,0 @@
using System;
using System.Collections;
using Core;
using Data.CardSystem;
using Pixelplacement;
using UnityEngine;
using UnityEngine.UI;
namespace UI.CardSystem
{
/// <summary>
/// Singleton UI component for granting booster packs from minigames.
/// Displays a booster pack with glow effect, waits for user to click continue,
/// then shows the scrapbook button and animates the pack flying to it before granting the reward.
/// The scrapbook button is automatically hidden after the animation completes.
/// </summary>
public class MinigameBoosterGiver : MonoBehaviour
{
public static MinigameBoosterGiver Instance { get; private set; }
[Header("Visual References")]
[SerializeField] private GameObject visualContainer;
[SerializeField] private RectTransform boosterImage;
[SerializeField] private RectTransform glowImage;
[SerializeField] private Button continueButton;
[Header("Animation Settings")]
[SerializeField] private float hoverAmount = 20f;
[SerializeField] private float hoverDuration = 1.5f;
[SerializeField] private float glowPulseMax = 1.1f;
[SerializeField] private float glowPulseDuration = 1.2f;
[Header("Disappear Animation")]
[SerializeField] private Vector2 targetBottomLeftOffset = new Vector2(100f, 100f);
[SerializeField] private float disappearDuration = 0.8f;
[SerializeField] private float disappearScale = 0.2f;
private Vector3 _boosterInitialPosition;
private Vector3 _boosterInitialScale;
private Vector3 _glowInitialScale;
private Coroutine _currentSequence;
private Action _onCompleteCallback;
private void Awake()
{
// Singleton pattern
if (Instance != null && Instance != this)
{
Logging.Warning("[MinigameBoosterGiver] Duplicate instance found. Destroying.");
Destroy(gameObject);
return;
}
Instance = this;
// Cache initial values
if (boosterImage != null)
{
_boosterInitialPosition = boosterImage.localPosition;
_boosterInitialScale = boosterImage.localScale;
}
if (glowImage != null)
{
_glowInitialScale = glowImage.localScale;
}
// Setup button listener
if (continueButton != null)
{
continueButton.onClick.AddListener(OnContinueClicked);
}
// Start hidden
if (visualContainer != null)
{
visualContainer.SetActive(false);
}
}
private void OnDestroy()
{
if (Instance == this)
{
Instance = null;
}
if (continueButton != null)
{
continueButton.onClick.RemoveListener(OnContinueClicked);
}
}
/// <summary>
/// Public API to give a booster pack. Displays UI, starts animations, and waits for user interaction.
/// </summary>
/// <param name="onComplete">Optional callback when the sequence completes and pack is granted</param>
public void GiveBooster(Action onComplete = null)
{
if (_currentSequence != null)
{
Logging.Warning("[MinigameBoosterGiver] Already running a sequence. Ignoring new request.");
return;
}
_onCompleteCallback = onComplete;
_currentSequence = StartCoroutine(GiveBoosterSequence());
}
private IEnumerator GiveBoosterSequence()
{
// Show the visual
if (visualContainer != null)
{
visualContainer.SetActive(true);
}
// Reset positions and scales
if (boosterImage != null)
{
boosterImage.localPosition = _boosterInitialPosition;
boosterImage.localScale = _boosterInitialScale;
}
if (glowImage != null)
{
glowImage.localScale = _glowInitialScale;
}
// Enable the continue button
if (continueButton != null)
{
continueButton.interactable = true;
}
// Start idle hovering animation on booster (ping-pong)
if (boosterImage != null)
{
Vector3 hoverTarget = _boosterInitialPosition + Vector3.up * hoverAmount;
Tween.LocalPosition(boosterImage, hoverTarget, hoverDuration, 0f, Tween.EaseLinear, Tween.LoopType.PingPong);
}
// Start pulsing animation on glow (ping-pong scale)
if (glowImage != null)
{
Vector3 glowPulseScale = _glowInitialScale * glowPulseMax;
Tween.LocalScale(glowImage, glowPulseScale, glowPulseDuration, 0f, Tween.EaseOut, Tween.LoopType.PingPong);
}
// Wait for button click (handled by OnContinueClicked)
yield return null;
}
private void OnContinueClicked()
{
if (_currentSequence == null)
{
return; // Not in a sequence
}
// Disable button to prevent double-clicks
if (continueButton != null)
{
continueButton.interactable = false;
}
// Stop the ongoing animations by stopping all tweens on these objects
if (boosterImage != null)
{
Tween.Stop(boosterImage.GetInstanceID());
}
if (glowImage != null)
{
Tween.Stop(glowImage.GetInstanceID());
// Fade out the glow
Tween.LocalScale(glowImage, Vector3.zero, disappearDuration * 0.5f, 0f, Tween.EaseInBack);
}
// Start disappear animation
StartCoroutine(DisappearSequence());
}
private IEnumerator DisappearSequence()
{
if (boosterImage == null)
{
yield break;
}
// Show scrapbook button temporarily using HUD visibility context
PlayerHudManager.HudVisibilityContext hudContext = null;
GameObject scrapbookButton = null;
scrapbookButton = PlayerHudManager.Instance.GetScrabookButton();
if (scrapbookButton != null)
{
hudContext = PlayerHudManager.Instance.ShowElementTemporarily(scrapbookButton);
}
else
{
Logging.Warning("[MinigameBoosterGiver] Scrapbook button not found in PlayerHudManager.");
}
// Calculate target position - use scrapbook button position if available
Vector3 targetPosition;
if (scrapbookButton != null)
{
// Get the scrapbook button's position in the same coordinate space as boosterImage
RectTransform scrapbookRect = scrapbookButton.GetComponent<RectTransform>();
if (scrapbookRect != null)
{
// Convert scrapbook button's world position to local position relative to boosterImage's parent
Canvas canvas = GetComponentInParent<Canvas>();
if (canvas != null && canvas.renderMode == RenderMode.ScreenSpaceOverlay)
{
// For overlay canvas, convert screen position to local position
Vector2 screenPos = RectTransformUtility.WorldToScreenPoint(null, scrapbookRect.position);
RectTransformUtility.ScreenPointToLocalPointInRectangle(
boosterImage.parent as RectTransform,
screenPos,
null,
out Vector2 localPoint);
targetPosition = localPoint;
}
else
{
// For world space or camera canvas
targetPosition = boosterImage.parent.InverseTransformPoint(scrapbookRect.position);
}
}
else
{
Logging.Warning("[MinigameBoosterGiver] Scrapbook button has no RectTransform, using fallback position.");
targetPosition = GetFallbackPosition();
}
}
else
{
// Fallback to bottom-left corner
targetPosition = GetFallbackPosition();
}
// Tween to scrapbook button position
Tween.LocalPosition(boosterImage, targetPosition, disappearDuration, 0f, Tween.EaseInBack);
// Scale down
Vector3 targetScale = _boosterInitialScale * disappearScale;
Tween.LocalScale(boosterImage, targetScale, disappearDuration, 0f, Tween.EaseInBack);
// Wait for animation to complete
yield return new WaitForSeconds(disappearDuration);
// Grant the booster pack
if (CardSystemManager.Instance != null)
{
CardSystemManager.Instance.AddBoosterPack(1);
Logging.Debug("[MinigameBoosterGiver] Booster pack granted!");
}
else
{
Logging.Warning("[MinigameBoosterGiver] CardSystemManager not found, cannot grant booster pack.");
}
// Hide scrapbook button by disposing the context
hudContext?.Dispose();
// Hide the visual
if (visualContainer != null)
{
visualContainer.SetActive(false);
}
// Invoke completion callback
_onCompleteCallback?.Invoke();
_onCompleteCallback = null;
// Clear sequence reference
_currentSequence = null;
}
private Vector3 GetFallbackPosition()
{
RectTransform canvasRect = GetComponentInParent<Canvas>()?.GetComponent<RectTransform>();
if (canvasRect != null)
{
// Convert bottom-left corner with offset to local position
Vector2 bottomLeft = new Vector2(-canvasRect.rect.width / 2f, -canvasRect.rect.height / 2f);
return bottomLeft + targetBottomLeftOffset;
}
else
{
// Ultimate fallback if no canvas found
return _boosterInitialPosition + new Vector3(-500f, -500f, 0f);
}
}
}
}

View File

@@ -1,12 +0,0 @@
fileFormatVersion: 2
guid: 7d8f3e9a2b4c5f6d1a8e9c0b3d4f5a6b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,71 +0,0 @@
 using System.Collections.Generic;
using Core;
using UI.DragAndDrop.Core;
using UnityEngine;
namespace UI.CardSystem
{
/// <summary>
/// Helper utility for shuffling draggable objects in a SlotContainer.
/// Moves objects to occupy the first available slots (0, 1, 2, etc.)
/// </summary>
public static class SlotContainerHelper
{
/// <summary>
/// Shuffles draggable objects to always occupy the first available slots.
/// Unassigns all objects from their current slots, then reassigns them starting from slot 0.
/// </summary>
/// <param name="container">The slot container holding the slots</param>
/// <param name="objects">List of draggable objects to shuffle</param>
/// <param name="animate">Whether to animate the movement</param>
public static void ShuffleToFront(SlotContainer container, List<DraggableObject> objects, bool animate = true)
{
if (container == null || objects == null || objects.Count == 0)
return;
Logging.Debug($"[SlotContainerHelper] Shuffling {objects.Count} objects to front slots");
// Unassign all objects from their current slots
foreach (var obj in objects)
{
if (obj.CurrentSlot != null)
{
obj.CurrentSlot.Vacate();
}
}
// Reassign objects to first N slots starting from slot 0
for (int i = 0; i < objects.Count; i++)
{
DraggableSlot targetSlot = FindSlotByIndex(container, i);
DraggableObject obj = objects[i];
if (targetSlot != null)
{
Logging.Debug($"[SlotContainerHelper] Assigning object to slot with SlotIndex {i}");
obj.AssignToSlot(targetSlot, animate);
}
else
{
Logging.Warning($"[SlotContainerHelper] Could not find slot with SlotIndex {i}");
}
}
}
/// <summary>
/// Find a slot by its SlotIndex property (not list position)
/// </summary>
private static DraggableSlot FindSlotByIndex(SlotContainer container, int slotIndex)
{
foreach (var slot in container.Slots)
{
if (slot.SlotIndex == slotIndex)
{
return slot;
}
}
return null;
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: cad44f85ab1a4672ab4bb14e2f919413
timeCreated: 1762470959

View File

@@ -243,6 +243,10 @@ namespace UI.DragAndDrop.Core
public virtual void OnDrag(PointerEventData eventData)
{
// Just ship all this shit when disabled
if (!_isDraggingEnabled)
return;
if (!_isDragging)
return;
@@ -269,6 +273,9 @@ namespace UI.DragAndDrop.Core
public virtual void OnEndDrag(PointerEventData eventData)
{
if (!_isDraggingEnabled)
return;
if (!_isDragging)
return;
@@ -282,14 +289,7 @@ namespace UI.DragAndDrop.Core
if (_canvasGroup != null)
_canvasGroup.blocksRaycasts = true;
// Find closest slot and snap
FindAndSnapToSlot();
// Snap base rotation back to slot rotation (if in a slot)
if (_currentSlot != null)
{
Tween.Rotation(transform, _currentSlot.transform.rotation, 0.3f, 0f, Tween.EaseOutBack);
}
// No auto-slotting - derived classes handle placement logic via OnDragEndedHook()
OnDragEnded?.Invoke(this);
OnDragEndedHook();
@@ -344,70 +344,8 @@ namespace UI.DragAndDrop.Core
#region Slot Management
protected virtual void FindAndSnapToSlot()
{
SlotContainer[] containers = FindObjectsByType<SlotContainer>(FindObjectsSortMode.None);
DraggableSlot closestSlot = null;
float closestDistance = float.MaxValue;
// Use RectTransform.position for overlay, transform.position for others
Vector3 myPosition = (_canvas != null && _canvas.renderMode == RenderMode.ScreenSpaceOverlay && RectTransform != null)
? RectTransform.position
: transform.position;
foreach (var container in containers)
{
DraggableSlot slot = container.FindClosestSlot(myPosition, this);
if (slot != null)
{
Vector3 slotPosition = slot.RectTransform != null ? slot.RectTransform.position : slot.transform.position;
float distance = Vector3.Distance(myPosition, slotPosition);
if (distance < closestDistance)
{
closestDistance = distance;
closestSlot = slot;
}
}
}
if (closestSlot != null)
{
// Check if slot is occupied
if (closestSlot.IsOccupied && closestSlot.Occupant != this)
{
// Swap with occupant
SwapWithSlot(closestSlot);
}
else
{
// Move to empty slot
AssignToSlot(closestSlot, true);
}
}
else if (_currentSlot != null)
{
// Return to current slot if no valid slot found
SnapToCurrentSlot();
}
}
protected virtual void SwapWithSlot(DraggableSlot targetSlot)
{
DraggableSlot mySlot = _currentSlot;
DraggableObject otherObject = targetSlot.Occupant;
if (otherObject != null)
{
// Both objects swap slots
targetSlot.Vacate();
if (mySlot != null)
mySlot.Vacate();
AssignToSlot(targetSlot, true);
if (mySlot != null)
otherObject.AssignToSlot(mySlot, true);
}
}
// Auto-slotting removed - derived classes (Card, etc.) handle placement via state machines
// AssignToSlot() and SnapToSlot() kept for explicit slot assignment
public virtual void AssignToSlot(DraggableSlot slot, bool animate)
{
@@ -461,14 +399,6 @@ namespace UI.DragAndDrop.Core
}
}
protected virtual void SnapToCurrentSlot()
{
if (_currentSlot != null)
{
SnapToSlot(_currentSlot);
}
}
#endregion
#region Selection