From 252cb998843429ae01c632b9d8c1aa404b2e7b46 Mon Sep 17 00:00:00 2001 From: Michal Pikulski Date: Mon, 10 Nov 2025 10:41:38 +0100 Subject: [PATCH 1/2] Update icons in minigame --- Assets/Prefabs/Managers/PlayerHUD.prefab | 1 + .../UI/CardSystem/MinigameBoosterGiver.cs | 82 +++++++++-- Assets/Scripts/UI/PlayerHudManager.cs | 130 ++++++++++++++++++ 3 files changed, 201 insertions(+), 12 deletions(-) diff --git a/Assets/Prefabs/Managers/PlayerHUD.prefab b/Assets/Prefabs/Managers/PlayerHUD.prefab index b6661425..d10dc50c 100644 --- a/Assets/Prefabs/Managers/PlayerHUD.prefab +++ b/Assets/Prefabs/Managers/PlayerHUD.prefab @@ -1185,6 +1185,7 @@ MonoBehaviour: CinematicBackground: {fileID: 1256355336041814197} eagleEye: {fileID: 8093509920149135307} ramaSjangButton: {fileID: 4599222264323240281} + scrabBookButton: {fileID: 2880351836456325619} cinematicSprites: {fileID: 0} cinematicBackgroundSprites: {fileID: 0} currentCinematicPlayer: {fileID: 0} diff --git a/Assets/Scripts/UI/CardSystem/MinigameBoosterGiver.cs b/Assets/Scripts/UI/CardSystem/MinigameBoosterGiver.cs index c395d718..1ba5e86a 100644 --- a/Assets/Scripts/UI/CardSystem/MinigameBoosterGiver.cs +++ b/Assets/Scripts/UI/CardSystem/MinigameBoosterGiver.cs @@ -10,7 +10,8 @@ namespace UI.CardSystem /// /// Singleton UI component for granting booster packs from minigames. /// Displays a booster pack with glow effect, waits for user to click continue, - /// then animates the pack flying to bottom-left corner before granting the reward. + /// 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. /// public class MinigameBoosterGiver : MonoBehaviour { @@ -187,23 +188,61 @@ namespace UI.CardSystem yield break; } - // Calculate bottom-left corner position in local space - RectTransform canvasRect = GetComponentInParent()?.GetComponent(); - Vector3 targetPosition; - - if (canvasRect != null) + // Show scrapbook button temporarily using HUD visibility context + PlayerHudManager.HudVisibilityContext hudContext = null; + GameObject scrapbookButton = null; + + scrapbookButton = PlayerHudManager.Instance.GetScrabookButton(); + if (scrapbookButton != null) { - // Convert bottom-left corner with offset to local position - Vector2 bottomLeft = new Vector2(-canvasRect.rect.width / 2f, -canvasRect.rect.height / 2f); - targetPosition = bottomLeft + targetBottomLeftOffset; + hudContext = PlayerHudManager.Instance.ShowElementTemporarily(scrapbookButton); } else { - // Fallback if no canvas found - targetPosition = _boosterInitialPosition + new Vector3(-500f, -500f, 0f); + Debug.LogWarning("[MinigameBoosterGiver] Scrapbook button not found in PlayerHudManager."); } - // Tween to bottom-left corner + // 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(); + if (scrapbookRect != null) + { + // Convert scrapbook button's world position to local position relative to boosterImage's parent + Canvas canvas = GetComponentInParent(); + 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 + { + Debug.LogWarning("[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 @@ -224,6 +263,9 @@ namespace UI.CardSystem Debug.LogWarning("[MinigameBoosterGiver] CardSystemManager not found, cannot grant booster pack."); } + // Hide scrapbook button by disposing the context + hudContext?.Dispose(); + // Hide the visual if (visualContainer != null) { @@ -237,6 +279,22 @@ namespace UI.CardSystem // Clear sequence reference _currentSequence = null; } + + private Vector3 GetFallbackPosition() + { + RectTransform canvasRect = GetComponentInParent()?.GetComponent(); + 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); + } + } } } diff --git a/Assets/Scripts/UI/PlayerHudManager.cs b/Assets/Scripts/UI/PlayerHudManager.cs index 770584ec..6b00c084 100644 --- a/Assets/Scripts/UI/PlayerHudManager.cs +++ b/Assets/Scripts/UI/PlayerHudManager.cs @@ -7,6 +7,7 @@ using UnityEngine.Playables; using UnityEngine.UI; using System.Linq; using System.Collections.Generic; +using System; namespace UI { @@ -19,6 +20,73 @@ namespace UI { public enum UIMode { Overworld, Puzzle, Minigame, HideAll }; + /// + /// Context object for managing temporary HUD element visibility. + /// Automatically restores previous visibility state when disposed. + /// Usage: using (var ctx = PlayerHudManager.Instance.ShowElementTemporarily(myButton)) { ... } + /// + public class HudVisibilityContext : IDisposable + { + private readonly GameObject _element; + private readonly bool _previousState; + private bool _disposed; + + internal HudVisibilityContext(GameObject element, bool show) + { + _element = element; + _previousState = element.activeSelf; + _element.SetActive(show); + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + if (_element != null) + { + _element.SetActive(_previousState); + } + } + } + + /// + /// Multi-element visibility context for managing multiple HUD elements at once. + /// Automatically restores previous visibility states when disposed. + /// + public class MultiHudVisibilityContext : IDisposable + { + private readonly List<(GameObject element, bool previousState)> _elements; + private bool _disposed; + + internal MultiHudVisibilityContext(IEnumerable elements, bool show) + { + _elements = new List<(GameObject, bool)>(); + foreach (var element in elements) + { + if (element != null) + { + _elements.Add((element, element.activeSelf)); + element.SetActive(show); + } + } + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + foreach (var (element, previousState) in _elements) + { + if (element != null) + { + element.SetActive(previousState); + } + } + } + } + public UIMode currentUIMode; [Header("HUD Management")] @@ -33,6 +101,7 @@ namespace UI [Header("HUD Elements")] public GameObject eagleEye; public GameObject ramaSjangButton; + public GameObject scrabBookButton; [HideInInspector] public Image cinematicSprites; [HideInInspector] public Image cinematicBackgroundSprites; @@ -254,6 +323,9 @@ namespace UI if (visible) { eagleEye.SetActive(false); + ramaSjangButton.SetActive(false); + scrabBookButton.SetActive(false); + } break; case UIMode.HideAll: @@ -318,6 +390,64 @@ namespace UI UnityEngine.Debug.LogError("[PlayerHudManager] Cannot push page - UIPageController not found!"); } } + + #region HUD Element Getters + + public GameObject GetEagleEye() => eagleEye; + public GameObject GetRamaSjangButton() => ramaSjangButton; + public GameObject GetScrabookButton() => scrabBookButton; + + #endregion + + #region Context-Based Visibility Management + + /// + /// Temporarily shows a HUD element. Returns a context object that restores the previous state when disposed. + /// Usage: using (var ctx = PlayerHudManager.Instance.ShowElementTemporarily(myButton)) { /* element is visible */ } + /// + public HudVisibilityContext ShowElementTemporarily(GameObject element) + { + if (element == null) + { + Logging.Warning("[PlayerHudManager] Attempted to show null element"); + return null; + } + return new HudVisibilityContext(element, true); + } + + /// + /// Temporarily hides a HUD element. Returns a context object that restores the previous state when disposed. + /// Usage: using (var ctx = PlayerHudManager.Instance.HideElementTemporarily(myButton)) { /* element is hidden */ } + /// + public HudVisibilityContext HideElementTemporarily(GameObject element) + { + if (element == null) + { + Logging.Warning("[PlayerHudManager] Attempted to hide null element"); + return null; + } + return new HudVisibilityContext(element, false); + } + + /// + /// Temporarily shows multiple HUD elements. Returns a context object that restores all previous states when disposed. + /// Usage: using (var ctx = PlayerHudManager.Instance.ShowElementsTemporarily(button1, button2, button3)) { /* elements are visible */ } + /// + public MultiHudVisibilityContext ShowElementsTemporarily(params GameObject[] elements) + { + return new MultiHudVisibilityContext(elements, true); + } + + /// + /// Temporarily hides multiple HUD elements. Returns a context object that restores all previous states when disposed. + /// Usage: using (var ctx = PlayerHudManager.Instance.HideElementsTemporarily(button1, button2, button3)) { /* elements are hidden */ } + /// + public MultiHudVisibilityContext HideElementsTemporarily(params GameObject[] elements) + { + return new MultiHudVisibilityContext(elements, false); + } + + #endregion } } From 75cd70a18ad785702994f80ce96e533defb38517 Mon Sep 17 00:00:00 2001 From: Michal Pikulski Date: Mon, 10 Nov 2025 11:09:06 +0100 Subject: [PATCH 2/2] Play intro audio only once --- Assets/Scenes/Levels/Quarry.unity | 4 ++ .../Core/Lifecycle/LifecycleManager.cs | 28 ++++++++ .../Core/Lifecycle/ManagedBehaviour.cs | 28 ++++++++ .../Scripts/Core/SaveLoad/SaveLoadManager.cs | 23 ++++-- Assets/Scripts/Sound/LevelAudioObject.cs | 70 +++++++++++++++++-- 5 files changed, 141 insertions(+), 12 deletions(-) diff --git a/Assets/Scenes/Levels/Quarry.unity b/Assets/Scenes/Levels/Quarry.unity index 7f1908bc..a9b002da 100644 --- a/Assets/Scenes/Levels/Quarry.unity +++ b/Assets/Scenes/Levels/Quarry.unity @@ -453761,6 +453761,10 @@ PrefabInstance: propertyPath: m_PlayOnAwake value: 0 objectReference: {fileID: 0} + - target: {fileID: 8545106365577783398, guid: ead4e790fa3a1924ebd1586c93cd5479, type: 3} + propertyPath: isOneTime + value: 1 + objectReference: {fileID: 0} m_RemovedComponents: [] m_RemovedGameObjects: [] m_AddedGameObjects: [] diff --git a/Assets/Scripts/Core/Lifecycle/LifecycleManager.cs b/Assets/Scripts/Core/Lifecycle/LifecycleManager.cs index 82bbd221..2ac71c73 100644 --- a/Assets/Scripts/Core/Lifecycle/LifecycleManager.cs +++ b/Assets/Scripts/Core/Lifecycle/LifecycleManager.cs @@ -469,6 +469,34 @@ namespace Core.Lifecycle LogDebug($"Restored scene data to {restoredCount} components"); } + /// + /// Broadcasts scene restore completed event to all registered components. + /// Called AFTER all OnSceneRestoreRequested calls complete. + /// + public void BroadcastSceneRestoreCompleted() + { + LogDebug("Broadcasting SceneRestoreCompleted"); + + // Create a copy to avoid collection modification during iteration + var componentsCopy = new List(managedAwakeList); + + foreach (var component in componentsCopy) + { + if (component == null) continue; + + try + { + component.InvokeSceneRestoreCompleted(); + } + catch (Exception ex) + { + Debug.LogError($"[LifecycleManager] Exception during scene restore completed for {component.SaveId}: {ex}"); + } + } + + LogDebug("SceneRestoreCompleted broadcast complete"); + } + /// /// Broadcasts global restore request to all registered components that opt-in. /// Distributes serialized data to matching components by SaveId. diff --git a/Assets/Scripts/Core/Lifecycle/ManagedBehaviour.cs b/Assets/Scripts/Core/Lifecycle/ManagedBehaviour.cs index 085797be..5281424c 100644 --- a/Assets/Scripts/Core/Lifecycle/ManagedBehaviour.cs +++ b/Assets/Scripts/Core/Lifecycle/ManagedBehaviour.cs @@ -89,6 +89,7 @@ namespace Core.Lifecycle public void InvokeSceneReady() => OnSceneReady(); public string InvokeSceneSaveRequested() => OnSceneSaveRequested(); public void InvokeSceneRestoreRequested(string data) => OnSceneRestoreRequested(data); + public void InvokeSceneRestoreCompleted() => OnSceneRestoreCompleted(); public string InvokeGlobalSaveRequested() => OnGlobalSaveRequested(); public void InvokeGlobalRestoreRequested(string data) => OnGlobalRestoreRequested(data); public void InvokeManagedDestroy() => OnManagedDestroy(); @@ -202,6 +203,10 @@ namespace Core.Lifecycle /// Called during scene transitions to restore scene-specific state. /// Receives previously serialized data (from OnSceneSaveRequested). /// + /// IMPORTANT: This method MUST be synchronous. Do not use coroutines or async/await. + /// OnSceneRestoreCompleted is called immediately after all restore calls complete, + /// so any async operations would still be running when it fires. + /// /// TIMING: /// - Called AFTER scene load, during OnSceneReady phase /// - Frequency: Every scene transition @@ -212,6 +217,29 @@ namespace Core.Lifecycle // Default: no-op } + /// + /// Called after all scene restore operations complete. + /// Does NOT receive data - use OnSceneRestoreRequested for that. + /// + /// GUARANTEE: + /// - ALWAYS called after scene load, whether there's save data or not + /// - All OnSceneRestoreRequested() calls have RETURNED (but async operations may still be running) + /// - Safe for synchronous restore operations (JSON deserialization, setting fields, etc.) + /// + /// TIMING: + /// - Called AFTER all OnSceneRestoreRequested calls complete (or immediately if no save data exists) + /// - Frequency: Every scene transition + /// - Use for: Post-restore initialization, first-time initialization, triggering events after state is restored + /// + /// COMMON PATTERN: + /// Use this to perform actions that depend on whether data was restored or not. + /// Example: Play one-time audio only if it hasn't been played before (_hasPlayed == false). + /// + protected virtual void OnSceneRestoreCompleted() + { + // Default: no-op + } + /// /// Called once on game boot to restore global persistent state. /// Receives data that was saved via OnGlobalSaveRequested. diff --git a/Assets/Scripts/Core/SaveLoad/SaveLoadManager.cs b/Assets/Scripts/Core/SaveLoad/SaveLoadManager.cs index ecbeb0ca..86a8ed76 100644 --- a/Assets/Scripts/Core/SaveLoad/SaveLoadManager.cs +++ b/Assets/Scripts/Core/SaveLoad/SaveLoadManager.cs @@ -504,9 +504,19 @@ namespace Core.SaveLoad /// public void RestoreSceneData() { + if (Lifecycle.LifecycleManager.Instance == null) + { + Logging.Warning("[SaveLoadManager] LifecycleManager not available for scene restore"); + return; + } + if (currentSaveData == null || currentSaveData.participantStates == null) { - Logging.Debug("[SaveLoadManager] No scene data to restore"); + Logging.Debug("[SaveLoadManager] No scene data to restore (first visit or no save data)"); + + // Still broadcast restore completed so components can initialize properly + Lifecycle.LifecycleManager.Instance.BroadcastSceneRestoreCompleted(); + Logging.Debug($"[SaveLoadManager] Scene restore completed (no data)"); return; } @@ -520,11 +530,12 @@ namespace Core.SaveLoad } // Restore scene data via LifecycleManager - if (Lifecycle.LifecycleManager.Instance != null) - { - Lifecycle.LifecycleManager.Instance.BroadcastSceneRestoreRequested(saveDataDict); - Logging.Debug($"[SaveLoadManager] Broadcast scene restore to LifecycleManager"); - } + Lifecycle.LifecycleManager.Instance.BroadcastSceneRestoreRequested(saveDataDict); + Logging.Debug($"[SaveLoadManager] Broadcast scene restore to LifecycleManager"); + + // Broadcast scene restore completed - ALWAYS called, whether there's data or not + Lifecycle.LifecycleManager.Instance.BroadcastSceneRestoreCompleted(); + Logging.Debug($"[SaveLoadManager] Scene restore completed"); } /// diff --git a/Assets/Scripts/Sound/LevelAudioObject.cs b/Assets/Scripts/Sound/LevelAudioObject.cs index ed74b94d..23fab489 100644 --- a/Assets/Scripts/Sound/LevelAudioObject.cs +++ b/Assets/Scripts/Sound/LevelAudioObject.cs @@ -1,22 +1,80 @@ using UnityEngine; using UnityEngine.Audio; using System; +using Core.Lifecycle; -public class LevelAudioObject : MonoBehaviour +[Serializable] +public class LevelAudioObjectSaveData { + public bool hasPlayed; +} + +public class LevelAudioObject : ManagedBehaviour +{ + [Header("Audio Settings")] public AppleAudioSource narratorAudioSource; public AudioResource firstNarration; + + [Header("Playback Settings")] + [Tooltip("If true, the audio will only play once and never again after being played")] + public bool isOneTime; - // Start is called once before the first execution of Update after the MonoBehaviour is created - void Start() + private bool _hasPlayed; + + #region ManagedBehaviour Overrides + + public override bool AutoRegisterForSave => isOneTime; // Only save if one-time audio + + protected override string OnSceneSaveRequested() { - PlayNarrationAudio(); + if (!isOneTime) + return null; // No need to save if not one-time + + LevelAudioObjectSaveData saveData = new LevelAudioObjectSaveData + { + hasPlayed = _hasPlayed + }; + + return JsonUtility.ToJson(saveData); } - void PlayNarrationAudio() + protected override void OnSceneRestoreRequested(string serializedData) { + if (!isOneTime || string.IsNullOrEmpty(serializedData)) + return; + + try + { + LevelAudioObjectSaveData saveData = JsonUtility.FromJson(serializedData); + _hasPlayed = saveData.hasPlayed; + } + catch (Exception e) + { + Debug.LogWarning($"[LevelAudioObject] Failed to restore audio state: {e.Message}"); + } + } + + protected override void OnSceneRestoreCompleted() + { + if (isOneTime && !_hasPlayed) + { + PlayNarrationAudio(); + } + } + + #endregion + + private void PlayNarrationAudio() + { + if (narratorAudioSource == null || firstNarration == null) + { + Debug.LogWarning($"[LevelAudioObject] Missing audio source or narration resource on {gameObject.name}"); + return; + } + narratorAudioSource.audioSource.resource = firstNarration; narratorAudioSource.Play(0); + + _hasPlayed = true; } - }