Make a generic booster pack giver

This commit is contained in:
Michal Pikulski
2025-12-15 11:59:40 +01:00
parent e2b74b1ea5
commit bb332933ec
64 changed files with 1228 additions and 312 deletions

View File

@@ -9,10 +9,9 @@ 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.
/// UI component for granting booster packs from minigames.
/// Supports awarding 1 to N booster packs with visual animations.
/// Shows up to 3 booster packs visually. For more than 3, duplicates the first pack and tweens them sequentially.
/// </summary>
public class MinigameBoosterGiver : MonoBehaviour
{
@@ -20,8 +19,8 @@ namespace UI.CardSystem
[Header("Visual References")]
[SerializeField] private GameObject visualContainer;
[SerializeField] private RectTransform boosterImage;
[SerializeField] private RectTransform glowImage;
[SerializeField] private RectTransform[] boosterImages; // Up to 3 booster pack visuals
[SerializeField] private RectTransform glowImage; // Single glow effect for all boosters
[SerializeField] private Button continueButton;
[Header("Animation Settings")]
@@ -32,14 +31,16 @@ namespace UI.CardSystem
[Header("Disappear Animation")]
[SerializeField] private Vector2 targetBottomLeftOffset = new Vector2(100f, 100f);
[SerializeField] private float disappearDuration = 0.8f;
[SerializeField] private float tweenDuration = 0.8f;
[SerializeField] private float delayBetweenTweens = 0.2f;
[SerializeField] private float disappearScale = 0.2f;
private Vector3 _boosterInitialPosition;
private Vector3 _boosterInitialScale;
private Vector3[] _boosterInitialPositions;
private Vector3[] _boosterInitialScales;
private Vector3 _glowInitialScale;
private Coroutine _currentSequence;
private Action _onCompleteCallback;
private int _totalPackCount = 1;
private void Awake()
{
@@ -53,11 +54,20 @@ namespace UI.CardSystem
Instance = this;
// Cache initial values
if (boosterImage != null)
// Cache initial values for all booster images
if (boosterImages != null && boosterImages.Length > 0)
{
_boosterInitialPosition = boosterImage.localPosition;
_boosterInitialScale = boosterImage.localScale;
_boosterInitialPositions = new Vector3[boosterImages.Length];
_boosterInitialScales = new Vector3[boosterImages.Length];
for (int i = 0; i < boosterImages.Length; i++)
{
if (boosterImages[i] != null)
{
_boosterInitialPositions[i] = boosterImages[i].localPosition;
_boosterInitialScales[i] = boosterImages[i].localScale;
}
}
}
if (glowImage != null)
@@ -78,6 +88,30 @@ namespace UI.CardSystem
}
}
/// <summary>
/// Initialize the booster giver with a specific pack count.
/// Shows up to 3 booster visuals based on count.
/// </summary>
/// <param name="packCount">Number of booster packs to grant</param>
public void Initialize(int packCount)
{
_totalPackCount = Mathf.Max(1, packCount);
// Show up to 3 booster visuals
int visualCount = Mathf.Min(_totalPackCount, boosterImages.Length);
for (int i = 0; i < boosterImages.Length; i++)
{
bool shouldShow = i < visualCount;
if (boosterImages[i] != null)
{
boosterImages[i].gameObject.SetActive(shouldShow);
}
}
Logging.Debug($"[MinigameBoosterGiver] Initialized with {_totalPackCount} packs, showing {visualCount} visuals");
}
private void OnDestroy()
{
if (Instance == this)
@@ -115,13 +149,19 @@ namespace UI.CardSystem
visualContainer.SetActive(true);
}
// Reset positions and scales
if (boosterImage != null)
// Reset positions and scales for visible boosters
int visualCount = Mathf.Min(_totalPackCount, boosterImages.Length);
for (int i = 0; i < visualCount; i++)
{
boosterImage.localPosition = _boosterInitialPosition;
boosterImage.localScale = _boosterInitialScale;
if (boosterImages[i] != null)
{
boosterImages[i].localPosition = _boosterInitialPositions[i];
boosterImages[i].localScale = _boosterInitialScales[i];
}
}
// Reset glow scale
if (glowImage != null)
{
glowImage.localScale = _glowInitialScale;
@@ -133,14 +173,17 @@ namespace UI.CardSystem
continueButton.interactable = true;
}
// Start idle hovering animation on booster (ping-pong)
if (boosterImage != null)
// Start idle hovering animation on all visible boosters (ping-pong)
for (int i = 0; i < visualCount; i++)
{
Vector3 hoverTarget = _boosterInitialPosition + Vector3.up * hoverAmount;
Tween.LocalPosition(boosterImage, hoverTarget, hoverDuration, 0f, Tween.EaseLinear, Tween.LoopType.PingPong);
if (boosterImages[i] != null)
{
Vector3 hoverTarget = _boosterInitialPositions[i] + Vector3.up * hoverAmount;
Tween.LocalPosition(boosterImages[i], hoverTarget, hoverDuration, 0f, Tween.EaseLinear, Tween.LoopType.PingPong);
}
}
// Start pulsing animation on glow (ping-pong scale)
// Start pulsing animation on the single glow (ping-pong scale)
if (glowImage != null)
{
Vector3 glowPulseScale = _glowInitialScale * glowPulseMax;
@@ -158,41 +201,23 @@ namespace UI.CardSystem
return; // Not in a sequence
}
// Disable button to prevent double-clicks
// Disable and hide button to prevent double-clicks
if (continueButton != null)
{
continueButton.interactable = false;
continueButton.gameObject.SetActive(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());
// Start moving all boosters to backpack
StartCoroutine(MoveAllBoostersToBackpack());
}
private IEnumerator DisappearSequence()
private IEnumerator MoveAllBoostersToBackpack()
{
if (boosterImage == null)
{
yield break;
}
// Show scrapbook button temporarily using HUD visibility context
PlayerHudManager.HudVisibilityContext hudContext = null;
GameObject scrapbookButton = null;
GameObject scrapbookButton = PlayerHudManager.Instance?.GetScrabookButton();
scrapbookButton = PlayerHudManager.Instance.GetScrabookButton();
if (scrapbookButton != null)
{
hudContext = PlayerHudManager.Instance.ShowElementTemporarily(scrapbookButton);
@@ -202,75 +227,86 @@ namespace UI.CardSystem
Logging.Warning("[MinigameBoosterGiver] Scrapbook button not found in PlayerHudManager.");
}
// Calculate target position - use scrapbook button position if available
Vector3 targetPosition;
// Calculate target position once
Vector3 targetPosition = GetTargetPosition(scrapbookButton);
if (scrapbookButton != null)
int remaining = _totalPackCount;
int visualCount = Mathf.Min(_totalPackCount, boosterImages.Length);
// Stop and fade out the single glow immediately
if (glowImage != null)
{
// Get the scrapbook button's position in the same coordinate space as boosterImage
RectTransform scrapbookRect = scrapbookButton.GetComponent<RectTransform>();
if (scrapbookRect != null)
Tween.Stop(glowImage.GetInstanceID());
Tween.LocalScale(glowImage, Vector3.zero, tweenDuration * 0.5f, 0f, Tween.EaseInBack);
}
// Phase 1: Animate duplicates for packs above 3 (if any)
int duplicatesToAnimate = Mathf.Max(0, _totalPackCount - 3);
for (int i = 0; i < duplicatesToAnimate; i++)
{
// Spawn duplicate at the first booster's current position
if (boosterImages.Length > 0 && boosterImages[0] != 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);
}
StartCoroutine(TweenPackToBackpack(boosterImages[0], targetPosition, isDuplicate: true));
}
else
remaining--;
// Wait for stagger delay before next
if (i < duplicatesToAnimate - 1 || remaining > 0)
{
Logging.Warning("[MinigameBoosterGiver] Scrapbook button has no RectTransform, using fallback position.");
targetPosition = GetFallbackPosition();
yield return new WaitForSeconds(delayBetweenTweens);
}
}
else
// Phase 2: Animate the actual visible packs (up to 3)
for (int i = 0; i < visualCount && i < remaining; i++)
{
// Fallback to bottom-left corner
targetPosition = GetFallbackPosition();
if (boosterImages[i] != null)
{
// Stop hover animations first
Tween.Stop(boosterImages[i].GetInstanceID());
// Tween the actual booster
StartCoroutine(TweenPackToBackpack(boosterImages[i], targetPosition, isDuplicate: false));
}
// Wait for stagger delay before next (except after last one)
if (i < visualCount - 1 && i < remaining - 1)
{
yield return new WaitForSeconds(delayBetweenTweens);
}
}
// 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
// Wait for the last tween to complete
yield return new WaitForSeconds(tweenDuration);
// Grant all booster packs at once
if (CardSystemManager.Instance != null)
{
CardSystemManager.Instance.AddBoosterPack(1);
Logging.Debug("[MinigameBoosterGiver] Booster pack granted!");
CardSystemManager.Instance.AddBoosterPack(_totalPackCount);
Logging.Debug($"[MinigameBoosterGiver] Granted {_totalPackCount} booster pack(s)!");
}
else
{
Logging.Warning("[MinigameBoosterGiver] CardSystemManager not found, cannot grant booster pack.");
Logging.Warning("[MinigameBoosterGiver] CardSystemManager not found, cannot grant booster packs.");
}
// Hide scrapbook button by disposing the context
hudContext?.Dispose();
// Hide the visual
// Hide the visual container
if (visualContainer != null)
{
visualContainer.SetActive(false);
}
// Show button again for next use
if (continueButton != null)
{
continueButton.gameObject.SetActive(true);
}
// Invoke completion callback
_onCompleteCallback?.Invoke();
@@ -279,6 +315,92 @@ namespace UI.CardSystem
// Clear sequence reference
_currentSequence = null;
}
/// <summary>
/// Generic method to tween a booster pack to the backpack button.
/// Handles both duplicates (instantiated clones) and actual booster visuals.
/// </summary>
private IEnumerator TweenPackToBackpack(RectTransform sourceRect, Vector3 targetPosition, bool isDuplicate)
{
RectTransform packToAnimate;
if (isDuplicate)
{
// Create a clone at the source position
GameObject clone = Instantiate(sourceRect.gameObject, sourceRect.parent);
packToAnimate = clone.GetComponent<RectTransform>();
if (packToAnimate != null)
{
packToAnimate.localPosition = sourceRect.localPosition;
packToAnimate.localScale = sourceRect.localScale;
packToAnimate.gameObject.SetActive(true);
}
}
else
{
packToAnimate = sourceRect;
}
if (packToAnimate == null)
{
yield break;
}
// Tween to target position
Tween.LocalPosition(packToAnimate, targetPosition, tweenDuration, 0f, Tween.EaseInBack);
// Scale down
Vector3 targetScale = packToAnimate.localScale * disappearScale;
Tween.LocalScale(packToAnimate, targetScale, tweenDuration, 0f, Tween.EaseInBack);
// Don't wait here - let animations overlap
// Hide/destroy the booster after tween completes (duplicate or not)
// Since we destroy the entire UI instance anyway, just clean up visual artifacts
yield return new WaitForSeconds(tweenDuration);
if (packToAnimate != null)
{
packToAnimate.gameObject.SetActive(false);
}
yield break;
}
/// <summary>
/// Get the target position for the booster pack animation (scrapbook button or fallback).
/// </summary>
private Vector3 GetTargetPosition(GameObject scrapbookButton)
{
if (scrapbookButton != null && boosterImages.Length > 0 && boosterImages[0] != null)
{
RectTransform scrapbookRect = scrapbookButton.GetComponent<RectTransform>();
if (scrapbookRect != null)
{
// Convert scrapbook button's world position to local position relative to booster'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(
boosterImages[0].parent as RectTransform,
screenPos,
null,
out Vector2 localPoint);
return localPoint;
}
else
{
// For world space or camera canvas
return boosterImages[0].parent.InverseTransformPoint(scrapbookRect.position);
}
}
}
// Fallback position
return GetFallbackPosition();
}
private Vector3 GetFallbackPosition()
{
@@ -292,7 +414,11 @@ namespace UI.CardSystem
else
{
// Ultimate fallback if no canvas found
return _boosterInitialPosition + new Vector3(-500f, -500f, 0f);
if (boosterImages.Length > 0 && _boosterInitialPositions != null && _boosterInitialPositions.Length > 0)
{
return _boosterInitialPositions[0] + new Vector3(-500f, -500f, 0f);
}
return new Vector3(-500f, -500f, 0f);
}
}
}