Fix up the UI + game flow in minigame, should function correctly now

This commit is contained in:
Michal Pikulski
2025-10-24 13:01:19 +02:00
parent 1003c3f6ac
commit 04737ab01b
11 changed files with 919 additions and 1203 deletions

View File

@@ -0,0 +1,112 @@
using Pixelplacement;
using UnityEngine;
namespace UI
{
/// <summary>
/// Adds a CanvasGroup (if missing) and plays a continuous blinking tween on its alpha.
/// Attach to any GameObject to make it pulse/fade in and out.
/// </summary>
[DisallowMultipleComponent]
[RequireComponent(typeof(CanvasGroup))]
public class BlinkingCanvasGroup : MonoBehaviour
{
[Header("Blink Settings")] [Tooltip("Minimum alpha value during blink")] [Range(0f, 1f)]
public float minAlpha;
[Tooltip("Maximum alpha value during blink")] [Range(0f, 1f)]
public float maxAlpha = 1f;
[Tooltip("Duration of one leg (min->max or max->min)")]
public float legDuration = 0.5f;
[Tooltip("Delay before starting the blinking")]
public float startDelay;
[Tooltip("Whether the tween should obey Time.timeScale (false = unscaled)")]
public bool obeyTimescale;
[Tooltip("Optional animation curve for easing. Leave null for default ease in/out.")]
public AnimationCurve easeCurve;
// Internal
private CanvasGroup _canvasGroup;
private Pixelplacement.TweenSystem.TweenBase _activeTween;
void Awake()
{
// Ensure we have a CanvasGroup
_canvasGroup = GetComponent<CanvasGroup>();
if (_canvasGroup == null)
{
_canvasGroup = gameObject.AddComponent<CanvasGroup>();
}
// Ensure starting alpha
_canvasGroup.alpha = minAlpha;
}
void Start()
{
StartBlinking();
}
void OnEnable()
{
// If re-enabled, ensure tween is running
if (_activeTween == null)
StartBlinking();
}
void OnDisable()
{
StopBlinking();
}
void OnDestroy()
{
StopBlinking();
}
/// <summary>
/// Start the continuous blinking tween.
/// </summary>
public void StartBlinking()
{
StopBlinking();
if (_canvasGroup == null) return;
// Use PingPong-like behavior by chaining two opposite legs with LoopType.Loop
// Simpler: Tween.CanvasGroupAlpha has an overload that sets startAlpha then end.
// We'll tween min->max then set LoopType.PingPong if available. The API supports LoopType.PingPong.
// Start from minAlpha to maxAlpha
_canvasGroup.alpha = minAlpha;
// Use the Tween.Value overload for float with obeyTimescale parameter
_activeTween = Tween.Value(minAlpha, maxAlpha, v => _canvasGroup.alpha = v, legDuration, startDelay,
easeCurve ?? Tween.EaseInOut, Tween.LoopType.PingPong, null, null, obeyTimescale);
}
/// <summary>
/// Stops the blinking tween if running.
/// </summary>
public void StopBlinking()
{
if (_activeTween != null)
{
try
{
_activeTween.Cancel();
}
catch
{
// Some versions of the tween API may not expose Cancel; ignore safely.
}
_activeTween = null;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b75de64fb2fd4ff0b869bf469e2becea
timeCreated: 1761299308

View File

@@ -2,7 +2,6 @@
using Bootstrap;
using Core;
using Data.CardSystem;
using Input;
using Pixelplacement;
using UI.Core;
using UnityEngine;
@@ -33,8 +32,7 @@ namespace UI.CardSystem
public GameObject BackpackIcon => backpackIcon;
private UIPageController PageController => UIPageController.Instance;
private CardSystemManager _cardManager;
private bool _isInitialized = false;
private bool _hasUnseenCards = false;
private bool _hasUnseenCards;
private void Awake()
{
@@ -59,6 +57,13 @@ namespace UI.CardSystem
// Initialize pages and hide them
InitializePages();
// React to global UI hide/show events (top-page only) by toggling this GameObject
if (UIPageController.Instance != null)
{
UIPageController.Instance.OnAllUIHidden += HandleAllUIHidden;
UIPageController.Instance.OnAllUIShown += HandleAllUIShown;
}
}
private void Start()
@@ -77,8 +82,6 @@ namespace UI.CardSystem
// Initialize UI with current values
UpdateBoosterCount(_cardManager.GetBoosterPackCount());
}
_isInitialized = true;
}
private void OnDestroy()
@@ -103,6 +106,13 @@ namespace UI.CardSystem
{
PageController.OnPageChanged -= OnPageChanged;
}
// Unsubscribe from global UI hide/show events
if (UIPageController.Instance != null)
{
UIPageController.Instance.OnAllUIHidden -= HandleAllUIHidden;
UIPageController.Instance.OnAllUIShown -= HandleAllUIShown;
}
}
/// <summary>
@@ -168,6 +178,7 @@ namespace UI.CardSystem
if (newPage == null)
{
ShowOnlyBackpackIcon();
GameManager.Instance.ReleasePause(this);
}
else
{
@@ -221,8 +232,6 @@ namespace UI.CardSystem
boosterNotificationDot.gameObject.SetActive(hasBooters || _hasUnseenCards);
}
}
GameManager.Instance.ReleasePause(this);
}
/// <summary>
@@ -313,5 +322,19 @@ namespace UI.CardSystem
// Just log the upgrade event without showing a notification
Logging.Debug($"[CardAlbumUI] Card upgraded: {card.Name} to {card.Rarity}");
}
// Handlers for UI controller hide/show events — toggle this GameObject active state
private void HandleAllUIHidden()
{
// Ensure UI returns to backpack-only state before deactivating so we don't leave the game paused
ShowOnlyBackpackIcon();
backpackButton.gameObject.SetActive(false);
}
private void HandleAllUIShown()
{
backpackButton.gameObject.SetActive(true);
}
}
}

