Refactor, cleanup code and add documentaiton

This commit is contained in:
Michal Pikulski
2025-11-18 09:29:59 +01:00
parent 034654c308
commit cf13fb78b5
100 changed files with 689 additions and 537 deletions

View File

@@ -0,0 +1,220 @@
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

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7cb791415c884e97ac181816424200e4
timeCreated: 1763454497

View File

@@ -0,0 +1,135 @@
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

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

View File

@@ -0,0 +1,219 @@
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

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

View File

@@ -0,0 +1,92 @@
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

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

View File

@@ -0,0 +1,300 @@
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

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c548995da9c746d1916b79304734c1c9
timeCreated: 1763454486

View File

@@ -0,0 +1,491 @@
using System.Collections.Generic;
using AppleHills.Data.CardSystem;
using Core;
using Data.CardSystem;
using Pixelplacement;
using UI.Core;
using UI.DragAndDrop.Core;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Serialization;
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;
[FormerlySerializedAs("albumCardPlacementPrefab")]
[SerializeField] private GameObject cardPrefab; // New Card prefab for placement
[Header("Card Enlarge System")]
[SerializeField] private GameObject cardEnlargedBackdrop; // Backdrop to block interactions
[SerializeField] private Transform cardEnlargedContainer; // Container for enlarged cards (sits above backdrop)
[Header("Booster Pack UI")]
[SerializeField] private GameObject[] boosterPackButtons;
[SerializeField] private BoosterOpeningPage boosterOpeningPage;
private Input.InputMode _previousInputMode;
// Controllers: Lazy-initialized services (auto-created on first use)
private CornerCardManager _cornerCardManager;
private CornerCardManager CornerCards => _cornerCardManager ??= new CornerCardManager(
bottomRightSlots,
cardPrefab,
this
);
private AlbumNavigationService _navigationService;
private AlbumNavigationService Navigation => _navigationService ??= new AlbumNavigationService(
book,
_zoneTabs
);
private CardEnlargeController _enlargeController;
private CardEnlargeController Enlarge => _enlargeController ??= new CardEnlargeController(
cardEnlargedBackdrop,
cardEnlargedContainer
);
/// <summary>
/// Query method: Check if the book is currently flipping to a page.
/// Used by card states to know if they should wait before placing.
/// </summary>
public bool IsPageFlipping => Navigation.IsPageFlipping;
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;
Button button = boosterPackButtons[i].GetComponent<Button>();
if (button != null)
{
button.onClick.AddListener(OnBoosterButtonClicked);
}
}
}
internal override void OnManagedDestroy()
{
// Unsubscribe from book events
if (book != null)
{
book.OnFlip.RemoveListener(OnPageFlipped);
}
// Unsubscribe from CardSystemManager
if (CardSystemManager.Instance != null)
{
CardSystemManager.Instance.OnBoosterCountChanged -= OnBoosterCountChanged;
}
// 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 pending corner cards
CleanupPendingCornerCards();
}
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
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");
}
// 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");
SpawnPendingCornerCards();
}
else
{
Logging.Debug("[AlbumViewPage] Opening to menu page - cards will spawn when entering album");
}
base.TransitionIn();
}
public override void TransitionOut()
{
// Clean up active pending cards to prevent duplicates on next opening
CleanupPendingCornerCards();
// 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()
{
Enlarge.CleanupEnlargedState();
}
/// <summary>
/// Check if we're currently viewing the album proper (not the menu page)
/// </summary>
private bool IsInAlbumProper()
{
return Navigation.IsInAlbumProper();
}
/// <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 && CornerCards.PendingCards.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");
SpawnPendingCornerCards();
}
else if (!isInAlbum && CornerCards.PendingCards.Count > 0)
{
// Returning to menu page - cleanup cards
Logging.Debug("[AlbumViewPage] Returning to menu page - cleaning up pending cards");
CleanupPendingCornerCards();
}
else
{
Logging.Debug($"[AlbumViewPage] Page flipped but no card state change needed (already in correct state)");
}
}
#region Card Enlarge System (Album Slots)
/// <summary>
/// Subscribe to a placed card's enlarged state events to manage backdrop and reparenting.
/// Called by AlbumCardSlot when it spawns an owned card in a slot.
/// </summary>
public void RegisterCardInAlbum(StateMachine.Card card)
{
Enlarge.RegisterCard(card);
}
public void UnregisterCardInAlbum(StateMachine.Card card)
{
Enlarge.UnregisterCard(card);
}
#endregion
/// <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;
}
public void SpawnPendingCornerCards()
{
CornerCards.SpawnCards();
}
private void CleanupPendingCornerCards()
{
CornerCards.CleanupAllCards();
}
#region Query Methods for Card States (Data Providers)
/// <summary>
/// Query method: Get card data for a pending slot.
/// Called by PendingFaceDownState when drag starts.
/// IMPORTANT: This removes the card from pending list immediately, then rebuilds corner.
/// </summary>
public CardData GetCardForPendingSlot()
{
return CornerCards.GetSmartSelection();
}
/// <summary>
/// Query method: Get target slot for a card.
/// Called by PendingFaceDownState to find where card should go.
/// </summary>
public AlbumCardSlot GetTargetSlotForCard(CardData cardData)
{
if (cardData == null) return null;
var allSlots = FindObjectsByType<AlbumCardSlot>(FindObjectsSortMode.None);
foreach (var slot in allSlots)
{
if (slot.TargetCardDefinition != null &&
slot.TargetCardDefinition.Id == cardData.DefinitionId)
{
return slot;
}
}
return null;
}
/// <summary>
/// Service method: Navigate to the page for a specific card.
/// Called by PendingFaceDownState to flip book to correct zone page.
/// </summary>
public void NavigateToCardPage(CardData cardData, System.Action onComplete)
{
Navigation.NavigateToCardPage(cardData, onComplete);
}
/// <summary>
/// Notify that a card has been placed (for cleanup).
/// Called by PlacedInSlotState after placement is complete.
/// </summary>
public void NotifyCardPlaced(StateMachine.Card card)
{
// Delegate to corner card manager for tracking removal
CornerCards.NotifyCardPlaced(card);
// Register for enlarge/shrink functionality
RegisterCardInAlbum(card);
}
#endregion
#region Helper Methods
public List<string> GetDefinitionsOnCurrentPage()
{
return Navigation.GetDefinitionsOnCurrentPage();
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,817 @@
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.Serialization;
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;
[FormerlySerializedAs("flippableCardPrefab")]
[SerializeField] private GameObject cardPrefab; // New Card prefab using state machine
[SerializeField] private float cardSpacing = 150f;
[SerializeField] private float cardWidth = 400f;
[SerializeField] private float cardHeight = 540f;
[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 List<StateMachine.Card> _currentCards = new List<StateMachine.Card>();
private CardData[] _currentCardData;
private StateMachine.Card _activeCard; // Currently selected/revealing card
private int _cardsCompletedInteraction; // Track how many cards finished their reveal flow
private bool _isProcessingOpening;
private const int MAX_VISIBLE_BOOSTERS = 3;
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 cards using new Card prefab
SpawnBoosterCards(_currentCardData);
// 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)
{
// See earlier comment for timing
Logging.Debug("[BoosterOpeningPage] Last booster opened, auto-transitioning to album main page");
if (UIPageController.Instance != null)
{
UIPageController.Instance.PopPage();
}
}
}
_isProcessingOpening = false;
}
/// <summary>
/// Spawn cards for booster opening flow using the new Card prefab and state machine.
/// </summary>
private void SpawnBoosterCards(CardData[] cards)
{
if (cardPrefab == null || cardDisplayContainer == null)
{
Logging.Warning("BoosterOpeningPage: Missing card prefab or container!");
return;
}
_currentRevealedCards.Clear();
_currentCards.Clear();
_cardsCompletedInteraction = 0;
_activeCard = null;
int count = cards.Length;
float totalWidth = (count - 1) * cardSpacing;
float startX = -totalWidth / 2f;
for (int i = 0; i < count; i++)
{
GameObject cardObj = Instantiate(cardPrefab, cardDisplayContainer);
RectTransform cardRect = cardObj.GetComponent<RectTransform>();
if (cardRect != null)
{
cardRect.anchoredPosition = new Vector2(startX + (i * cardSpacing), 0);
cardRect.sizeDelta = new Vector2(cardWidth, cardHeight); // Set card size
cardRect.localScale = Vector3.zero; // for pop-in
}
var card = cardObj.GetComponent<StateMachine.Card>();
var context = cardObj.GetComponent<StateMachine.CardContext>();
if (card != null && context != null)
{
// Setup card for booster reveal
// States will query CardSystemManager for current collection state as needed
context.SetupCard(cards[i]);
card.SetupForBoosterReveal(cards[i], false); // isNew parameter not used anymore
card.SetDraggingEnabled(false);
// Subscribe to CardDisplay click for selection
context.CardDisplay.OnCardClicked += (_) => OnCardClicked(card);
// Subscribe to reveal flow complete event from booster context
context.BoosterContext.OnRevealFlowComplete += () => OnCardRevealComplete(card);
// Track the card
_currentCards.Add(card);
// Tween in
Tween.LocalScale(cardObj.transform, Vector3.one, 0.3f, i * 0.1f, Tween.EaseOutBack);
}
else
{
Logging.Warning($"[BoosterOpeningPage] Card component or context missing on spawned card {i}!");
}
_currentRevealedCards.Add(cardObj);
}
}
/// <summary>
/// Handle when a card is clicked - start reveal flow if conditions are met
/// </summary>
private void OnCardClicked(StateMachine.Card card)
{
// Only allow clicking idle cards when no other card is active
if (_activeCard == null && card.IsIdle && card.Context.IsClickable)
{
Logging.Debug($"[BoosterOpeningPage] Card {card.CardData?.Name} selected for reveal");
// Set as active and disable all other idle cards
_activeCard = card;
foreach (var otherCard in _currentCards)
{
if (otherCard != card && otherCard.IsIdle)
{
otherCard.Context.IsClickable = false;
}
}
// Click will route to IdleState automatically and trigger flip
}
}
/// <summary>
/// Handle when a card completes its reveal flow
/// </summary>
private void OnCardRevealComplete(StateMachine.Card card)
{
_cardsCompletedInteraction++;
Logging.Debug($"[BoosterOpeningPage] Card {card.CardData?.Name} reveal complete ({_cardsCompletedInteraction}/{_currentCardData.Length})");
// Add card to inventory NOW (after player saw it)
if (card.CardData != null)
{
Data.CardSystem.CardSystemManager.Instance.AddCardToInventoryDelayed(card.CardData);
}
// Clear active card and re-enable remaining idle cards
if (_activeCard == card)
{
_activeCard = null;
foreach (var otherCard in _currentCards)
{
if (otherCard.IsIdle)
{
otherCard.Context.IsClickable = true;
}
}
}
}
/// <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>
/// Wait until all cards complete their reveal flow
/// </summary>
private IEnumerator WaitForCardReveals()
{
// Wait until all cards have completed their reveal flow
while (_cardsCompletedInteraction < _currentCardData.Length)
{
yield return null;
}
Logging.Debug($"[BoosterOpeningPage] All cards revealed! Animating cards to album...");
// Small pause
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)
{
float delay = cardIndex * 0.5f;
// Use world space position tween for root transform
Tween.Position(cardObj.transform, targetPosition, 0.5f, delay, Tween.EaseInBack);
Tween.LocalScale(cardObj.transform, Vector3.zero, 0.5f, delay, Tween.EaseInBack,
completeCallback: () => { if (cardObj != null) Destroy(cardObj); });
cardIndex++;
}
}
float totalAnimationTime = _currentCardData.Length * 0.5f;
_currentRevealedCards.Clear();
_currentCards.Clear();
yield return new WaitForSeconds(totalAnimationTime);
}
/// <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

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