using System.Collections; using System.Collections.Generic; using System.Linq; using AppleHills.Data.CardSystem; using Data.CardSystem; using Pixelplacement; using UI.Core; using UI.CardSystem.DragDrop; using UI.DragAndDrop.Core; using Unity.Cinemachine; using UnityEngine; using UnityEngine.UI; namespace UI.CardSystem { /// /// UI page for opening booster packs and displaying the cards received. /// Manages the entire booster opening flow with drag-and-drop interaction. /// public class BoosterOpeningPage : UIPage { [Header("UI References")] [SerializeField] private CanvasGroup canvasGroup; [Header("Booster Management")] [SerializeField] private GameObject boosterPackPrefab; // Prefab to instantiate new boosters [SerializeField] private SlotContainer bottomRightSlots; // Holds waiting boosters (max 3) [SerializeField] private DraggableSlot centerOpeningSlot; // Where booster goes to open [Header("Card Display")] [SerializeField] private Transform cardDisplayContainer; [SerializeField] private GameObject flippableCardPrefab; // Placeholder for card backs [SerializeField] private float cardSpacing = 150f; [Header("Settings")] [SerializeField] private float cardRevealDelay = 0.5f; [SerializeField] private float boosterDisappearDuration = 0.5f; [SerializeField] private CinemachineImpulseSource impulseSource; [SerializeField] private ParticleSystem openingParticleSystem; [SerializeField] private Transform albumIcon; // Target for card fly-away animation private int _availableBoosterCount; private BoosterPackDraggable _currentBoosterInCenter; private List _activeBoostersInSlots = new List(); private List _currentRevealedCards = new List(); private CardData[] _currentCardData; private int _revealedCardCount; private int _cardsCompletedInteraction; // Track how many cards finished their new/repeat interaction private bool _isProcessingOpening; private const int MAX_VISIBLE_BOOSTERS = 3; private FlippableCard _currentActiveCard; // The card currently awaiting interaction private void Awake() { // Make sure we have a CanvasGroup for transitions if (canvasGroup == null) canvasGroup = GetComponent(); if (canvasGroup == null) canvasGroup = gameObject.AddComponent(); // UI pages should start disabled gameObject.SetActive(false); } private void OnDestroy() { // Unsubscribe from slot events if (centerOpeningSlot != null) { centerOpeningSlot.OnOccupied -= OnBoosterPlacedInCenter; centerOpeningSlot.OnVacated -= OnBoosterRemovedFromCenter; } // Unsubscribe from booster events UnsubscribeFromAllBoosters(); } /// /// Set the number of available booster packs before showing the page /// public void SetAvailableBoosterCount(int count) { _availableBoosterCount = count; } public override void TransitionIn() { base.TransitionIn(); InitializeBoosterDisplay(); } public override void TransitionOut() { CleanupPage(); base.TransitionOut(); } /// /// Initialize the booster pack display based on available count /// private void InitializeBoosterDisplay() { Debug.Log($"[BoosterOpeningPage] InitializeBoosterDisplay called with {_availableBoosterCount} boosters available"); if (boosterPackPrefab == null) { Debug.LogWarning("BoosterOpeningPage: No booster pack prefab assigned!"); return; } if (bottomRightSlots == null || bottomRightSlots.SlotCount == 0) { Debug.LogWarning("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); Debug.Log($"[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; Debug.Log($"[BoosterOpeningPage] Subscribed to center slot events"); } else { Debug.LogWarning("[BoosterOpeningPage] centerOpeningSlot is null!"); } } /// /// Spawn a new booster and place it in the specified slot index /// private void SpawnBoosterInSlot(int slotIndex) { DraggableSlot slot = FindSlotByIndex(slotIndex); if (slot == null) { Debug.LogWarning($"[BoosterOpeningPage] Could not find slot with SlotIndex {slotIndex}!"); return; } // Instantiate booster GameObject boosterObj = Instantiate(boosterPackPrefab, slot.transform); BoosterPackDraggable booster = boosterObj.GetComponent(); 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); Debug.Log($"[BoosterOpeningPage] Spawned booster in slot with SlotIndex {slotIndex}"); } else { Debug.LogWarning($"[BoosterOpeningPage] Spawned booster has no BoosterPackDraggable component!"); Destroy(boosterObj); } } /// /// Remove and destroy the booster from the specified slot /// 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(); } Debug.Log($"[BoosterOpeningPage] Removed booster from slot {slotIndex}"); } _activeBoostersInSlots.RemoveAt(slotIndex); } /// /// Update visible boosters based on available count /// 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); } Debug.Log($"[BoosterOpeningPage] Updated visible boosters: {_activeBoostersInSlots.Count}/{targetCount}"); } /// /// Shuffle boosters so they always occupy the first available slots /// private void ShuffleBoostersToFront() { if (_activeBoostersInSlots.Count == 0) return; Debug.Log($"[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) { Debug.Log($"[BoosterOpeningPage] Assigning booster to slot with SlotIndex {i} {targetSlot.name}"); booster.AssignToSlot(targetSlot, true); // Animate the move } else { Debug.LogWarning($"[BoosterOpeningPage] Could not find slot with SlotIndex {i} {targetSlot.name}"); } } } /// /// Find a slot by its SlotIndex property (not list position) /// private DraggableSlot FindSlotByIndex(int slotIndex) { foreach (var slot in bottomRightSlots.Slots) { if (slot.SlotIndex == slotIndex) { return slot; } } return null; } /// /// 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) /// 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); Debug.Log($"[BoosterOpeningPage] Spawned new booster in slot with SlotIndex {i}"); break; } } } /// /// Handle when a booster is placed in the center opening slot /// private void OnBoosterPlacedInCenter(DraggableObject draggable) { BoosterPackDraggable booster = draggable as BoosterPackDraggable; if (booster == null) return; _currentBoosterInCenter = booster; // Remove from active slots list _activeBoostersInSlots.Remove(booster); // Lock the slot so it can't be dragged out Debug.Log($"[BoosterOpeningPage] Locking center slot. IsLocked before: {centerOpeningSlot.IsLocked}"); centerOpeningSlot.SetLocked(true); Debug.Log($"[BoosterOpeningPage] IsLocked after: {centerOpeningSlot.IsLocked}"); // Configure booster for opening (disables drag, enables tapping, resets tap count) Debug.Log($"[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(); Debug.Log($"[BoosterOpeningPage] Booster placed in center, ready for taps. Active boosters in slots: {_activeBoostersInSlots.Count}"); } /// /// Handle when a booster is removed from the center opening slot /// 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; Debug.Log($"[BoosterOpeningPage] Booster removed from center"); } private void OnBoosterTapped(BoosterPackDraggable booster, int currentTaps, int maxTaps) { Debug.Log($"[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); } } /// /// Handle when booster is opened - play particle effects /// private void OnBoosterOpened(BoosterPackDraggable booster) { Debug.Log($"[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(); } } /// /// Handle tap-to-place: When player taps a booster in bottom slots, move it to center /// public void OnBoosterTappedInBottomSlot(BoosterPackDraggable booster) { if (_currentBoosterInCenter != null || centerOpeningSlot == null) return; // Center slot already occupied // Move booster to center slot booster.AssignToSlot(centerOpeningSlot, true); } /// /// Handle when booster is ready to open (after max taps) /// private void OnBoosterReadyToOpen(BoosterPackDraggable booster) { if (_isProcessingOpening) return; Debug.Log($"[BoosterOpeningPage] Booster ready to open!"); // Trigger the actual opening sequence booster.TriggerOpen(); StartCoroutine(ProcessBoosterOpening(booster)); } /// /// Process the booster opening sequence /// private IEnumerator ProcessBoosterOpening(BoosterPackDraggable booster) { _isProcessingOpening = true; // Call CardSystemManager to open the pack if (CardSystemManager.Instance != null) { List revealedCardsList = CardSystemManager.Instance.OpenBoosterPack(); _currentCardData = revealedCardsList.ToArray(); // Animate booster disappearing yield return StartCoroutine(AnimateBoosterDisappear(booster)); // Decrement available count _availableBoosterCount--; // Update visible boosters (remove from end if we drop below thresholds) UpdateVisibleBoosters(); // Show card backs SpawnCardBacks(_currentCardData.Length); // Wait for player to reveal all cards bool isLastBooster = _availableBoosterCount <= 0; yield return StartCoroutine(WaitForCardReveals()); // Check if this was the last booster pack if (isLastBooster) { // Wait for all card animations to complete before transitioning // WaitForCardReveals already includes: 0.5s wait + (cardCount * 0.5s stagger) + 0.5s animation + 0.5s final // Total is: 1.5s + (cardCount * 0.5s) // For 5 cards that's 4 seconds total, which should be enough Debug.Log("[BoosterOpeningPage] Last booster opened, auto-transitioning to album main page"); if (UIPageController.Instance != null) { UIPageController.Instance.PopPage(); } } } _isProcessingOpening = false; } /// /// Animate the booster pack disappearing /// 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(); 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); } /// /// Spawn card back placeholders for revealing /// private void SpawnCardBacks(int count) { if (flippableCardPrefab == null || cardDisplayContainer == null) { Debug.LogWarning("BoosterOpeningPage: Missing card prefab or container!"); return; } _currentRevealedCards.Clear(); _revealedCardCount = 0; _cardsCompletedInteraction = 0; // Reset interaction count // Calculate positions float totalWidth = (count - 1) * cardSpacing; float startX = -totalWidth / 2f; for (int i = 0; i < count; i++) { GameObject cardObj = Instantiate(flippableCardPrefab, cardDisplayContainer); RectTransform cardRect = cardObj.GetComponent(); if (cardRect != null) { cardRect.anchoredPosition = new Vector2(startX + (i * cardSpacing), 0); } // Get FlippableCard component and setup the card data FlippableCard flippableCard = cardObj.GetComponent(); if (flippableCard != null) { // Setup the card data (stored but not revealed yet) flippableCard.SetupCard(_currentCardData[i]); // Subscribe to flip started event (to disable other cards IMMEDIATELY) int cardIndex = i; // Capture for closure flippableCard.OnFlipStarted += OnCardFlipStarted; // Subscribe to reveal event to track when flipped flippableCard.OnCardRevealed += (card, data) => OnCardRevealed(cardIndex); // Subscribe to inactive click event (for jiggle effect) flippableCard.OnClickedWhileInactive += OnCardClickedWhileInactive; // Initially, all cards are clickable (for flipping) flippableCard.SetClickable(true); } else { Debug.LogWarning($"[BoosterOpeningPage] FlippableCard component not found on card {i}!"); } _currentRevealedCards.Add(cardObj); // Animate cards flying in cardRect.localScale = Vector3.zero; Tween.LocalScale(cardRect, Vector3.one, 0.3f, i * 0.1f, Tween.EaseOutBack); } } /// /// Handle when a card flip starts (disable all other cards IMMEDIATELY) /// private void OnCardFlipStarted(FlippableCard flippingCard) { Debug.Log($"[BoosterOpeningPage] Card flip started, disabling all other cards."); // Disable ALL cards immediately to prevent multi-flip foreach (GameObject cardObj in _currentRevealedCards) { FlippableCard card = cardObj.GetComponent(); if (card != null) { card.SetClickable(false); } } } /// /// Handle card reveal (when flipped) /// private void OnCardRevealed(int cardIndex) { Debug.Log($"[BoosterOpeningPage] Card {cardIndex} revealed!"); _revealedCardCount++; // Get the flippable card and card data FlippableCard flippableCard = _currentRevealedCards[cardIndex].GetComponent(); if (flippableCard == null) { Debug.LogWarning($"[BoosterOpeningPage] FlippableCard not found for card {cardIndex}!"); return; } CardData cardData = flippableCard.CardData; // Check if this is a new card using CardSystemManager bool isNew = Data.CardSystem.CardSystemManager.Instance.IsCardNew(cardData, out CardData existingCard); if (isNew) { Debug.Log($"[BoosterOpeningPage] Card '{cardData.Name}' is NEW!"); flippableCard.ShowAsNew(); } else { // Check if card is already Legendary - if so, skip progress bar and auto-progress if (existingCard.Rarity == AppleHills.Data.CardSystem.CardRarity.Legendary) { Debug.Log($"[BoosterOpeningPage] Card '{cardData.Name}' is LEGENDARY - auto-progressing!"); // Add to inventory immediately and move to next card Data.CardSystem.CardSystemManager.Instance.AddCardToInventoryDelayed(cardData); _cardsCompletedInteraction++; _revealedCardCount++; // This was already incremented earlier, but we need to track completion EnableUnrevealedCards(); return; // Skip showing the card enlarged } int ownedCount = existingCard.CopiesOwned; Debug.Log($"[BoosterOpeningPage] Card '{cardData.Name}' is a REPEAT! Owned: {ownedCount}"); // Check if this card will trigger an upgrade (ownedCount + 1 >= threshold) bool willUpgrade = (ownedCount + 1) >= flippableCard.CardsToUpgrade && existingCard.Rarity < AppleHills.Data.CardSystem.CardRarity.Legendary; if (willUpgrade) { Debug.Log($"[BoosterOpeningPage] This card will trigger upgrade! ({ownedCount + 1}/{flippableCard.CardsToUpgrade})"); // Show as repeat - progress bar will fill and auto-trigger upgrade flippableCard.ShowAsRepeatWithUpgrade(ownedCount, existingCard); } else { // Normal repeat, no upgrade flippableCard.ShowAsRepeat(ownedCount); } } // Set this card as the active one (only this card is clickable now) SetActiveCard(flippableCard); // Subscribe to tap event to know when interaction is complete flippableCard.OnCardTappedAfterReveal += (card) => OnCardCompletedInteraction(card, cardIndex); } /// /// Handle when a card's interaction is complete (tapped after reveal) /// private void OnCardCompletedInteraction(FlippableCard card, int cardIndex) { Debug.Log($"[BoosterOpeningPage] Card {cardIndex} interaction complete!"); // Add card to inventory NOW (after player saw it) Data.CardSystem.CardSystemManager.Instance.AddCardToInventoryDelayed(card.CardData); // Return card to normal size card.ReturnToNormalSize(); // Increment completed interaction count _cardsCompletedInteraction++; // Clear active card _currentActiveCard = null; // Re-enable all unrevealed cards (they can be flipped now) EnableUnrevealedCards(); Debug.Log($"[BoosterOpeningPage] Cards completed interaction: {_cardsCompletedInteraction}/{_currentCardData.Length}"); } /// /// Set which card is currently active (only this card can be clicked) /// private void SetActiveCard(FlippableCard activeCard) { _currentActiveCard = activeCard; // Disable all other cards foreach (GameObject cardObj in _currentRevealedCards) { FlippableCard card = cardObj.GetComponent(); if (card != null) { // Only the active card is clickable card.SetClickable(card == activeCard); } } Debug.Log($"[BoosterOpeningPage] Set active card. Only one card is now clickable."); } /// /// Re-enable all unrevealed cards (allow them to be flipped) /// private void EnableUnrevealedCards() { foreach (GameObject cardObj in _currentRevealedCards) { FlippableCard card = cardObj.GetComponent(); if (card != null && !card.IsFlipped) { card.SetClickable(true); } } Debug.Log($"[BoosterOpeningPage] Re-enabled unrevealed cards for flipping."); } /// /// Handle when a card is clicked while not active (jiggle the active card) /// private void OnCardClickedWhileInactive(FlippableCard inactiveCard) { Debug.Log($"[BoosterOpeningPage] Inactive card clicked, jiggling active card."); if (_currentActiveCard != null) { _currentActiveCard.Jiggle(); } } /// /// Wait until all cards are revealed AND all interactions are complete /// private IEnumerator WaitForCardReveals() { // Wait until all cards are flipped while (_revealedCardCount < _currentCardData.Length) { yield return null; } Debug.Log($"[BoosterOpeningPage] All cards revealed! Waiting for interactions..."); // Wait until all cards have completed their new/repeat interaction while (_cardsCompletedInteraction < _currentCardData.Length) { yield return null; } Debug.Log($"[BoosterOpeningPage] All interactions complete! Animating cards to album..."); // All cards revealed and interacted with, wait a moment yield return new WaitForSeconds(0.5f); // Animate cards to album icon (or center if no icon assigned) with staggered delays Vector3 targetPosition = albumIcon != null ? albumIcon.position : Vector3.zero; int cardIndex = 0; foreach (GameObject cardObj in _currentRevealedCards) { if (cardObj != null) { // Stagger each card with 0.5s delay float delay = cardIndex * 0.5f; // Animate to album icon position, then destroy Tween.Position(cardObj.transform, targetPosition, 0.5f, delay, Tween.EaseInBack); Tween.LocalScale(cardObj.transform, Vector3.zero, 0.5f, delay, Tween.EaseInBack, completeCallback: () => Destroy(cardObj)); cardIndex++; } } // Wait for all animations to complete // Last card starts at: (cardCount - 1) * 0.5s delay // Last card finishes at: (cardCount - 1) * 0.5s + 0.5s animation duration = cardCount * 0.5s float totalAnimationTime = _currentCardData.Length * 0.5f; _currentRevealedCards.Clear(); yield return new WaitForSeconds(totalAnimationTime); } /// /// Clean up the page when hidden /// 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; } /// /// Unsubscribe from all booster events /// 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(); } } } }