View File

@@ -23,6 +23,10 @@ namespace UI.Core
// Event fired when the page stack changes
public event Action<UIPage> OnPageChanged;
// Events fired when the controller hides or shows all UI pages
public event Action OnAllUIHidden;
public event Action OnAllUIShown;
private PlayerInput _playerInput;
private InputAction _cancelAction;
@@ -141,5 +145,39 @@ namespace UI.Core
OnPageChanged?.Invoke(null);
Logging.Debug("[UIPageController] Cleared page stack");
}
/// <summary>
/// Hide all currently stacked UI pages by calling TransitionOut on each.
/// This does not modify the page stack; it simply asks pages to transition out
/// so UI elements can be hidden. Subscribers can react to <see cref="OnAllUIHidden"/>.
/// </summary>
public void HideAllUI()
{
// Only transition out the top (active) page — maintains stack semantics.
var current = CurrentPage;
if (current != null)
{
current.TransitionOut();
}
OnAllUIHidden?.Invoke();
}
/// <summary>
/// Show all currently stacked UI pages by calling TransitionIn on each from bottom to top.
/// This does not modify the page stack; it asks pages to transition in so UI elements become visible.
/// Subscribers can react to <see cref="OnAllUIShown"/>.
/// </summary>
public void ShowAllUI()
{
// Only transition in the top (active) page — maintains stack semantics.
var current = CurrentPage;
if (current != null)
{
current.TransitionIn();
}
OnAllUIShown?.Invoke();
}
}
}

View File

@@ -2,7 +2,6 @@ using System;
using Core;
using UnityEngine;
using UnityEngine.SceneManagement;
using Input;
using Bootstrap;
using UI.Core;
using Pixelplacement;
@@ -47,6 +46,13 @@ namespace UI
// Subscribe to scene loaded events
SceneManagerService.Instance.SceneLoadCompleted += SetPauseMenuByLevel;
// Also react to global UI hide/show events from the page controller
if (UIPageController.Instance != null)
{
UIPageController.Instance.OnAllUIHidden += HandleAllUIHidden;
UIPageController.Instance.OnAllUIShown += HandleAllUIShown;
}
// SceneManagerService subscription moved to InitializePostBoot
// Set initial state based on current scene
@@ -62,6 +68,11 @@ namespace UI
{
SceneManagerService.Instance.SceneLoadCompleted -= SetPauseMenuByLevel;
}
if (UIPageController.Instance != null)
{
UIPageController.Instance.OnAllUIHidden -= HandleAllUIHidden;
UIPageController.Instance.OnAllUIShown -= HandleAllUIShown;
}
}
/// <summary>
@@ -132,7 +143,6 @@ namespace UI
pauseMenuPanel.SetActive(false);
if (pauseButton != null)
pauseButton.SetActive(true);
EndPauseSideEffects();
if (canvasGroup != null)
{
canvasGroup.alpha = 0f;
@@ -143,12 +153,18 @@ namespace UI
}
}
public void HidePauseMenuAndResumeGame()
{
HidePauseMenu();
EndPauseSideEffects();
}
/// <summary>
/// Resumes the game by hiding the pause menu.
/// </summary>
public void ResumeGame()
{
HidePauseMenu();
HidePauseMenuAndResumeGame();
}
private void BeginPauseSideEffects()
@@ -253,7 +269,7 @@ namespace UI
public async void LoadLevel(int levelSelection)
{
// Hide the pause menu before loading a new level
HidePauseMenu();
HidePauseMenuAndResumeGame();
// Replace with the actual scene name as set in Build Settings
var progress = new Progress<float>(p => Logging.Debug($"Loading progress: {p * 100:F0}%"));
@@ -270,5 +286,42 @@ namespace UI
break;
}
}
// Handlers for UI controller hide/show events — make these safe with respect to transitions and pause state
private void HandleAllUIHidden()
{
var parent = transform.parent?.gameObject ?? gameObject;
// If we're currently transitioning, wait for the transition out to complete before deactivating
if (_isTransitioning)
{
Action handler = null;
handler = () =>
{
OnTransitionOutCompleted -= handler;
parent.SetActive(false);
};
OnTransitionOutCompleted += handler;
return;
}
// If this page is visible, request a proper hide so transition/out side-effects run (e.g. releasing pause)
if (_isVisible)
{
HidePauseMenu();
return;
}
// Otherwise it's safe to immediately deactivate
parent.SetActive(false);
}
private void HandleAllUIShown()
{
var parent = transform.parent?.gameObject ?? gameObject;
// Just ensure the parent is active. Do not force pause or transitions here.
parent.SetActive(true);
}
}
}

