Merge branch 'main' into DamianBranch

This commit is contained in:
2025-11-10 11:10:14 +01:00
8 changed files with 342 additions and 24 deletions

View File

@@ -1185,6 +1185,7 @@ MonoBehaviour:
CinematicBackground: {fileID: 1256355336041814197} CinematicBackground: {fileID: 1256355336041814197}
eagleEye: {fileID: 8093509920149135307} eagleEye: {fileID: 8093509920149135307}
ramaSjangButton: {fileID: 4599222264323240281} ramaSjangButton: {fileID: 4599222264323240281}
scrabBookButton: {fileID: 2880351836456325619}
cinematicSprites: {fileID: 0} cinematicSprites: {fileID: 0}
cinematicBackgroundSprites: {fileID: 0} cinematicBackgroundSprites: {fileID: 0}
currentCinematicPlayer: {fileID: 0} currentCinematicPlayer: {fileID: 0}

View File

@@ -453761,6 +453761,10 @@ PrefabInstance:
propertyPath: m_PlayOnAwake propertyPath: m_PlayOnAwake
value: 0 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 8545106365577783398, guid: ead4e790fa3a1924ebd1586c93cd5479, type: 3}
propertyPath: isOneTime
value: 1
objectReference: {fileID: 0}
m_RemovedComponents: [] m_RemovedComponents: []
m_RemovedGameObjects: [] m_RemovedGameObjects: []
m_AddedGameObjects: [] m_AddedGameObjects: []

View File

@@ -469,6 +469,34 @@ namespace Core.Lifecycle
LogDebug($"Restored scene data to {restoredCount} components"); LogDebug($"Restored scene data to {restoredCount} components");
} }
/// <summary>
/// Broadcasts scene restore completed event to all registered components.
/// Called AFTER all OnSceneRestoreRequested calls complete.
/// </summary>
public void BroadcastSceneRestoreCompleted()
{
LogDebug("Broadcasting SceneRestoreCompleted");
// Create a copy to avoid collection modification during iteration
var componentsCopy = new List<ManagedBehaviour>(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");
}
/// <summary> /// <summary>
/// Broadcasts global restore request to all registered components that opt-in. /// Broadcasts global restore request to all registered components that opt-in.
/// Distributes serialized data to matching components by SaveId. /// Distributes serialized data to matching components by SaveId.

View File

@@ -89,6 +89,7 @@ namespace Core.Lifecycle
public void InvokeSceneReady() => OnSceneReady(); public void InvokeSceneReady() => OnSceneReady();
public string InvokeSceneSaveRequested() => OnSceneSaveRequested(); public string InvokeSceneSaveRequested() => OnSceneSaveRequested();
public void InvokeSceneRestoreRequested(string data) => OnSceneRestoreRequested(data); public void InvokeSceneRestoreRequested(string data) => OnSceneRestoreRequested(data);
public void InvokeSceneRestoreCompleted() => OnSceneRestoreCompleted();
public string InvokeGlobalSaveRequested() => OnGlobalSaveRequested(); public string InvokeGlobalSaveRequested() => OnGlobalSaveRequested();
public void InvokeGlobalRestoreRequested(string data) => OnGlobalRestoreRequested(data); public void InvokeGlobalRestoreRequested(string data) => OnGlobalRestoreRequested(data);
public void InvokeManagedDestroy() => OnManagedDestroy(); public void InvokeManagedDestroy() => OnManagedDestroy();
@@ -202,6 +203,10 @@ namespace Core.Lifecycle
/// Called during scene transitions to restore scene-specific state. /// Called during scene transitions to restore scene-specific state.
/// Receives previously serialized data (from OnSceneSaveRequested). /// 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: /// TIMING:
/// - Called AFTER scene load, during OnSceneReady phase /// - Called AFTER scene load, during OnSceneReady phase
/// - Frequency: Every scene transition /// - Frequency: Every scene transition
@@ -212,6 +217,29 @@ namespace Core.Lifecycle
// Default: no-op // Default: no-op
} }
/// <summary>
/// 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).
/// </summary>
protected virtual void OnSceneRestoreCompleted()
{
// Default: no-op
}
/// <summary> /// <summary>
/// Called once on game boot to restore global persistent state. /// Called once on game boot to restore global persistent state.
/// Receives data that was saved via OnGlobalSaveRequested. /// Receives data that was saved via OnGlobalSaveRequested.

View File

@@ -504,9 +504,19 @@ namespace Core.SaveLoad
/// </summary> /// </summary>
public void RestoreSceneData() public void RestoreSceneData()
{ {
if (Lifecycle.LifecycleManager.Instance == null)
{
Logging.Warning("[SaveLoadManager] LifecycleManager not available for scene restore");
return;
}
if (currentSaveData == null || currentSaveData.participantStates == null) 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; return;
} }
@@ -520,11 +530,12 @@ namespace Core.SaveLoad
} }
// Restore scene data via LifecycleManager // 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");
} }
/// <summary> /// <summary>

View File

@@ -1,22 +1,80 @@
using UnityEngine; using UnityEngine;
using UnityEngine.Audio; using UnityEngine.Audio;
using System; 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 AppleAudioSource narratorAudioSource;
public AudioResource firstNarration; public AudioResource firstNarration;
// Start is called once before the first execution of Update after the MonoBehaviour is created [Header("Playback Settings")]
void Start() [Tooltip("If true, the audio will only play once and never again after being played")]
public bool isOneTime;
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<LevelAudioObjectSaveData>(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.audioSource.resource = firstNarration;
narratorAudioSource.Play(0); narratorAudioSource.Play(0);
}
_hasPlayed = true;
}
} }

View File

