- **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
221 lines
8.2 KiB
C#
221 lines
8.2 KiB
C#
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
|
|
}
|
|
}
|