- **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
301 lines
11 KiB
C#
301 lines
11 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|