@@ -10,7 +10,8 @@ namespace UI.CardSystem
/// <summary> /// <summary>
/// Singleton UI component for granting booster packs from minigames. /// Singleton UI component for granting booster packs from minigames.
/// Displays a booster pack with glow effect, waits for user to click continue, /// 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.
/// </summary> /// </summary>
public class MinigameBoosterGiver : MonoBehaviour public class MinigameBoosterGiver : MonoBehaviour
{ {
@@ -187,23 +188,61 @@ namespace UI.CardSystem
yield break; yield break;
} }
// Calculate bottom-left corner position in local space // Show scrapbook button temporarily using HUD visibility context
RectTransform canvasRect = GetComponentInParent<Canvas>()?.GetComponent<RectTransform>(); PlayerHudManager.HudVisibilityContext hudContext = null;
Vector3 targetPosition; GameObject scrapbookButton = null;
if (canvasRect != null) scrapbookButton = PlayerHudManager.Instance.GetScrabookButton();
if (scrapbookButton != null)
{ {
// Convert bottom-left corner with offset to local position hudContext = PlayerHudManager.Instance.ShowElementTemporarily(scrapbookButton);
Vector2 bottomLeft = new Vector2(-canvasRect.rect.width / 2f, -canvasRect.rect.height / 2f);
targetPosition = bottomLeft + targetBottomLeftOffset;
} }
else else
{ {
// Fallback if no canvas found Debug.LogWarning("[MinigameBoosterGiver] Scrapbook button not found in PlayerHudManager.");
targetPosition = _boosterInitialPosition + new Vector3(-500f, -500f, 0f);
} }
// 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<RectTransform>();
if (scrapbookRect != 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);
}
}
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); Tween.LocalPosition(boosterImage, targetPosition, disappearDuration, 0f, Tween.EaseInBack);
// Scale down // Scale down
@@ -224,6 +263,9 @@ namespace UI.CardSystem
Debug.LogWarning("[MinigameBoosterGiver] CardSystemManager not found, cannot grant booster pack."); Debug.LogWarning("[MinigameBoosterGiver] CardSystemManager not found, cannot grant booster pack.");
} }
// Hide scrapbook button by disposing the context
hudContext?.Dispose();
// Hide the visual // Hide the visual
if (visualContainer != null) if (visualContainer != null)
{ {
@@ -237,6 +279,22 @@ namespace UI.CardSystem
// Clear sequence reference // Clear sequence reference
_currentSequence = null; _currentSequence = null;
} }
private Vector3 GetFallbackPosition()
{
RectTransform canvasRect = GetComponentInParent<Canvas>()?.GetComponent<RectTransform>();
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);
}
}
} }
} }

View File

@@ -7,6 +7,7 @@ using UnityEngine.Playables;
using UnityEngine.UI; using UnityEngine.UI;
using System.Linq; using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using System;
namespace UI namespace UI
{ {
@@ -19,6 +20,73 @@ namespace UI
{ {
public enum UIMode { Overworld, Puzzle, Minigame, HideAll }; public enum UIMode { Overworld, Puzzle, Minigame, HideAll };
/// <summary>
/// Context object for managing temporary HUD element visibility.
/// Automatically restores previous visibility state when disposed.
/// Usage: using (var ctx = PlayerHudManager.Instance.ShowElementTemporarily(myButton)) { ... }
/// </summary>
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);
}
}
}
/// <summary>
/// Multi-element visibility context for managing multiple HUD elements at once.
/// Automatically restores previous visibility states when disposed.
/// </summary>
public class MultiHudVisibilityContext : IDisposable
{
private readonly List<(GameObject element, bool previousState)> _elements;
private bool _disposed;
internal MultiHudVisibilityContext(IEnumerable<GameObject> 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; public UIMode currentUIMode;
[Header("HUD Management")] [Header("HUD Management")]
@@ -33,6 +101,7 @@ namespace UI
[Header("HUD Elements")] [Header("HUD Elements")]
public GameObject eagleEye; public GameObject eagleEye;
public GameObject ramaSjangButton; public GameObject ramaSjangButton;
public GameObject scrabBookButton;
[HideInInspector] public Image cinematicSprites; [HideInInspector] public Image cinematicSprites;
[HideInInspector] public Image cinematicBackgroundSprites; [HideInInspector] public Image cinematicBackgroundSprites;
@@ -254,6 +323,9 @@ namespace UI
if (visible) if (visible)
{ {
eagleEye.SetActive(false); eagleEye.SetActive(false);
ramaSjangButton.SetActive(false);
scrabBookButton.SetActive(false);
} }
break; break;
case UIMode.HideAll: case UIMode.HideAll:
@@ -318,6 +390,64 @@ namespace UI
UnityEngine.Debug.LogError("[PlayerHudManager] Cannot push page - UIPageController not found!"); 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
/// <summary>
/// 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 */ }
/// </summary>
public HudVisibilityContext ShowElementTemporarily(GameObject element)
{
if (element == null)
{
Logging.Warning("[PlayerHudManager] Attempted to show null element");
return null;
}
return new HudVisibilityContext(element, true);
}
/// <summary>
/// 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 */ }
/// </summary>
public HudVisibilityContext HideElementTemporarily(GameObject element)
{
if (element == null)
{
Logging.Warning("[PlayerHudManager] Attempted to hide null element");
return null;
}
return new HudVisibilityContext(element, false);
}
/// <summary>
/// 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 */ }
/// </summary>
public MultiHudVisibilityContext ShowElementsTemporarily(params GameObject[] elements)
{
return new MultiHudVisibilityContext(elements, true);
}
/// <summary>
/// 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 */ }
/// </summary>
public MultiHudVisibilityContext HideElementsTemporarily(params GameObject[] elements)
{
return new MultiHudVisibilityContext(elements, false);
}
#endregion
} }
} }