View File

@@ -1,175 +1,205 @@
using System.Collections;
using System.Linq;
using UnityEngine;
using Pixelplacement;
using Minigames.DivingForPictures;
using Bootstrap;
using Core;
using Input;
using UnityEngine.Events;
using Pixelplacement;
using UI.Core;
using UnityEngine;
public class DivingTutorial : MonoBehaviour, ITouchInputConsumer
namespace UI.Tutorial
{
private StateMachine stateMachine;
public DivingGameManager divingGameManager;
public bool playTutorial;
// gating for input until current state's animation finishes first loop
private bool canAcceptInput = false;
private Coroutine waitLoopCoroutine;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
public class DivingTutorial : MonoBehaviour, ITouchInputConsumer
{
if (playTutorial == true)
private StateMachine _stateMachine;
public bool playTutorial;
// gating for input until current state's animation finishes first loop
[SerializeField] private GameObject tapPrompt;
private bool _canAcceptInput;
private Coroutine _waitLoopCoroutine;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
InitializeTutorial();
BootCompletionService.RegisterInitAction(InitializeTutorial);
// Ensure prompt is hidden initially (even before tutorial initialization)
if (tapPrompt != null)
tapPrompt.SetActive(false);
}
else { RemoveTutorial(); }
}
void InitializeTutorial()
{
stateMachine = GetComponentInChildren<StateMachine>();
divingGameManager.Pause();
InputManager.Instance.RegisterOverrideConsumer(this);
stateMachine.OnLastStateExited.AddListener(RemoveTutorial);
// prepare gating for the initial active state
SetupInputGateForCurrentState();
}
void RemoveTutorial()
{
Debug.Log("Remove me!");
if (waitLoopCoroutine != null)
void InitializeTutorial()
{
StopCoroutine(waitLoopCoroutine);
waitLoopCoroutine = null;
}
InputManager.Instance.UnregisterOverrideConsumer(this);
divingGameManager.DoResume();
Destroy(gameObject);
}
public void OnTap(Vector2 position)
{
if (!canAcceptInput) return; // block taps until allowed
// consume this tap and immediately block further taps
canAcceptInput = false;
// move to next state
stateMachine.Next(true);
// after the state changes, set up gating for the new active state's animation
SetupInputGateForCurrentState();
}
public void OnHoldStart(Vector2 position)
{
return;
}
public void OnHoldMove(Vector2 position)
{
return;
}
public void OnHoldEnd(Vector2 position)
{
return;
}
private void SetupInputGateForCurrentState()
{
if (waitLoopCoroutine != null)
{
StopCoroutine(waitLoopCoroutine);
waitLoopCoroutine = null;
}
waitLoopCoroutine = StartCoroutine(WaitForFirstLoopOnActiveState());
}
private IEnumerator WaitForFirstLoopOnActiveState()
{
// wait a frame to ensure StateMachine has activated the correct state GameObject
yield return null;
// find the active child under the StateMachine (the current state)
Transform smTransform = stateMachine != null ? stateMachine.transform : transform;
Transform activeState = null;
for (int i = 0; i < smTransform.childCount; i++)
{
var child = smTransform.GetChild(i);
if (child.gameObject.activeInHierarchy)
if (playTutorial)
{
activeState = child;
break;
// pause the game, hide UI, and register for input overrides
GameManager.Instance.RequestPause(this);
UIPageController.Instance.HideAllUI();
InputManager.Instance.RegisterOverrideConsumer(this);
// Setup references
_stateMachine = GetComponentInChildren<StateMachine>();
_stateMachine.OnLastStateExited.AddListener(RemoveTutorial);
// prepare gating for the initial active state
SetupInputGateForCurrentState();
}
else
{
RemoveTutorial();
}
}
if (activeState == null)
void RemoveTutorial()
{
// if we can't find an active state, fail open: allow input
canAcceptInput = true;
yield break;
}
// look for a legacy Animation component on the active state
var anim = activeState.GetComponent<Animation>();
if (anim == null)
{
// no animation to wait for; allow input immediately
canAcceptInput = true;
yield break;
}
// determine a clip/state to observe
string clipName = anim.clip != null ? anim.clip.name : null;
AnimationState observedState = null;
if (!string.IsNullOrEmpty(clipName))
{
observedState = anim[clipName];
}
else
{
// fallback: take the first enabled state in the Animation
foreach (AnimationState st in anim)
Debug.Log("Remove me!");
if (_waitLoopCoroutine != null)
{
observedState = st;
break;
StopCoroutine(_waitLoopCoroutine);
_waitLoopCoroutine = null;
}
// Unpause, unregister input, and show UI
InputManager.Instance.UnregisterOverrideConsumer(this);
UIPageController.Instance.ShowAllUI();
GameManager.Instance.ReleasePause(this);
// hide prompt if present
if (tapPrompt != null)
tapPrompt.SetActive(false);
Destroy(gameObject);
}
public void OnTap(Vector2 position)
{
if (!_canAcceptInput) return; // block taps until allowed
// consume this tap and immediately block further taps
SetInputEnabled(false);
// move to next state
_stateMachine.Next(true);
// after the state changes, set up gating for the new active state's animation
SetupInputGateForCurrentState();
}
public void OnHoldStart(Vector2 position)
{
}
public void OnHoldMove(Vector2 position)
{
}
public void OnHoldEnd(Vector2 position)
{
}
// centralize enabling/disabling input and the tap prompt
private void SetInputEnabled(bool allow)
{
_canAcceptInput = allow;
if (tapPrompt != null)
{
tapPrompt.SetActive(allow);
}
}
if (observedState == null)
private void SetupInputGateForCurrentState()
{
// nothing to observe; allow input
canAcceptInput = true;
yield break;
if (_waitLoopCoroutine != null)
{
StopCoroutine(_waitLoopCoroutine);
_waitLoopCoroutine = null;
}
_waitLoopCoroutine = StartCoroutine(WaitForFirstLoopOnActiveState());
}
// wait until the animation starts playing the observed clip
float safetyTimer = 0f;
while (anim.isActiveAndEnabled && activeState.gameObject.activeInHierarchy && !anim.IsPlaying(observedState.name) && safetyTimer < 2f)
private IEnumerator WaitForFirstLoopOnActiveState()
{
safetyTimer += Time.deltaTime;
// wait a frame to ensure StateMachine has activated the correct state GameObject
yield return null;
}
// wait until the first loop completes (normalizedTime >= 1)
while (anim.isActiveAndEnabled && activeState.gameObject.activeInHierarchy)
{
// if state changed (not playing anymore), allow input to avoid deadlock
if (!anim.IsPlaying(observedState.name)) break;
if (observedState.normalizedTime >= 1f)
// find the active child under the StateMachine (the current state)
Transform smTransform = _stateMachine != null ? _stateMachine.transform : transform;
Transform activeState = null;
for (int i = 0; i < smTransform.childCount; i++)
{
break;
var child = smTransform.GetChild(i);
if (child.gameObject.activeInHierarchy)
{
activeState = child;
break;
}
}
yield return null;
}
canAcceptInput = true;
waitLoopCoroutine = null;
if (activeState == null)
{
// if we can't find an active state, fail open: allow input
SetInputEnabled(true);
yield break;
}
// look for a legacy Animation component on the active state
var anim = activeState.GetComponent<Animation>();
if (anim == null)
{
// no animation to wait for; allow input immediately
SetInputEnabled(true);
yield break;
}
// determine a clip/state to observe
string clipName = anim.clip != null ? anim.clip.name : null;
AnimationState observedState = null;
if (!string.IsNullOrEmpty(clipName))
{
observedState = anim[clipName];
}
else
{
// fallback: take the first enabled state in the Animation
foreach (AnimationState st in anim)
{
observedState = st;
break;
}
}
if (observedState == null)
{
// nothing to observe; allow input
SetInputEnabled(true);
yield break;
}
// wait until the animation starts playing the observed clip
float safetyTimer = 0f;
while (anim.isActiveAndEnabled && activeState.gameObject.activeInHierarchy && !anim.IsPlaying(observedState.name) && safetyTimer < 2f)
{
safetyTimer += Time.deltaTime;
yield return null;
}
// wait until the first loop completes (normalizedTime >= 1)
while (anim.isActiveAndEnabled && activeState.gameObject.activeInHierarchy)
{
// if state changed (not playing anymore), allow input to avoid deadlock
if (!anim.IsPlaying(observedState.name)) break;
if (observedState.normalizedTime >= 1f)
{
break;
}
yield return null;
}
SetInputEnabled(true);
_waitLoopCoroutine = null;
}
}
}