Rework of base interactables and managed behaviors

This commit is contained in:
Michal Pikulski
2025-11-04 11:11:27 +01:00
parent 0dc3f3e803
commit c57e3aa7e0
62 changed files with 11193 additions and 1376 deletions

View File

@@ -1,161 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AppleHills.Core.Settings;
using Core;
using UnityEngine;
namespace Bootstrap
{
/// <summary>
/// Service that provides notification and management of boot completion status.
/// Allows systems to subscribe to boot completion events, register initialization actions with priorities,
/// or await boot completion asynchronously.
/// </summary>
public static class BootCompletionService
{
/// <summary>
/// Indicates if the boot process has completed
/// </summary>
public static bool IsBootComplete { get; private set; } = false;
/// <summary>
/// Event triggered when boot completes
/// </summary>
public static event Action OnBootComplete;
/// <summary>
/// Represents an initialization action with priority
/// </summary>
private class InitializationAction
{
public Action Action { get; }
public int Priority { get; }
public string Name { get; }
public InitializationAction(Action action, int priority, string name)
{
Action = action;
Priority = priority;
Name = name;
}
}
// List of initialization actions to be executed once boot completes
private static List<InitializationAction> _initializationActions = new List<InitializationAction>();
// TaskCompletionSource for async await pattern
private static TaskCompletionSource<bool> _bootCompletionTask = new TaskCompletionSource<bool>();
/// <summary>
/// Called by CustomBoot when the boot process is complete
/// </summary>
internal static void HandleBootCompleted()
{
if (IsBootComplete)
return;
IsBootComplete = true;
LogDebugMessage("Boot process completed, executing initialization actions");
// Execute initialization actions in priority order (lower number = higher priority)
ExecuteInitializationActions();
// Trigger the event
OnBootComplete?.Invoke();
// Complete the task for async waiters
_bootCompletionTask.TrySetResult(true);
LogDebugMessage("All boot completion handlers executed");
}
/// <summary>
/// Register an action to be executed when boot completes.
/// Lower priority numbers run first.
/// </summary>
/// <param name="action">The action to execute</param>
/// <param name="priority">Priority (lower numbers run first)</param>
/// <param name="name">Name for debugging</param>
public static void RegisterInitAction(Action action, int priority = 100, string name = null)
{
if (action == null)
return;
if (string.IsNullOrEmpty(name))
name = $"Action_{_initializationActions.Count}";
var initAction = new InitializationAction(action, priority, name);
if (IsBootComplete)
{
// If boot is already complete, execute immediately
LogDebugMessage($"Executing late registration: {name} (Priority: {priority})");
try
{
action();
}
catch (Exception ex)
{
LogDebugMessage($"Error executing init action '{name}': {ex}");
}
}
else
{
// Otherwise add to the queue
_initializationActions.Add(initAction);
LogDebugMessage($"Registered init action: {name} (Priority: {priority})");
}
}
/// <summary>
/// Wait asynchronously for boot completion
/// </summary>
/// <returns>Task that completes when boot is complete</returns>
public static Task WaitForBootCompletionAsync()
{
if (IsBootComplete)
return Task.CompletedTask;
return _bootCompletionTask.Task;
}
/// <summary>
/// Execute all registered initialization actions in priority order
/// </summary>
private static void ExecuteInitializationActions()
{
// Sort by priority (lowest first)
var sortedActions = _initializationActions
.OrderBy(a => a.Priority)
.ToList();
foreach (var action in sortedActions)
{
try
{
LogDebugMessage($"Executing: {action.Name} (Priority: {action.Priority})");
action.Action();
}
catch (Exception ex)
{
LogDebugMessage($"Error executing init action '{action.Name}': {ex}");
}
}
// Clear the list after execution
_initializationActions.Clear();
}
private static void LogDebugMessage(string message)
{
if (DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().bootstrapLogVerbosity <=
LogVerbosity.Debug)
{
Logging.Debug($"[BootCompletionService] {message}");
}
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: aa0228cf33a64515bc166b7a9bc8c0b9
timeCreated: 1760606319

View File

@@ -1,18 +1,17 @@
using System;
using System;
using AppleHills.Core.Settings;
using UnityEngine;
using UI;
using Core;
using Core.Lifecycle;
using UnityEngine.SceneManagement;
using Cinematics;
using UnityEngine.Serialization;
namespace Bootstrap
{
/// <summary>
/// Controller for the boot scene that coordinates bootstrap initialization with loading screen
/// </summary>
public class BootSceneController : MonoBehaviour
public class BootSceneController : ManagedBehaviour
{
[SerializeField] private string mainSceneName = "AppleHillsOverworld";
[SerializeField] private float minDelayAfterBoot = 0.5f; // Small delay after boot to ensure smooth transition
@@ -30,35 +29,35 @@ namespace Bootstrap
private float _sceneLoadingProgress = 0f;
private LogVerbosity _logVerbosity = LogVerbosity.Warning;
private void Start()
// Run very early - need to set up loading screen before other systems initialize
public override int ManagedAwakePriority => 5;
protected override void Awake()
{
LogDebugMessage("Boot scene started");
base.Awake(); // Register with LifecycleManager
// Ensure the initial loading screen exists
LogDebugMessage("BootSceneController.Awake() - Initializing loading screen DURING bootstrap");
// Validate loading screen exists
if (initialLoadingScreen == null)
{
Debug.LogError("[BootSceneController] No InitialLoadingScreen assigned! Please assign it in the inspector.");
return;
}
// Subscribe to the loading screen completion event
initialLoadingScreen.OnLoadingScreenFullyHidden += OnInitialLoadingComplete;
// Show the loading screen immediately with our combined progress provider
// This needs to happen DURING bootstrap to show progress
initialLoadingScreen.ShowLoadingScreen(GetCombinedProgress);
// Subscribe to boot progress events
CustomBoot.OnBootProgressChanged += OnBootProgressChanged;
// Subscribe to loading screen completion event
initialLoadingScreen.OnLoadingScreenFullyHidden += OnInitialLoadingComplete;
RegisterManagedEvent(initialLoadingScreen, nameof(initialLoadingScreen.OnLoadingScreenFullyHidden),
(Action)OnInitialLoadingComplete);
// Subscribe to boot progress for real-time updates during bootstrap
CustomBoot.OnBootProgressChanged += OnBootProgressChanged;
// Note: Static events need manual cleanup in OnDestroy
// Register our boot completion handler with the BootCompletionService
// This will execute either immediately if boot is already complete,
// or when the boot process completes
BootCompletionService.RegisterInitAction(
OnBootCompleted,
50, // Higher priority (lower number)
"BootSceneController.OnBootCompleted"
);
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().bootstrapLogVerbosity;
// In debug mode, log additional information
@@ -67,6 +66,28 @@ namespace Bootstrap
InvokeRepeating(nameof(LogDebugInfo), 0.1f, 0.5f);
}
}
protected override void OnManagedAwake()
{
LogDebugMessage("BootSceneController.OnManagedAwake() - Boot is GUARANTEED complete, starting scene loading");
// Boot is GUARANTEED complete at this point - that's the whole point of OnManagedAwake!
// No need to subscribe to OnBootCompleted or check CustomBoot.Initialised
_bootComplete = true;
_currentPhase = LoadingPhase.SceneLoading;
// Start loading the main scene after a small delay
// This prevents jerky transitions if boot happens very quickly
Invoke(nameof(StartLoadingMainMenu), minDelayAfterBoot);
}
protected override void OnDestroy()
{
// Manual cleanup for static event (RegisterManagedEvent doesn't handle static events properly)
CustomBoot.OnBootProgressChanged -= OnBootProgressChanged;
base.OnDestroy(); // Handles other managed event cleanup
}
/// <summary>
/// Called when the initial loading screen is fully hidden
@@ -94,23 +115,6 @@ namespace Bootstrap
}
}
private void OnDestroy()
{
// Clean up event subscriptions
CustomBoot.OnBootCompleted -= OnBootCompleted;
CustomBoot.OnBootProgressChanged -= OnBootProgressChanged;
if (initialLoadingScreen != null)
{
initialLoadingScreen.OnLoadingScreenFullyHidden -= OnInitialLoadingComplete;
}
if (debugMode)
{
CancelInvoke(nameof(LogDebugInfo));
}
}
/// <summary>
/// Progress provider that combines bootstrap and scene loading progress
/// </summary>
@@ -145,19 +149,7 @@ namespace Bootstrap
$"Scene: {_sceneLoadingProgress:P0}, Combined: {GetCombinedProgress():P0}, Boot Complete: {_bootComplete}");
}
private void OnBootCompleted()
{
// Unsubscribe to prevent duplicate calls
CustomBoot.OnBootCompleted -= OnBootCompleted;
LogDebugMessage("Boot process completed");
_bootComplete = true;
// After a small delay, start loading the main menu
// This prevents jerky transitions if boot happens very quickly
Invoke(nameof(StartLoadingMainMenu), minDelayAfterBoot);
}
private void StartLoadingMainMenu()
{
if (_hasStartedLoading)
@@ -207,6 +199,13 @@ namespace Bootstrap
// Ensure progress is complete
_sceneLoadingProgress = 1f;
// CRITICAL: Broadcast lifecycle events so components get their OnSceneReady callbacks
LogDebugMessage($"Broadcasting OnSceneReady for: {mainSceneName}");
LifecycleManager.Instance?.BroadcastSceneReady(mainSceneName);
LogDebugMessage($"Broadcasting OnRestoreRequested for: {mainSceneName}");
LifecycleManager.Instance?.BroadcastRestoreRequested(mainSceneName);
// Step 2: Scene is fully loaded, now hide the loading screen
// This will trigger OnInitialLoadingComplete via the event when animation completes
initialLoadingScreen.HideLoadingScreen();

View File

@@ -2,6 +2,7 @@
using System.Threading.Tasks;
using AppleHills.Core.Settings;
using Core;
using Core.Lifecycle;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
@@ -39,6 +40,10 @@ namespace Bootstrap
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)]
private static void Initialise()
{
// Create LifecycleManager FIRST - before any bootstrap logic
// This ensures it exists when boot completes
LifecycleManager.CreateInstance();
//We should always clean up after Addressables, so let's take care of that immediately
Application.quitting += ApplicationOnUnloading;
@@ -97,12 +102,14 @@ namespace Bootstrap
OnBootProgressChanged?.Invoke(1f);
OnBootCompleted?.Invoke();
// Notify the BootCompletionService that boot is complete
// Notify the LifecycleManager that boot is complete
if (Application.isPlaying)
{
// Direct call to boot completion service
LogDebugMessage("Calling BootCompletionService.HandleBootCompleted()");
BootCompletionService.HandleBootCompleted();
LogDebugMessage("Calling LifecycleManager.OnBootCompletionTriggered()");
if (LifecycleManager.Instance != null)
{
LifecycleManager.Instance.OnBootCompletionTriggered();
}
}
}
@@ -117,12 +124,14 @@ namespace Bootstrap
OnBootProgressChanged?.Invoke(1f);
OnBootCompleted?.Invoke();
// Notify the BootCompletionService that boot is complete
// Notify the LifecycleManager that boot is complete
if (Application.isPlaying)
{
// Direct call to boot completion service
LogDebugMessage("Calling BootCompletionService.HandleBootCompleted()");
BootCompletionService.HandleBootCompleted();
LogDebugMessage("Calling LifecycleManager.OnBootCompletionTriggered()");
if (LifecycleManager.Instance != null)
{
LifecycleManager.Instance.OnBootCompletionTriggered();
}
}
}

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic;
using Bootstrap;
using Core;
using Core.Lifecycle;
using UI;
using UnityEngine;
using UnityEngine.AddressableAssets;
@@ -14,7 +14,7 @@ namespace Cinematics
/// <summary>
/// Handles loading, playing and unloading cinematics
/// </summary>
public class CinematicsManager : MonoBehaviour
public class CinematicsManager : ManagedBehaviour
{
public event System.Action OnCinematicStarted;
public event System.Action OnCinematicStopped;
@@ -37,20 +37,21 @@ namespace Cinematics
public PlayableDirector playableDirector;
private void Awake()
public override int ManagedAwakePriority => 170; // Cinematic systems
private new void Awake()
{
_instance = this;
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Register for post-boot initialization
BootCompletionService.RegisterInitAction(InitializePostBoot);
// Set instance immediately so it's available before OnManagedAwake() is called
_instance = this;
}
private void InitializePostBoot()
protected override void OnManagedAwake()
{
// Initialize any dependencies that require other services to be ready
// For example, subscribe to SceneManagerService events if needed
Logging.Debug("[CinematicsManager] Post-boot initialization complete");
Logging.Debug("[CinematicsManager] Initialized");
}
private void OnEnable()
{

View File

@@ -1,12 +1,12 @@
using Bootstrap;
using Core;
using Core.Lifecycle;
using Input;
using UnityEngine;
using UnityEngine.UI;
namespace Cinematics
{
public class SkipCinematic : MonoBehaviour, ITouchInputConsumer
public class SkipCinematic : ManagedBehaviour, ITouchInputConsumer
{
[Header("Configuration")]
[SerializeField] private float holdDuration = 2.0f;
@@ -17,39 +17,28 @@ namespace Cinematics
private bool _skipPerformed;
private bool _initialized = false;
void Awake()
{
// Register for post-boot initialization
BootCompletionService.RegisterInitAction(InitializePostBoot);
}
public override int ManagedAwakePriority => 180; // Cinematic UI
void Start()
protected override void OnManagedAwake()
{
// Reset the progress bar
if (radialProgressBar != null)
{
radialProgressBar.fillAmount = 0f;
}
}
void OnDisable()
{
// Clean up subscriptions regardless of initialization state
UnsubscribeFromCinematicsEvents();
}
private void InitializePostBoot()
{
// Safe initialization of manager dependencies after boot is complete
if (_initialized)
return;
_initialized = true;
// Subscribe to CinematicsManager events now that boot is complete
SubscribeToCinematicsEvents();
Logging.Debug("[SkipCinematic] Post-boot initialization complete");
Logging.Debug("[SkipCinematic] Initialized");
}
protected override void OnDestroy()
{
base.OnDestroy();
// Clean up subscriptions
UnsubscribeFromCinematicsEvents();
}
private void SubscribeToCinematicsEvents()

View File

@@ -2,7 +2,7 @@
using System.Collections.Generic;
using AppleHills.Core.Interfaces;
using AppleHills.Core.Settings;
using Bootstrap;
using Core.Lifecycle;
using Core.Settings;
using Input;
using UnityEngine;
@@ -12,7 +12,7 @@ namespace Core
/// <summary>
/// Singleton manager for global game state and settings. Provides accessors for various gameplay parameters.
/// </summary>
public class GameManager : MonoBehaviour
public class GameManager : ManagedBehaviour
{
// Singleton implementation
private static GameManager _instance;
@@ -34,33 +34,33 @@ namespace Core
public event Action OnGamePaused;
public event Action OnGameResumed;
void Awake()
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 10; // Core infrastructure - runs early
private new void Awake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
_instance = this;
// Create settings providers if it doesn't exist
// Create settings providers - must happen in Awake so other managers can access settings in their ManagedAwake
SettingsProvider.Instance.gameObject.name = "Settings Provider";
DeveloperSettingsProvider.Instance.gameObject.name = "Developer Settings Provider";
// Load all settings synchronously during Awake
// Load all settings synchronously - critical infrastructure for other managers
InitializeSettings();
InitializeDeveloperSettings();
// Register for post-boot initialization
BootCompletionService.RegisterInitAction(InitializePostBoot);
// DontDestroyOnLoad(gameObject);
}
private void Start()
{
// Load verbosity settings early
_settingsLogVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().settingsLogVerbosity;
_managerLogVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().gameManagerLogVerbosity;
}
private void InitializePostBoot()
protected override void OnManagedAwake()
{
// For post-boot correct initialization order
// Settings are already initialized in Awake()
// This is available for future initialization that depends on other managers
}
/// <summary>

View File

@@ -2,7 +2,7 @@
using System.Collections.Generic;
using UnityEngine;
using Interactions;
using Bootstrap;
using Core.Lifecycle;
using Core.SaveLoad;
namespace Core
@@ -11,7 +11,7 @@ namespace Core
/// Central registry for pickups and item slots.
/// Mirrors the singleton pattern used by PuzzleManager.
/// </summary>
public class ItemManager : MonoBehaviour
public class ItemManager : ManagedBehaviour
{
private static ItemManager _instance;
@@ -48,35 +48,32 @@ namespace Core
// Args: first item data, second item data, result item data
public event Action<PickupItemData, PickupItemData, PickupItemData> OnItemsCombined;
void Awake()
public override int ManagedAwakePriority => 75; // Item registry
private new void Awake()
{
_instance = this;
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Register for post-boot initialization
BootCompletionService.RegisterInitAction(InitializePostBoot);
// Set instance immediately so it's available before OnManagedAwake() is called
_instance = this;
}
private void InitializePostBoot()
protected override void OnManagedAwake()
{
// Subscribe to scene load completed so we can clear registrations when scenes change
SceneManagerService.Instance.SceneLoadStarted += OnSceneLoadStarted;
Logging.Debug("[ItemManager] Subscribed to SceneManagerService events");
Logging.Debug("[ItemManager] Initialized");
}
void OnDestroy()
protected override void OnSceneReady()
{
// Unsubscribe from SceneManagerService
if (SceneManagerService.Instance != null)
SceneManagerService.Instance.SceneLoadStarted -= OnSceneLoadStarted;
// Ensure we clean up any subscriptions from registered items when the manager is destroyed
// Replaces SceneLoadStarted subscription for clearing registrations
ClearAllRegistrations();
}
private void OnSceneLoadStarted(string sceneName)
protected override void OnDestroy()
{
// Clear all registrations when a new scene is loaded, so no stale references persist
base.OnDestroy();
// Ensure we clean up any subscriptions from registered items when the manager is destroyed
ClearAllRegistrations();
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 06a2c07342e5422eae1eb613f614ed61
timeCreated: 1762206473

View File

@@ -0,0 +1,48 @@
namespace Core.Lifecycle
{
/// <summary>
/// Defines the different lifecycle phases that can be broadcast by the LifecycleManager.
/// All ManagedBehaviours participate in all lifecycle phases by default.
/// </summary>
public enum LifecyclePhase
{
/// <summary>
/// Called once per component after bootstrap completes.
/// Guaranteed to be called after all bootstrap resources are loaded.
/// For late-registered components, called immediately upon registration.
/// </summary>
ManagedAwake,
/// <summary>
/// Called before a scene is unloaded.
/// Only called for components in the scene being unloaded.
/// </summary>
SceneUnloading,
/// <summary>
/// Called after a scene has finished loading.
/// Only called for components in the scene being loaded.
/// </summary>
SceneReady,
/// <summary>
/// Called before scene unloads to save data via SaveLoadManager.
/// Integrates with existing SaveLoadManager save system.
/// </summary>
SaveRequested,
/// <summary>
/// Called after scene loads to restore data via SaveLoadManager.
/// Integrates with existing SaveLoadManager restore system.
/// </summary>
RestoreRequested,
/// <summary>
/// Called during OnDestroy before component is destroyed.
/// Use for custom cleanup logic.
/// Most cleanup is automatic (managed events, auto-registrations).
/// </summary>
ManagedDestroy
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5f5f0f19f08240d4d9863b6be6a3cf03

View File

@@ -0,0 +1,420 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Core.Lifecycle
{
/// <summary>
/// Central orchestrator for ManagedBehaviour lifecycle events.
/// Singleton that broadcasts lifecycle events in priority-ordered manner.
/// </summary>
public class LifecycleManager : MonoBehaviour
{
#region Singleton
private static LifecycleManager _instance;
/// <summary>
/// Singleton instance of the LifecycleManager.
/// Created by CustomBoot.Initialise() before bootstrap begins.
/// </summary>
public static LifecycleManager Instance => _instance;
/// <summary>
/// Create LifecycleManager instance. Called by CustomBoot.Initialise() before bootstrap begins.
/// </summary>
public static void CreateInstance()
{
if (_instance != null)
{
Debug.LogWarning("[LifecycleManager] Instance already exists");
return;
}
var go = new GameObject("LifecycleManager");
_instance = go.AddComponent<LifecycleManager>();
DontDestroyOnLoad(go);
Debug.Log("[LifecycleManager] Instance created");
}
#endregion
#region Lifecycle Lists
private List<ManagedBehaviour> managedAwakeList = new List<ManagedBehaviour>();
private List<ManagedBehaviour> sceneUnloadingList = new List<ManagedBehaviour>();
private List<ManagedBehaviour> sceneReadyList = new List<ManagedBehaviour>();
private List<ManagedBehaviour> saveRequestedList = new List<ManagedBehaviour>();
private List<ManagedBehaviour> restoreRequestedList = new List<ManagedBehaviour>();
private List<ManagedBehaviour> destroyList = new List<ManagedBehaviour>();
#endregion
#region Tracking Dictionaries
private Dictionary<ManagedBehaviour, string> componentScenes = new Dictionary<ManagedBehaviour, string>();
#endregion
#region State Flags
private bool isBootComplete = false;
private string currentSceneReady = "";
[SerializeField] private bool enableDebugLogging = true;
#endregion
#region Unity Lifecycle
void Awake()
{
// Instance should already be set by CreateInstance() called from CustomBoot
// This Awake is backup in case LifecycleManager was manually added to a scene
if (_instance == null)
{
_instance = this;
DontDestroyOnLoad(gameObject);
LogDebug("LifecycleManager initialized via Awake (fallback)");
}
else if (_instance != this)
{
Debug.LogWarning("[LifecycleManager] Duplicate instance detected. Destroying.");
Destroy(gameObject);
}
}
void OnDestroy()
{
if (_instance == this)
{
_instance = null;
}
}
#endregion
#region Registration
/// <summary>
/// Register a ManagedBehaviour with the lifecycle system.
/// Called automatically from ManagedBehaviour.Awake().
/// All components participate in all lifecycle hooks.
/// </summary>
public void Register(ManagedBehaviour component)
{
if (component == null)
{
Debug.LogWarning("[LifecycleManager] Attempted to register null component");
return;
}
var sceneName = component.gameObject.scene.name;
// Track which scene this component belongs to
componentScenes[component] = sceneName;
// Register for ManagedAwake
if (isBootComplete)
{
// Boot already complete - call OnManagedAwake immediately
LogDebug($"Late registration: Calling OnManagedAwake immediately for {component.gameObject.name}");
try
{
component.InvokeManagedAwake();
HandleAutoRegistrations(component);
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Error in OnManagedAwake for {component.gameObject.name}: {ex}");
}
}
else
{
// Boot not complete yet - add to list for broadcast
InsertSorted(managedAwakeList, component, component.ManagedAwakePriority);
}
// Register for all scene lifecycle hooks
InsertSorted(sceneUnloadingList, component, component.SceneUnloadingPriority);
InsertSorted(sceneReadyList, component, component.SceneReadyPriority);
InsertSorted(saveRequestedList, component, component.SavePriority);
InsertSorted(restoreRequestedList, component, component.RestorePriority);
InsertSorted(destroyList, component, component.DestroyPriority);
// If this scene is already ready, call OnSceneReady immediately
// Check both currentSceneReady AND if the Unity scene is actually loaded
// (during scene loading, components Awake before BroadcastSceneReady is called)
bool sceneIsReady = currentSceneReady == sceneName;
// Also check if this is happening during boot and the scene is the active scene
// This handles components that register during initial scene load
if (!sceneIsReady && isBootComplete && sceneName != "DontDestroyOnLoad")
{
var scene = UnityEngine.SceneManagement.SceneManager.GetSceneByName(sceneName);
sceneIsReady = scene.isLoaded;
}
if (sceneIsReady)
{
LogDebug($"Late registration: Calling OnSceneReady immediately for {component.gameObject.name}");
try
{
component.InvokeSceneReady();
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Error in OnSceneReady for {component.gameObject.name}: {ex}");
}
}
LogDebug($"Registered {component.gameObject.name} (Scene: {sceneName})");
}
/// <summary>
/// Unregister a ManagedBehaviour from the lifecycle system.
/// Called automatically from ManagedBehaviour.OnDestroy().
/// </summary>
public void Unregister(ManagedBehaviour component)
{
if (component == null)
return;
managedAwakeList.Remove(component);
sceneUnloadingList.Remove(component);
sceneReadyList.Remove(component);
saveRequestedList.Remove(component);
restoreRequestedList.Remove(component);
destroyList.Remove(component);
componentScenes.Remove(component);
LogDebug($"Unregistered {component.gameObject.name}");
}
#endregion
#region Broadcast Methods
/// <summary>
/// Called by CustomBoot when boot completes.
/// Broadcasts ManagedAwake to all registered components.
/// </summary>
public void OnBootCompletionTriggered()
{
if (isBootComplete)
return;
LogDebug("=== Boot Completion Triggered ===");
BroadcastManagedAwake();
isBootComplete = true;
}
/// <summary>
/// Broadcast OnManagedAwake to all registered components (priority ordered).
/// </summary>
private void BroadcastManagedAwake()
{
LogDebug($"Broadcasting ManagedAwake to {managedAwakeList.Count} components");
foreach (var component in managedAwakeList)
{
if (component == null) continue;
try
{
component.InvokeManagedAwake();
HandleAutoRegistrations(component);
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Error in OnManagedAwake for {component.gameObject.name}: {ex}");
}
}
// Clear the list - components already initialized
managedAwakeList.Clear();
}
/// <summary>
/// Broadcast OnSceneUnloading to components in the specified scene (reverse priority order).
/// </summary>
public void BroadcastSceneUnloading(string sceneName)
{
LogDebug($"Broadcasting SceneUnloading for scene: {sceneName}");
// Iterate backwards (high priority → low priority)
for (int i = sceneUnloadingList.Count - 1; i >= 0; i--)
{
var component = sceneUnloadingList[i];
if (component == null) continue;
if (componentScenes.TryGetValue(component, out string compScene) && compScene == sceneName)
{
try
{
component.InvokeSceneUnloading();
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Error in OnSceneUnloading for {component.gameObject.name}: {ex}");
}
}
}
}
/// <summary>
/// Broadcast OnSaveRequested to components in the specified scene (reverse priority order).
/// </summary>
public void BroadcastSaveRequested(string sceneName)
{
LogDebug($"Broadcasting SaveRequested for scene: {sceneName}");
// Iterate backwards (high priority → low priority)
for (int i = saveRequestedList.Count - 1; i >= 0; i--)
{
var component = saveRequestedList[i];
if (component == null) continue;
if (componentScenes.TryGetValue(component, out string compScene) && compScene == sceneName)
{
try
{
component.InvokeSaveRequested();
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Error in OnSaveRequested for {component.gameObject.name}: {ex}");
}
}
}
}
/// <summary>
/// Broadcast OnSceneReady to components in the specified scene (priority order).
/// </summary>
public void BroadcastSceneReady(string sceneName)
{
LogDebug($"Broadcasting SceneReady for scene: {sceneName}");
currentSceneReady = sceneName;
foreach (var component in sceneReadyList)
{
if (component == null) continue;
if (componentScenes.TryGetValue(component, out string compScene) && compScene == sceneName)
{
try
{
component.InvokeSceneReady();
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Error in OnSceneReady for {component.gameObject.name}: {ex}");
}
}
}
}
/// <summary>
/// Broadcast OnRestoreRequested to components in the specified scene (priority order).
/// </summary>
public void BroadcastRestoreRequested(string sceneName)
{
LogDebug($"Broadcasting RestoreRequested for scene: {sceneName}");
foreach (var component in restoreRequestedList)
{
if (component == null) continue;
if (componentScenes.TryGetValue(component, out string compScene) && compScene == sceneName)
{
try
{
component.InvokeRestoreRequested();
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Error in OnRestoreRequested for {component.gameObject.name}: {ex}");
}
}
}
}
#endregion
#region Auto-Registration
/// <summary>
/// Handle automatic registration with GameManager.
/// </summary>
private void HandleAutoRegistrations(ManagedBehaviour component)
{
// Auto-register IPausable
if (component.AutoRegisterPausable && component is AppleHills.Core.Interfaces.IPausable pausable)
{
if (GameManager.Instance != null)
{
GameManager.Instance.RegisterPausableComponent(pausable);
LogDebug($"Auto-registered IPausable: {component.gameObject.name}");
}
}
}
#endregion
#region Helper Methods
/// <summary>
/// Insert component into list maintaining sorted order by priority.
/// Uses binary search for efficient insertion.
/// </summary>
private void InsertSorted(List<ManagedBehaviour> list, ManagedBehaviour component, int priority)
{
// Simple linear insertion for now (can optimize with binary search later if needed)
int index = 0;
for (int i = 0; i < list.Count; i++)
{
int existingPriority = GetPriorityForList(list[i], list);
if (priority < existingPriority)
{
index = i;
break;
}
index = i + 1;
}
list.Insert(index, component);
}
/// <summary>
/// Get the priority value for a component based on which list it's in.
/// </summary>
private int GetPriorityForList(ManagedBehaviour component, List<ManagedBehaviour> list)
{
if (list == managedAwakeList) return component.ManagedAwakePriority;
if (list == sceneUnloadingList) return component.SceneUnloadingPriority;
if (list == sceneReadyList) return component.SceneReadyPriority;
if (list == saveRequestedList) return component.SavePriority;
if (list == restoreRequestedList) return component.RestorePriority;
if (list == destroyList) return component.DestroyPriority;
return 100;
}
/// <summary>
/// Log debug message if debug logging is enabled.
/// </summary>
private void LogDebug(string message)
{
if (enableDebugLogging)
{
Debug.Log($"[LifecycleManager] {message}");
}
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: db6d4743867a3a44381d511cea39218d

View File

@@ -0,0 +1,224 @@
using System;
using UnityEngine;
namespace Core.Lifecycle
{
/// <summary>
/// Base class for all managed behaviours with deterministic lifecycle hooks.
/// Automatically registers with LifecycleManager and provides ordered lifecycle callbacks.
/// </summary>
public abstract class ManagedBehaviour : MonoBehaviour
{
#region Priority Properties
/// <summary>
/// Priority for OnManagedAwake (lower values execute first).
/// Default: 100
/// </summary>
public virtual int ManagedAwakePriority => 100;
/// <summary>
/// Priority for OnSceneUnloading (executed in reverse: higher values execute first).
/// Default: 100
/// </summary>
public virtual int SceneUnloadingPriority => 100;
/// <summary>
/// Priority for OnSceneReady (lower values execute first).
/// Default: 100
/// </summary>
public virtual int SceneReadyPriority => 100;
/// <summary>
/// Priority for OnSaveRequested (executed in reverse: higher values execute first).
/// Default: 100
/// </summary>
public virtual int SavePriority => 100;
/// <summary>
/// Priority for OnRestoreRequested (lower values execute first).
/// Default: 100
/// </summary>
public virtual int RestorePriority => 100;
/// <summary>
/// Priority for OnManagedDestroy (executed in reverse: higher values execute first).
/// Default: 100
/// </summary>
public virtual int DestroyPriority => 100;
#endregion
#region Configuration Properties
/// <summary>
/// If true and component implements IPausable, automatically registers with GameManager.
/// Default: false
/// </summary>
public virtual bool AutoRegisterPausable => false;
#endregion
#region Public Accessors (for LifecycleManager)
// Public wrappers to invoke protected lifecycle methods
public void InvokeManagedAwake() => OnManagedAwake();
public void InvokeSceneUnloading() => OnSceneUnloading();
public void InvokeSceneReady() => OnSceneReady();
public void InvokeSaveRequested() => OnSaveRequested();
public void InvokeRestoreRequested() => OnRestoreRequested();
public void InvokeManagedDestroy() => OnManagedDestroy();
#endregion
#region Private Fields
private ManagedEventManager _eventManager;
private bool _isRegistered;
#endregion
#region Unity Lifecycle
/// <summary>
/// Unity Awake - automatically registers with LifecycleManager.
/// IMPORTANT: Derived classes that override Awake MUST call base.Awake()
/// </summary>
protected virtual void Awake()
{
_eventManager = new ManagedEventManager();
if (LifecycleManager.Instance != null)
{
LifecycleManager.Instance.Register(this);
_isRegistered = true;
}
else
{
Debug.LogWarning($"[ManagedBehaviour] LifecycleManager not found for {gameObject.name}. Component will not receive lifecycle callbacks.");
}
}
/// <summary>
/// Unity OnDestroy - automatically unregisters and cleans up.
/// IMPORTANT: Derived classes that override OnDestroy MUST call base.OnDestroy()
/// </summary>
protected virtual void OnDestroy()
{
if (!_isRegistered)
return;
// Unregister from LifecycleManager
if (LifecycleManager.Instance != null)
{
LifecycleManager.Instance.Unregister(this);
}
// Auto-cleanup managed events
_eventManager?.UnregisterAllEvents();
// Auto-unregister from GameManager if auto-registered
if (AutoRegisterPausable && this is AppleHills.Core.Interfaces.IPausable pausable)
{
GameManager.Instance?.UnregisterPausableComponent(pausable);
}
_isRegistered = false;
}
#endregion
#region Managed Lifecycle Hooks
/// <summary>
/// Called once per component after bootstrap completes.
/// GUARANTEE: Bootstrap resources are available, all managers are initialized.
/// For boot-time components: Called during LifecycleManager.BroadcastManagedAwake (priority ordered).
/// For late-registered components: Called immediately upon registration (bootstrap already complete).
/// Replaces the old Awake + InitializePostBoot pattern.
/// </summary>
protected virtual void OnManagedAwake()
{
// Override in derived classes
}
/// <summary>
/// Called before the scene this component belongs to is unloaded.
/// Called in REVERSE priority order (higher values execute first).
/// Use for scene-specific cleanup.
/// </summary>
protected virtual void OnSceneUnloading()
{
// Override in derived classes
}
/// <summary>
/// Called after the scene this component belongs to has finished loading.
/// Called in priority order (lower values execute first).
/// Use for scene-specific initialization.
/// </summary>
protected virtual void OnSceneReady()
{
// Override in derived classes
}
/// <summary>
/// Called before scene unloads to save data via SaveLoadManager.
/// Called in REVERSE priority order (higher values execute first).
/// Integrates with existing SaveLoadManager save system.
/// Return serialized state string (e.g., JsonUtility.ToJson(myData)).
/// </summary>
protected virtual void OnSaveRequested()
{
// Override in derived classes
}
/// <summary>
/// Called after scene loads to restore data via SaveLoadManager.
/// Called in priority order (lower values execute first).
/// Integrates with existing SaveLoadManager restore system.
/// Receives serialized state string to restore from.
/// </summary>
protected virtual void OnRestoreRequested()
{
// Override in derived classes
}
/// <summary>
/// Called during OnDestroy before component is destroyed.
/// Called in REVERSE priority order (higher values execute first).
/// NOTE: Most cleanup is automatic (managed events, auto-registrations).
/// Only override if you need custom cleanup logic.
/// </summary>
protected virtual void OnManagedDestroy()
{
// Override in derived classes
}
#endregion
#region Helper Methods
/// <summary>
/// Register an event subscription for automatic cleanup on destroy.
/// Prevents memory leaks by ensuring the event is unsubscribed when this component is destroyed.
/// </summary>
/// <param name="target">The object that owns the event</param>
/// <param name="eventName">Name of the event (e.g., "SceneLoadCompleted")</param>
/// <param name="handler">The delegate/handler for the event</param>
protected void RegisterManagedEvent(object target, string eventName, Delegate handler)
{
if (_eventManager == null)
{
Debug.LogWarning($"[ManagedBehaviour] Event manager not initialized for {gameObject.name}");
return;
}
_eventManager.RegisterEvent(target, eventName, handler);
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: af776ef1493d6e543aa3cbe2601f4ef2

View File

@@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
namespace Core.Lifecycle
{
/// <summary>
/// Stores information about a single event subscription for automatic cleanup.
/// </summary>
internal class EventSubscriptionInfo
{
public object Target { get; set; }
public Delegate Handler { get; set; }
public string EventName { get; set; }
}
/// <summary>
/// Manages event subscriptions for a ManagedBehaviour with automatic cleanup on destroy.
/// Prevents memory leaks by ensuring all event subscriptions are properly unsubscribed.
/// </summary>
public class ManagedEventManager
{
private readonly List<EventSubscriptionInfo> _subscriptions = new List<EventSubscriptionInfo>();
/// <summary>
/// Register an event subscription for automatic cleanup.
/// </summary>
/// <param name="target">The object that owns the event</param>
/// <param name="eventName">Name of the event (e.g., "OnSomethingHappened")</param>
/// <param name="handler">The delegate/handler for the event</param>
public void RegisterEvent(object target, string eventName, Delegate handler)
{
if (target == null)
{
Debug.LogWarning("[ManagedEventManager] Cannot register event on null target");
return;
}
if (string.IsNullOrEmpty(eventName))
{
Debug.LogWarning("[ManagedEventManager] Event name cannot be null or empty");
return;
}
if (handler == null)
{
Debug.LogWarning("[ManagedEventManager] Handler cannot be null");
return;
}
_subscriptions.Add(new EventSubscriptionInfo
{
Target = target,
EventName = eventName,
Handler = handler
});
}
/// <summary>
/// Unregister all event subscriptions. Called automatically on ManagedBehaviour destruction.
/// </summary>
public void UnregisterAllEvents()
{
foreach (var subscription in _subscriptions)
{
try
{
if (subscription.Target == null)
continue;
// Use reflection to get the event and unsubscribe
var eventInfo = subscription.Target.GetType().GetEvent(
subscription.EventName,
BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static
);
if (eventInfo != null)
{
eventInfo.RemoveEventHandler(subscription.Target, subscription.Handler);
}
else
{
Debug.LogWarning($"[ManagedEventManager] Could not find event '{subscription.EventName}' on type '{subscription.Target.GetType().Name}'");
}
}
catch (Exception ex)
{
Debug.LogError($"[ManagedEventManager] Error unsubscribing from event '{subscription.EventName}': {ex.Message}");
}
}
_subscriptions.Clear();
}
/// <summary>
/// Get the count of registered event subscriptions (for debugging).
/// </summary>
public int SubscriptionCount => _subscriptions.Count;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 63e107279fdbf1542a9d93d57e60285c

View File

@@ -1,7 +1,7 @@
using UnityEngine;
using AppleHills.Data.CardSystem;
using Cinematics;
using Core;
using Core.Lifecycle;
using Data.CardSystem;
using Input;
using PuzzleS;
@@ -12,7 +12,7 @@ namespace AppleHills.Core
/// Provides quick access to frequently used game objects, components, and manager instances.
/// References are cached for performance and automatically invalidated on scene changes.
/// </summary>
public class QuickAccess : MonoBehaviour
public class QuickAccess : ManagedBehaviour
{
#region Singleton Setup
private static QuickAccess _instance;
@@ -24,6 +24,9 @@ namespace AppleHills.Core
#endregion Singleton Setup
// Very early initialization - QuickAccess should be available immediately
public override int ManagedAwakePriority => 5;
#region Manager Instances
// Core Managers
@@ -46,7 +49,6 @@ namespace AppleHills.Core
private PlayerTouchController _playerController;
private FollowerController _followerController;
private Camera _mainCamera;
private bool _initialized = false;
/// <summary>
/// Returns the player GameObject. Finds it if not already cached.
@@ -125,31 +127,31 @@ namespace AppleHills.Core
#endregion
#region Initialization and Scene Management
#region Lifecycle Methods
private void Awake()
private new void Awake()
{
_instance = this;
base.Awake(); // CRITICAL: Register with LifecycleManager!
if (!_initialized)
{
// Subscribe to scene changes
if (SceneManager != null)
{
SceneManager.SceneLoadCompleted += OnSceneLoadCompleted;
}
_initialized = true;
}
// Set instance immediately so it's available before OnManagedAwake() is called
_instance = this;
}
/// <summary>
/// Handle scene changes by clearing cached references.
/// </summary>
private void OnSceneLoadCompleted(string sceneName)
protected override void OnManagedAwake()
{
// QuickAccess has minimal initialization
}
protected override void OnSceneUnloading()
{
// Clear references BEFORE scene unloads for better cleanup timing
ClearReferences();
}
#endregion
#region Reference Management
/// <summary>
/// Clear all cached references.
/// </summary>

View File

@@ -1,6 +1,5 @@
using UnityEngine;
using Pixelplacement;
using Bootstrap;
namespace Core.SaveLoad
{
@@ -82,18 +81,15 @@ namespace Core.SaveLoad
private void Start()
{
// Register with save system (no validation needed - we auto-generate ID)
BootCompletionService.RegisterInitAction(() =>
// Direct registration - SaveLoadManager guaranteed available (priority 25)
if (SaveLoadManager.Instance != null)
{
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.RegisterParticipant(this);
}
else
{
Debug.LogWarning($"[SaveableStateMachine] SaveLoadManager.Instance is null, cannot register '{name}'", this);
}
});
SaveLoadManager.Instance.RegisterParticipant(this);
}
else
{
Debug.LogWarning($"[AppleMachine] SaveLoadManager not available for '{name}'", this);
}
}
#if UNITY_EDITOR

View File

@@ -4,20 +4,20 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using AppleHills.Core.Settings;
using Bootstrap;
using Core.Lifecycle;
using UnityEngine;
namespace Core.SaveLoad
{
/// <summary>
/// Save/Load manager that follows the project's bootstrap pattern.
/// Save/Load manager that follows the project's lifecycle pattern.
/// - Singleton instance
/// - Registers a post-boot init action with BootCompletionService
/// - Inherits from ManagedBehaviour for lifecycle integration
/// - Manages participant registration for save/load operations
/// - Exposes simple async Save/Load methods
/// - Fires events on completion
/// </summary>
public class SaveLoadManager : MonoBehaviour
public class SaveLoadManager : ManagedBehaviour
{
private static SaveLoadManager _instance;
public static SaveLoadManager Instance => _instance;
@@ -43,24 +43,49 @@ namespace Core.SaveLoad
public event Action<string> OnLoadCompleted;
public event Action OnParticipantStatesRestored;
void Awake()
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 20; // After GameManager and SceneManagerService
private new void Awake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
_instance = this;
// Initialize critical state immediately
IsSaveDataLoaded = false;
IsRestoringState = false;
BootCompletionService.RegisterInitAction(InitializePostBoot);
}
private void Start()
protected override void OnManagedAwake()
{
Logging.Debug("[SaveLoadManager] Initialized");
#if UNITY_EDITOR
OnSceneLoadCompleted("RestoreInEditor");
DiscoverInactiveSaveables("RestoreInEditor");
#endif
// Load save data if save system is enabled (depends on settings from GameManager)
if (DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().useSaveLoadSystem)
{
Load();
}
}
protected override void OnSceneReady()
{
// Discover and register inactive SaveableInteractables in the newly loaded scene
string sceneName = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name;
DiscoverInactiveSaveables(sceneName);
}
protected override void OnSaveRequested()
{
// Scene is about to unload - this is now handled by SceneManagerService
// which calls Save() globally before scene transitions
Logging.Debug($"[SaveLoadManager] OnSaveRequested called");
}
private void OnApplicationQuit()
{
@@ -70,30 +95,14 @@ namespace Core.SaveLoad
}
}
private void InitializePostBoot()
{
Logging.Debug("[SaveLoadManager] Post-boot initialization complete");
// Subscribe to scene lifecycle events if SceneManagerService is available
if (SceneManagerService.Instance != null)
{
SceneManagerService.Instance.SceneLoadCompleted += OnSceneLoadCompleted;
SceneManagerService.Instance.SceneUnloadStarted += OnSceneUnloadStarted;
Logging.Debug("[SaveLoadManager] Subscribed to SceneManagerService events");
}
}
// ...existing code...
void OnDestroy()
protected override void OnDestroy()
{
base.OnDestroy(); // Important: call base to unregister from LifecycleManager
if (_instance == this)
{
// Unsubscribe from scene events
if (SceneManagerService.Instance != null)
{
SceneManagerService.Instance.SceneLoadCompleted -= OnSceneLoadCompleted;
SceneManagerService.Instance.SceneUnloadStarted -= OnSceneUnloadStarted;
}
_instance = null;
}
}
@@ -175,7 +184,11 @@ namespace Core.SaveLoad
#region Scene Lifecycle
private void OnSceneLoadCompleted(string sceneName)
/// <summary>
/// Discovers and registers inactive SaveableInteractables in the scene.
/// Active SaveableInteractables register themselves via their Start() method.
/// </summary>
private void DiscoverInactiveSaveables(string sceneName)
{
Logging.Debug($"[SaveLoadManager] Scene '{sceneName}' loaded. Discovering inactive SaveableInteractables...");
@@ -201,13 +214,6 @@ namespace Core.SaveLoad
Logging.Debug($"[SaveLoadManager] Discovered and registered {registeredCount} inactive SaveableInteractables");
}
private void OnSceneUnloadStarted(string sceneName)
{
Logging.Debug($"[SaveLoadManager] Scene '{sceneName}' unloading. Note: Participants should unregister themselves.");
// We don't force-clear here because participants should manage their own lifecycle
// This allows for proper cleanup in OnDestroy
}
#endregion

View File

@@ -2,17 +2,18 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using AppleHills.Core.Settings;
using Core.Lifecycle;
using Core.SaveLoad;
using UI;
using UnityEngine;
using UnityEngine.SceneManagement;
using Bootstrap;
namespace Core
{
/// <summary>
/// Singleton service for loading and unloading Unity scenes asynchronously, with events for progress and completion.
/// </summary>
public class SceneManagerService : MonoBehaviour
public class SceneManagerService : ManagedBehaviour
{
private LoadingScreenController _loadingScreen;
private static SceneManagerService _instance;
@@ -23,29 +24,39 @@ namespace Core
public static SceneManagerService Instance => _instance;
// Events for scene lifecycle
// NOTE: Most components should use lifecycle hooks (OnSceneReady, OnSceneUnloading)
// instead of subscribing to these events. Events are primarily for orchestration.
/// <summary>
/// Fired when a scene starts loading. Used by loading screen orchestration.
/// </summary>
public event Action<string> SceneLoadStarted;
public event Action<string, float> SceneLoadProgress;
/// <summary>
/// Fired when a scene finishes loading.
/// Used by loading screen orchestration and cross-scene components (e.g., PauseMenu).
/// For component initialization, use OnSceneReady() lifecycle hook instead.
/// </summary>
public event Action<string> SceneLoadCompleted;
public event Action<string> SceneUnloadStarted;
public event Action<string, float> SceneUnloadProgress;
public event Action<string> SceneUnloadCompleted;
private readonly Dictionary<string, AsyncOperation> _activeLoads = new();
private readonly Dictionary<string, AsyncOperation> _activeUnloads = new();
private LogVerbosity _logVerbosity = LogVerbosity.Debug;
private const string BootstrapSceneName = "BootstrapScene";
void Awake()
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 15; // Core infrastructure, after GameManager
private new void Awake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
_instance = this;
// DontDestroyOnLoad(gameObject);
// Initialize current scene tracking immediately in Awake
// Initialize current scene tracking - critical for scene management
InitializeCurrentSceneTracking();
// Register for post-boot initialization
BootCompletionService.RegisterInitAction(InitializePostBoot);
// Ensure BootstrapScene is loaded at startup
var bootstrap = SceneManager.GetSceneByName(BootstrapSceneName);
if (!bootstrap.isLoaded)
@@ -54,9 +65,17 @@ namespace Core
}
}
private void Start()
protected override void OnManagedAwake()
{
// Set up loading screen reference and events
// This must happen in ManagedAwake because LoadingScreenController instance needs to be set first
_loadingScreen = LoadingScreenController.Instance;
SetupLoadingScreenEvents();
// Load verbosity settings
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().sceneLogVerbosity;
LogDebugMessage($"SceneManagerService initialized, current scene is: {CurrentGameplayScene}");
}
/// <summary>
@@ -89,17 +108,6 @@ namespace Core
LogDebugMessage($"No valid active scene, defaulting to: {CurrentGameplayScene}");
}
}
private void InitializePostBoot()
{
// Set up loading screen reference and events after boot is complete
_loadingScreen = LoadingScreenController.Instance;
// Set up loading screen event handlers if available
SetupLoadingScreenEvents();
LogDebugMessage($"Post-boot initialization complete, current scene is: {CurrentGameplayScene}");
}
private void SetupLoadingScreenEvents()
{
@@ -122,7 +130,6 @@ namespace Core
while (!op.isDone)
{
progress?.Report(op.progress);
SceneLoadProgress?.Invoke(sceneName, op.progress);
await Task.Yield();
}
_activeLoads.Remove(sceneName);
@@ -142,17 +149,15 @@ namespace Core
Logging.Warning($"[SceneManagerService] Attempted to unload scene '{sceneName}', but it is not loaded.");
return;
}
SceneUnloadStarted?.Invoke(sceneName);
var op = SceneManager.UnloadSceneAsync(sceneName);
_activeUnloads[sceneName] = op;
while (!op.isDone)
{
progress?.Report(op.progress);
SceneUnloadProgress?.Invoke(sceneName, op.progress);
await Task.Yield();
}
_activeUnloads.Remove(sceneName);
SceneUnloadCompleted?.Invoke(sceneName);
}
/// <summary>
@@ -230,7 +235,6 @@ namespace Core
var op = SceneManager.UnloadSceneAsync(name);
_activeUnloads[name] = op;
ops.Add(op);
SceneUnloadStarted?.Invoke(name);
}
while (done < total)
@@ -251,7 +255,6 @@ namespace Core
foreach (var name in sceneNames)
{
_activeUnloads.Remove(name);
SceneUnloadCompleted?.Invoke(name);
}
// Hide loading screen after all scenes are unloaded
@@ -293,13 +296,35 @@ namespace Core
/// <param name="autoHideLoadingScreen">Whether to automatically hide the loading screen when complete. If false, caller must hide it manually.</param>
public async Task SwitchSceneAsync(string newSceneName, IProgress<float> progress = null, bool autoHideLoadingScreen = true)
{
// Show loading screen at the start (whether using auto-hide or not)
if (_loadingScreen != null && !_loadingScreen.IsActive)
string oldSceneName = CurrentGameplayScene;
// PHASE 1: Show loading screen at the start
// Use explicit progress provider to combine unload + load progress
if (_loadingScreen != null)
{
_loadingScreen.ShowLoadingScreen();
_loadingScreen.ShowLoadingScreen(() => GetAggregateLoadProgress());
}
// Remove all AstarPath (A* Pathfinder) singletons before loading the new scene
// PHASE 2: Broadcast scene unloading - notify components to cleanup
LogDebugMessage($"Broadcasting OnSceneUnloading for: {oldSceneName}");
LifecycleManager.Instance?.BroadcastSceneUnloading(oldSceneName);
// PHASE 3: Broadcast save request - components save their level-specific data
LogDebugMessage($"Broadcasting OnSaveRequested for: {oldSceneName}");
LifecycleManager.Instance?.BroadcastSaveRequested(oldSceneName);
// PHASE 4: Trigger global save if save system is enabled
if (SaveLoadManager.Instance != null)
{
var debugSettings = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>();
if (debugSettings.useSaveLoadSystem)
{
LogDebugMessage("Saving global game state");
SaveLoadManager.Instance.Save();
}
}
// PHASE 5: Remove all AstarPath (A* Pathfinder) singletons before loading the new scene
var astarPaths = FindObjectsByType<AstarPath>(FindObjectsSortMode.None);
foreach (var astar in astarPaths)
{
@@ -308,31 +333,41 @@ namespace Core
else
DestroyImmediate(astar.gameObject);
}
// Unload previous gameplay scene (if not BootstrapScene and not same as new)
if (!string.IsNullOrEmpty(CurrentGameplayScene)&& CurrentGameplayScene != BootstrapSceneName)
// PHASE 6: Unload previous gameplay scene (Unity will call OnDestroy → OnManagedDestroy)
if (!string.IsNullOrEmpty(oldSceneName) && oldSceneName != BootstrapSceneName)
{
var prevScene = SceneManager.GetSceneByName(CurrentGameplayScene);
var prevScene = SceneManager.GetSceneByName(oldSceneName);
if (prevScene.isLoaded)
{
await UnloadSceneAsync(CurrentGameplayScene);
await UnloadSceneAsync(oldSceneName);
}
else
{
Logging.Warning($"[SceneManagerService] Previous scene '{CurrentGameplayScene}' is not loaded, skipping unload.");
Logging.Warning($"[SceneManagerService] Previous scene '{oldSceneName}' is not loaded, skipping unload.");
}
}
// Ensure BootstrapScene is loaded before loading new scene
// PHASE 7: Ensure BootstrapScene is loaded before loading new scene
var bootstrap = SceneManager.GetSceneByName(BootstrapSceneName);
if (!bootstrap.isLoaded)
{
SceneManager.LoadScene(BootstrapSceneName, LoadSceneMode.Additive);
}
// Load new gameplay scene
// PHASE 8: Load new gameplay scene
await LoadSceneAsync(newSceneName, progress);
// Update tracker
CurrentGameplayScene = newSceneName;
// Only hide the loading screen if autoHideLoadingScreen is true
// PHASE 9: Broadcast scene ready - components can now initialize scene-specific state
LogDebugMessage($"Broadcasting OnSceneReady for: {newSceneName}");
LifecycleManager.Instance?.BroadcastSceneReady(newSceneName);
// PHASE 10: Broadcast restore request - components restore their level-specific data
LogDebugMessage($"Broadcasting OnRestoreRequested for: {newSceneName}");
LifecycleManager.Instance?.BroadcastRestoreRequested(newSceneName);
// PHASE 11: Only hide the loading screen if autoHideLoadingScreen is true
if (autoHideLoadingScreen && _loadingScreen != null)
{
_loadingScreen.HideLoadingScreen();

View File

@@ -1,15 +1,13 @@
using System;
using System.Collections;
using System.Collections;
using AppleHills.Core.Settings;
using Bootstrap;
using Input;
using Core.Lifecycle;
using Settings;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Core
{
public class SceneOrientationEnforcer : MonoBehaviour
public class SceneOrientationEnforcer : ManagedBehaviour
{
// Singleton instance
private static SceneOrientationEnforcer _instance;
@@ -20,12 +18,23 @@ namespace Core
public GameObject orientationPromptPrefab;
private LogVerbosity _logVerbosity = LogVerbosity.Warning;
void Awake()
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 70; // Platform-specific utility
private new void Awake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
_instance = this;
// Register for post-boot initialization
BootCompletionService.RegisterInitAction(InitializePostBoot);
// Load verbosity settings early (GameManager sets up settings in its Awake)
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().sceneLogVerbosity;
LogDebugMessage("Initialized");
// Subscribe to sceneLoaded event immediately
SceneManager.sceneLoaded += OnSceneLoaded;
#if UNITY_EDITOR
// When playing in the editor, manually invoke OnSceneLoaded for the currently active scene
@@ -36,20 +45,11 @@ namespace Core
#endif
}
private void Start()
protected override void OnManagedAwake()
{
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().sceneLogVerbosity;
// Initialization already done in Awake
}
private void InitializePostBoot()
{
// Initialize any dependencies that require other services to be ready
LogDebugMessage("Post-boot initialization complete");
// Subscribe to sceneLoaded event
SceneManager.sceneLoaded += OnSceneLoaded;
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
// Determine desired orientation for this scene
@@ -91,8 +91,9 @@ namespace Core
}
}
void OnDestroy()
protected override void OnDestroy()
{
base.OnDestroy(); // Important: call base
SceneManager.sceneLoaded -= OnSceneLoaded;
}

View File

@@ -2,8 +2,8 @@
using System.Collections.Generic;
using System.Linq;
using AppleHills.Data.CardSystem;
using Bootstrap;
using Core;
using Core.Lifecycle;
using Core.SaveLoad;
using UnityEngine;
#if UNITY_EDITOR
@@ -17,7 +17,7 @@ namespace Data.CardSystem
/// Uses a singleton pattern for global access.
/// Implements ISaveParticipant to integrate with the save/load system.
/// </summary>
public class CardSystemManager : MonoBehaviour, ISaveParticipant
public class CardSystemManager : ManagedBehaviour, ISaveParticipant
{
private static CardSystemManager _instance;
public static CardSystemManager Instance => _instance;
@@ -37,20 +37,22 @@ namespace Data.CardSystem
public event Action<CardData> OnCardRarityUpgraded;
public event Action<int> OnBoosterCountChanged;
private void Awake()
public override int ManagedAwakePriority => 60; // Data systems
private new void Awake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
_instance = this;
// Register for post-boot initialization
BootCompletionService.RegisterInitAction(InitializePostBoot);
}
private void InitializePostBoot()
protected override void OnManagedAwake()
{
// Load card definitions from Addressables, then register with save system
LoadCardDefinitionsFromAddressables();
Logging.Debug("[CardSystemManager] Post-boot initialization complete");
Logging.Debug("[CardSystemManager] Initialized");
}
/// <summary>

View File

@@ -1,8 +1,8 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Bootstrap;
using Core;
using Core.Lifecycle;
using Interactions;
using UnityEngine;
using PuzzleS;
@@ -12,7 +12,7 @@ namespace Dialogue
{
[AddComponentMenu("AppleHills/Dialogue/Dialogue Component")]
[RequireComponent(typeof(AppleAudioSource))]
public class DialogueComponent : MonoBehaviour
public class DialogueComponent : ManagedBehaviour
{
[SerializeField] private RuntimeDialogueGraph dialogueGraph;
@@ -35,7 +35,9 @@ namespace Dialogue
public string CurrentSpeakerName => dialogueGraph?.speakerName;
private void Start()
public override int ManagedAwakePriority => 150; // Dialogue systems
protected override void OnManagedAwake()
{
// Get required components
appleAudioSource = GetComponent<AppleAudioSource>();
@@ -58,11 +60,6 @@ namespace Dialogue
speechBubble.UpdatePromptVisibility(HasAnyLines());
}
BootCompletionService.RegisterInitAction(InitializePostBoot);
}
private void InitializePostBoot()
{
// Register for global events
PuzzleManager.Instance.OnStepCompleted += OnAnyPuzzleStepCompleted;
ItemManager.Instance.OnItemPickedUp += OnAnyItemPickedUp;

View File

@@ -1,12 +1,11 @@
using System;
using System.Collections.Generic; // Added for List<ITouchInputConsumer>
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using UnityEngine.SceneManagement;
using AppleHills.Core.Settings;
using Bootstrap;
using Core; // Added for IInteractionSettings
using Core;
using Core.Lifecycle;
namespace Input
{
@@ -22,7 +21,7 @@ namespace Input
/// Handles input events and dispatches them to the appropriate ITouchInputConsumer.
/// Supports tap and hold/drag logic, with interactable delegation and debug logging.
/// </summary>
public class InputManager : MonoBehaviour
public class InputManager : ManagedBehaviour
{
private const string UiActions = "UI";
private const string GameActions = "PlayerTouch";
@@ -51,33 +50,29 @@ namespace Input
private bool isHoldActive;
private LogVerbosity _logVerbosity = LogVerbosity.Warning;
void Awake()
public override int ManagedAwakePriority => 25; // Input infrastructure
private new void Awake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
_instance = this;
// Register for post-boot initialization
BootCompletionService.RegisterInitAction(InitializePostBoot);
}
private void Start()
{
// Load verbosity settings early
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().inputLogVerbosity;
}
private void InitializePostBoot()
{
// Subscribe to scene load completed events now that boot is complete
SceneManagerService.Instance.SceneLoadCompleted += SwitchInputOnSceneLoaded;
// Initialize settings reference
// Initialize settings reference early (GameManager sets these up in its Awake)
_interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
// Set up PlayerInput component and actions - critical for input to work
playerInput = GetComponent<PlayerInput>();
if (playerInput == null)
{
Debug.LogError("[InputManager] InputManager requires a PlayerInput component attached to the same GameObject.");
return;
}
tapMoveAction = playerInput.actions.FindAction("TapMove", false);
holdMoveAction = playerInput.actions.FindAction("HoldMove", false);
positionAction = playerInput.actions.FindAction("TouchPosition", false);
@@ -90,14 +85,39 @@ namespace Input
holdMoveAction.canceled += OnHoldMoveCanceled;
}
// Initialize input mode for current scene
SwitchInputOnSceneLoaded(SceneManager.GetActiveScene().name);
}
private void OnDestroy()
protected override void OnManagedAwake()
{
// Unsubscribe from SceneManagerService
// Subscribe to scene load events from SceneManagerService
// This must happen in ManagedAwake because SceneManagerService instance needs to be set first
if (SceneManagerService.Instance != null)
SceneManagerService.Instance.SceneLoadCompleted -= SwitchInputOnSceneLoaded;
{
SceneManagerService.Instance.SceneLoadCompleted += OnSceneLoadCompleted;
}
}
/// <summary>
/// Called when any scene finishes loading. Restores input to GameAndUI mode.
/// </summary>
private void OnSceneLoadCompleted(string sceneName)
{
LogDebugMessage($"Scene loaded: {sceneName}, restoring input mode");
SwitchInputOnSceneLoaded(sceneName);
}
protected override void OnDestroy()
{
// Unsubscribe from SceneManagerService events
if (SceneManagerService.Instance != null)
{
SceneManagerService.Instance.SceneLoadCompleted -= OnSceneLoadCompleted;
}
base.OnDestroy();
// Input action cleanup happens automatically
}
private void SwitchInputOnSceneLoaded(string sceneName)

View File

@@ -2,8 +2,8 @@
using Pathfinding;
using AppleHills.Core.Settings;
using Core;
using Core.Lifecycle;
using Core.SaveLoad;
using Bootstrap;
namespace Input
{
@@ -21,7 +21,7 @@ namespace Input
/// Handles player movement in response to tap and hold input events.
/// Supports both direct and pathfinding movement modes, and provides event/callbacks for arrival/cancellation.
/// </summary>
public class PlayerTouchController : MonoBehaviour, ITouchInputConsumer, ISaveParticipant
public class PlayerTouchController : ManagedBehaviour, ITouchInputConsumer, ISaveParticipant
{
// --- Movement State ---
private Vector3 targetPosition;
@@ -70,7 +70,9 @@ namespace Input
// Save system tracking
private bool hasBeenRestored;
void Awake()
public override int ManagedAwakePriority => 100; // Player controller
protected override void OnManagedAwake()
{
aiPath = GetComponent<AIPath>();
artTransform = transform.Find("CharacterArt");
@@ -87,19 +89,12 @@ namespace Input
// Initialize settings reference using GetSettingsObject
_settings = GameManager.GetSettingsObject<IPlayerFollowerSettings>();
// Register for post-boot initialization
BootCompletionService.RegisterInitAction(InitializePostBoot);
}
void Start()
{
// Set default input consumer
InputManager.Instance?.SetDefaultConsumer(this);
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().inputLogVerbosity;
}
private void InitializePostBoot()
{
// Register with save system after boot
// Register with save system
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.RegisterParticipant(this);
@@ -110,9 +105,11 @@ namespace Input
Logging.Warning("[PlayerTouchController] SaveLoadManager not available for registration");
}
}
void OnDestroy()
protected override void OnDestroy()
{
base.OnDestroy();
// Unregister from save system
if (SaveLoadManager.Instance != null)
{

View File

@@ -5,6 +5,7 @@ using System.Collections.Generic;
using UnityEngine.Events;
using System.Threading.Tasks;
using Core;
using Core.Lifecycle;
namespace Interactions
{
@@ -20,7 +21,7 @@ namespace Interactions
/// Base class for interactable objects that can respond to tap input events.
/// Subclasses should override OnCharacterArrived() to implement interaction-specific logic.
/// </summary>
public class InteractableBase : MonoBehaviour, ITouchInputConsumer
public class InteractableBase : ManagedBehaviour, ITouchInputConsumer
{
[Header("Interaction Settings")]
public bool isOneTime;
@@ -33,21 +34,16 @@ namespace Interactions
public UnityEvent characterArrived;
public UnityEvent<bool> interactionComplete;
// Helpers for managing interaction state
private bool _interactionInProgress;
protected PlayerTouchController _playerRef;
protected FollowerController _followerController;
private bool _isActive = true;
private InteractionEventType _currentEventType;
private PlayerTouchController playerRef;
protected FollowerController FollowerController;
private bool isActive = true;
// Action component system
private List<InteractionActionBase> _registeredActions = new List<InteractionActionBase>();
private void Awake()
{
// Subscribe to interactionComplete event
interactionComplete.AddListener(OnInteractionComplete);
}
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 100; // Gameplay base classes
/// <summary>
/// Register an action component with this interactable
@@ -73,14 +69,12 @@ namespace Interactions
/// </summary>
private async Task DispatchEventAsync(InteractionEventType eventType)
{
_currentEventType = eventType;
// Collect all tasks from actions that want to respond
List<Task<bool>> tasks = new List<Task<bool>>();
foreach (var action in _registeredActions)
{
Task<bool> task = action.OnInteractionEvent(eventType, _playerRef, _followerController);
Task<bool> task = action.OnInteractionEvent(eventType, playerRef, FollowerController);
if (task != null)
{
tasks.Add(task);
@@ -97,39 +91,178 @@ namespace Interactions
/// <summary>
/// Handles tap input. Triggers interaction logic.
/// Can be overridden for fully custom interaction logic.
/// </summary>
public void OnTap(Vector2 worldPosition)
public virtual void OnTap(Vector2 worldPosition)
{
if (!_isActive)
// 1. High-level validation
if (!CanBeClicked())
{
Logging.Debug($"[Interactable] Is disabled!");
return;
return; // Silent failure
}
Logging.Debug($"[Interactable] OnTap at {worldPosition} on {gameObject.name}");
// Start the interaction process asynchronously
_ = TryInteractAsync();
_ = StartInteractionFlowAsync();
}
private async Task TryInteractAsync()
/// <summary>
/// Template method that orchestrates the entire interaction flow.
/// </summary>
private async Task StartInteractionFlowAsync()
{
_interactionInProgress = true;
// 2. Find characters
playerRef = FindFirstObjectByType<PlayerTouchController>();
FollowerController = FindFirstObjectByType<FollowerController>();
_playerRef = FindFirstObjectByType<PlayerTouchController>();
_followerController = FindFirstObjectByType<FollowerController>();
// 3. Virtual hook: Setup
OnInteractionStarted();
interactionStarted?.Invoke(_playerRef, _followerController);
// Dispatch the InteractionStarted event to action components
// 4. Fire events
interactionStarted?.Invoke(playerRef, FollowerController);
await DispatchEventAsync(InteractionEventType.InteractionStarted);
// After all InteractionStarted actions complete, proceed to player movement
await StartPlayerMovementAsync();
// 5. Orchestrate character movement
await MoveCharactersAsync();
// 6. Virtual hook: Arrival reaction
OnInteractingCharacterArrived();
// 7. Fire arrival events
characterArrived?.Invoke();
await DispatchEventAsync(InteractionEventType.InteractingCharacterArrived);
// 8. Validation (base + child)
var (canProceed, errorMessage) = ValidateInteraction();
if (!canProceed)
{
if (!string.IsNullOrEmpty(errorMessage))
{
DebugUIMessage.Show(errorMessage, Color.yellow);
}
FinishInteraction(false);
return;
}
// 9. Virtual main logic: Do the thing!
bool success = DoInteraction();
// 10. Finish up
FinishInteraction(success);
}
private async Task StartPlayerMovementAsync()
#region Virtual Lifecycle Methods
/// <summary>
/// High-level clickability check. Called BEFORE interaction starts.
/// Override to add custom high-level validation (is active, on cooldown, etc.)
/// </summary>
/// <returns>True if interaction can start, false for silent rejection</returns>
protected virtual bool CanBeClicked()
{
if (_playerRef == null)
if (!isActive) return false;
// Note: isOneTime and cooldown handled in FinishInteraction
return true;
}
/// <summary>
/// Called after characters are found but before movement starts.
/// Override to perform setup logic.
/// </summary>
protected virtual void OnInteractionStarted()
{
// Default: do nothing
}
/// <summary>
/// Called when the interacting character reaches destination.
/// Override to trigger animations or other arrival reactions.
/// </summary>
protected virtual void OnInteractingCharacterArrived()
{
// Default: do nothing
}
/// <summary>
/// Main interaction logic. OVERRIDE THIS in child classes.
/// </summary>
/// <returns>True if interaction succeeded, false otherwise</returns>
protected virtual bool DoInteraction()
{
Debug.LogWarning($"[Interactable] DoInteraction not implemented for {GetType().Name}");
return false;
}
/// <summary>
/// Called after interaction completes. Override to perform cleanup logic.
/// </summary>
/// <param name="success">Whether the interaction succeeded</param>
protected virtual void OnInteractionFinished(bool success)
{
// Default: do nothing
}
/// <summary>
/// Child-specific validation. Override to add interaction-specific validation.
/// </summary>
/// <returns>Tuple of (canProceed, errorMessage)</returns>
protected virtual (bool canProceed, string errorMessage) CanProceedWithInteraction()
{
return (true, null); // Default: always allow
}
#endregion
#region Validation
/// <summary>
/// Combines base and child validation.
/// </summary>
private (bool, string) ValidateInteraction()
{
// Base validation (always runs)
var (baseValid, baseError) = ValidateInteractionBase();
if (!baseValid)
return (false, baseError);
// Child validation (optional override)
var (childValid, childError) = CanProceedWithInteraction();
if (!childValid)
return (false, childError);
return (true, null);
}
/// <summary>
/// Base validation that always runs. Checks puzzle step locks and common prerequisites.
/// </summary>
private (bool canProceed, string errorMessage) ValidateInteractionBase()
{
// Check if there's an ObjectiveStepBehaviour attached
var step = GetComponent<PuzzleS.ObjectiveStepBehaviour>();
if (step != null && !step.IsStepUnlocked())
{
// Special case: ItemSlots can still be interacted with when locked (to swap items)
if (!(this is ItemSlot))
{
return (false, "This step is locked!");
}
}
return (true, null);
}
#endregion
#region Character Movement Orchestration
/// <summary>
/// Orchestrates character movement based on characterToInteract setting.
/// </summary>
private async Task MoveCharactersAsync()
{
if (playerRef == null)
{
Logging.Debug($"[Interactable] Player character could not be found. Aborting interaction.");
interactionInterrupted.Invoke();
@@ -137,350 +270,222 @@ namespace Interactions
return;
}
// If characterToInteract is None, immediately trigger the characterArrived event
// If characterToInteract is None, skip movement
if (characterToInteract == CharacterToInteract.None)
{
await BroadcastCharacterArrivedAsync();
return;
return; // Continue to arrival
}
// Check for a CharacterMoveToTarget component for Trafalgar (player) or Both
Vector3 stopPoint;
// Move player and optionally follower based on characterToInteract setting
if (characterToInteract == CharacterToInteract.Trafalgar)
{
await MovePlayerAsync();
}
else if (characterToInteract == CharacterToInteract.Pulver || characterToInteract == CharacterToInteract.Both)
{
await MovePlayerAsync(); // Move player to range first
await MoveFollowerAsync(); // Then move follower to interaction point
}
}
/// <summary>
/// Moves the player to the interaction point or custom target.
/// </summary>
private async Task MovePlayerAsync()
{
Vector3 stopPoint = transform.position; // Default to interactable position
bool customTargetFound = false;
// Check for a CharacterMoveToTarget component for Trafalgar or Both
CharacterMoveToTarget[] moveTargets = GetComponentsInChildren<CharacterMoveToTarget>();
foreach (var target in moveTargets)
{
// Target is valid if it matches Trafalgar specifically or is set to Both
if (target.characterType == CharacterToInteract.Trafalgar || target.characterType == CharacterToInteract.Both)
{
stopPoint = target.GetTargetPosition();
customTargetFound = true;
// We need to wait for the player to arrive, so use a TaskCompletionSource
var tcs = new TaskCompletionSource<bool>();
// Use local functions instead of circular lambda references
void OnPlayerArrivedLocal()
{
// First remove both event handlers to prevent memory leaks
if (_playerRef != null)
{
_playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal;
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal;
}
// Then continue with the interaction flow
OnPlayerArrivedAsync().ContinueWith(_ => tcs.TrySetResult(true));
}
void OnPlayerMoveCancelledLocal()
{
// First remove both event handlers to prevent memory leaks
if (_playerRef != null)
{
_playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal;
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal;
}
// Then handle the cancellation
OnPlayerMoveCancelledAsync().ContinueWith(_ => tcs.TrySetResult(false));
}
// Unsubscribe previous handlers (if any)
_playerRef.OnArrivedAtTarget -= OnPlayerArrived;
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelled;
// Subscribe our new handlers
_playerRef.OnArrivedAtTarget += OnPlayerArrivedLocal;
_playerRef.OnMoveToCancelled += OnPlayerMoveCancelledLocal;
// Start the player movement
_playerRef.MoveToAndNotify(stopPoint);
// Await player arrival
await tcs.Task;
return;
break;
}
}
// If no custom target was found, use the default behavior
// If no custom target, use default distance
if (!customTargetFound)
{
// Compute closest point on the interaction radius
Vector3 interactablePos = transform.position;
Vector3 playerPos = _playerRef.transform.position;
Vector3 playerPos = playerRef.transform.position;
float stopDistance = characterToInteract == CharacterToInteract.Pulver
? GameManager.Instance.PlayerStopDistance
: GameManager.Instance.PlayerStopDistanceDirectInteraction;
Vector3 toPlayer = (playerPos - interactablePos).normalized;
stopPoint = interactablePos + toPlayer * stopDistance;
// We need to wait for the player to arrive, so use a TaskCompletionSource
var tcs = new TaskCompletionSource<bool>();
// Use local functions instead of circular lambda references
void OnPlayerArrivedLocal()
{
// First remove both event handlers to prevent memory leaks
if (_playerRef != null)
{
_playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal;
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal;
}
// Then continue with the interaction flow
OnPlayerArrivedAsync().ContinueWith(_ => tcs.TrySetResult(true));
}
void OnPlayerMoveCancelledLocal()
{
// First remove both event handlers to prevent memory leaks
if (_playerRef != null)
{
_playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal;
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal;
}
// Then handle the cancellation
OnPlayerMoveCancelledAsync().ContinueWith(_ => tcs.TrySetResult(false));
}
// Unsubscribe previous handlers (if any)
_playerRef.OnArrivedAtTarget -= OnPlayerArrived;
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelled;
// Subscribe our new handlers
_playerRef.OnArrivedAtTarget += OnPlayerArrivedLocal;
_playerRef.OnMoveToCancelled += OnPlayerMoveCancelledLocal;
// Start the player movement
_playerRef.MoveToAndNotify(stopPoint);
// Await player arrival
await tcs.Task;
}
}
private async Task OnPlayerMoveCancelledAsync()
{
_interactionInProgress = false;
interactionInterrupted?.Invoke();
await DispatchEventAsync(InteractionEventType.InteractionInterrupted);
}
private async Task OnPlayerArrivedAsync()
{
if (!_interactionInProgress)
return;
// Dispatch PlayerArrived event
await DispatchEventAsync(InteractionEventType.PlayerArrived);
// Wait for player to arrive
var tcs = new TaskCompletionSource<bool>();
// After all PlayerArrived actions complete, proceed to character interaction
await HandleCharacterInteractionAsync();
void OnPlayerArrivedLocal()
{
if (playerRef != null)
{
playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal;
playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal;
}
tcs.TrySetResult(true);
}
void OnPlayerMoveCancelledLocal()
{
if (playerRef != null)
{
playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal;
playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal;
}
_ = HandleInteractionCancelledAsync();
tcs.TrySetResult(false);
}
playerRef.OnArrivedAtTarget += OnPlayerArrivedLocal;
playerRef.OnMoveToCancelled += OnPlayerMoveCancelledLocal;
playerRef.MoveToAndNotify(stopPoint);
await tcs.Task;
}
private async Task HandleCharacterInteractionAsync()
/// <summary>
/// Moves the follower to the interaction point or custom target.
/// </summary>
private async Task MoveFollowerAsync()
{
if (characterToInteract == CharacterToInteract.Pulver)
{
// We need to wait for the follower to arrive, so use a TaskCompletionSource
var tcs = new TaskCompletionSource<bool>();
// Create a proper local function for the event handler
void OnFollowerArrivedLocal()
{
// First remove the event handler to prevent memory leaks
if (_followerController != null)
{
_followerController.OnPickupArrived -= OnFollowerArrivedLocal;
}
// Then continue with the interaction flow
OnFollowerArrivedAsync().ContinueWith(_ => tcs.TrySetResult(true));
}
// Register our new local function handler
_followerController.OnPickupArrived += OnFollowerArrivedLocal;
// Check for a CharacterMoveToTarget component for Pulver or Both
Vector3 targetPosition = transform.position;
CharacterMoveToTarget[] moveTargets = GetComponentsInChildren<CharacterMoveToTarget>();
foreach (var target in moveTargets)
{
if (target.characterType == CharacterToInteract.Pulver || target.characterType == CharacterToInteract.Both)
{
targetPosition = target.GetTargetPosition();
break;
}
}
// Use the new GoToPoint method instead of GoToPointAndReturn
_followerController.GoToPoint(targetPosition);
// Await follower arrival
await tcs.Task;
}
else if (characterToInteract == CharacterToInteract.Trafalgar)
{
await BroadcastCharacterArrivedAsync();
}
else if (characterToInteract == CharacterToInteract.Both)
{
// We need to wait for the follower to arrive, so use a TaskCompletionSource
var tcs = new TaskCompletionSource<bool>();
// Create a proper local function for the event handler
void OnFollowerArrivedLocal()
{
// First remove the event handler to prevent memory leaks
if (_followerController != null)
{
_followerController.OnPickupArrived -= OnFollowerArrivedLocal;
}
// Then continue with the interaction flow
OnFollowerArrivedAsync().ContinueWith(_ => tcs.TrySetResult(true));
}
// Register our new local function handler
_followerController.OnPickupArrived += OnFollowerArrivedLocal;
// Check for a CharacterMoveToTarget component for Pulver or Both
Vector3 targetPosition = transform.position;
CharacterMoveToTarget[] moveTargets = GetComponentsInChildren<CharacterMoveToTarget>();
foreach (var target in moveTargets)
{
if (target.characterType == CharacterToInteract.Pulver || target.characterType == CharacterToInteract.Both)
{
targetPosition = target.GetTargetPosition();
break;
}
}
// Use the new GoToPoint method instead of GoToPointAndReturn
_followerController.GoToPoint(targetPosition);
// Await follower arrival
await tcs.Task;
}
}
private async Task OnFollowerArrivedAsync()
{
if (!_interactionInProgress)
if (FollowerController == null)
return;
// Dispatch InteractingCharacterArrived event and WAIT for all actions to complete
// This ensures we wait for any timeline animations to finish before proceeding
Logging.Debug("[Interactable] Follower arrived, dispatching InteractingCharacterArrived event and waiting for completion");
await DispatchEventAsync(InteractionEventType.InteractingCharacterArrived);
Logging.Debug("[Interactable] All InteractingCharacterArrived actions completed, proceeding with interaction");
// Check if we have any components that might have paused the interaction flow
foreach (var action in _registeredActions)
// Check for a CharacterMoveToTarget component for Pulver or Both
Vector3 targetPosition = transform.position;
CharacterMoveToTarget[] moveTargets = GetComponentsInChildren<CharacterMoveToTarget>();
foreach (var target in moveTargets)
{
if (action is InteractionTimelineAction timelineAction &&
timelineAction.respondToEvents.Contains(InteractionEventType.InteractingCharacterArrived) &&
timelineAction.pauseInteractionFlow)
if (target.characterType == CharacterToInteract.Pulver || target.characterType == CharacterToInteract.Both)
{
targetPosition = target.GetTargetPosition();
break;
}
}
// Tell the follower to return to the player
if (_followerController != null && _playerRef != null)
// Wait for follower to arrive
var tcs = new TaskCompletionSource<bool>();
void OnFollowerArrivedLocal()
{
_followerController.ReturnToPlayer(_playerRef.transform);
if (FollowerController != null)
{
FollowerController.OnPickupArrived -= OnFollowerArrivedLocal;
}
// Tell follower to return to player
if (FollowerController != null && playerRef != null)
{
FollowerController.ReturnToPlayer(playerRef.transform);
}
tcs.TrySetResult(true);
}
// After all InteractingCharacterArrived actions complete, proceed to character arrived
await BroadcastCharacterArrivedAsync();
}
// Legacy non-async method to maintain compatibility with existing code
private void OnPlayerArrived()
{
// This is now just a wrapper for the async version
_ = OnPlayerArrivedAsync();
}
// Legacy non-async method to maintain compatibility with existing code
private void OnPlayerMoveCancelled()
{
// This is now just a wrapper for the async version
_ = OnPlayerMoveCancelledAsync();
}
private Task BroadcastCharacterArrivedAsync()
{
// Check for ObjectiveStepBehaviour and lock state
var step = GetComponent<PuzzleS.ObjectiveStepBehaviour>();
var slot = GetComponent<ItemSlot>();
if (step != null && !step.IsStepUnlocked() && slot == null)
{
DebugUIMessage.Show("This step is locked!", Color.yellow);
CompleteInteraction(false);
// Reset variables for next time
_interactionInProgress = false;
_playerRef = null;
_followerController = null;
return Task.CompletedTask;
}
// Dispatch CharacterArrived event
// await DispatchEventAsync(InteractionEventType.InteractingCharacterArrived);
FollowerController.OnPickupArrived += OnFollowerArrivedLocal;
FollowerController.GoToPoint(targetPosition);
// Broadcast appropriate event
characterArrived?.Invoke();
// Call the virtual method for subclasses to override
OnCharacterArrived();
// Reset variables for next time
_interactionInProgress = false;
_playerRef = null;
_followerController = null;
return Task.CompletedTask;
await tcs.Task;
}
/// <summary>
/// Called when the character has arrived at the interaction point.
/// Subclasses should override this to implement interaction-specific logic
/// and call CompleteInteraction(bool success) when done.
/// Handles interaction being cancelled (player stopped moving).
/// </summary>
protected virtual void OnCharacterArrived()
private async Task HandleInteractionCancelledAsync()
{
// Default implementation does nothing - subclasses should override
// and call CompleteInteraction when their logic is complete
interactionInterrupted?.Invoke();
await DispatchEventAsync(InteractionEventType.InteractionInterrupted);
}
private async void OnInteractionComplete(bool success)
#endregion
#region Finalization
/// <summary>
/// Finalizes the interaction after DoInteraction completes.
/// </summary>
private async void FinishInteraction(bool success)
{
// Dispatch InteractionComplete event
// Virtual hook: Cleanup
OnInteractionFinished(success);
// Fire completion events
interactionComplete?.Invoke(success);
await DispatchEventAsync(InteractionEventType.InteractionComplete);
// Handle one-time / cooldown
if (success)
{
if (isOneTime)
{
_isActive = false;
isActive = false;
}
else if (cooldown >= 0f)
{
StartCoroutine(HandleCooldown());
}
}
// Reset state
playerRef = null;
FollowerController = null;
}
private System.Collections.IEnumerator HandleCooldown()
{
_isActive = false;
isActive = false;
yield return new WaitForSeconds(cooldown);
_isActive = true;
isActive = true;
}
#endregion
#region Legacy Methods & Compatibility
/// <summary>
/// DEPRECATED: Override DoInteraction() instead.
/// This method is kept temporarily for backward compatibility during migration.
/// </summary>
[Obsolete("Override DoInteraction() instead")]
protected virtual void OnCharacterArrived()
{
// Default implementation does nothing
// Children should override DoInteraction() in the new pattern
}
/// <summary>
/// Call this from subclasses to mark the interaction as complete.
/// NOTE: In the new pattern, just return true/false from DoInteraction().
/// This is kept for backward compatibility during migration.
/// </summary>
protected void CompleteInteraction(bool success)
{
// For now, this manually triggers completion
// After migration, DoInteraction() return value will replace this
interactionComplete?.Invoke(success);
}
/// <summary>
/// Legacy method for backward compatibility.
/// </summary>
[Obsolete("Use CompleteInteraction instead")]
public void BroadcastInteractionComplete(bool success)
{
CompleteInteraction(success);
}
#endregion
#region ITouchInputConsumer Implementation
public void OnHoldStart(Vector2 position)
{
throw new NotImplementedException();
@@ -495,25 +500,8 @@ namespace Interactions
{
throw new NotImplementedException();
}
/// <summary>
/// Call this from subclasses to mark the interaction as complete.
/// </summary>
/// <param name="success">Whether the interaction was successful</param>
protected void CompleteInteraction(bool success)
{
interactionComplete?.Invoke(success);
}
/// <summary>
/// Legacy method for backward compatibility. Use CompleteInteraction instead.
/// </summary>
/// TODO: Remove this method in future versions
[Obsolete("Use CompleteInteraction instead")]
public void BroadcastInteractionComplete(bool success)
{
CompleteInteraction(success);
}
#endregion
#if UNITY_EDITOR
/// <summary>

View File

@@ -19,32 +19,39 @@ namespace Interactions
/// <summary>
/// Saveable data for ItemSlot state
/// </summary>
[System.Serializable]
[Serializable]
public class ItemSlotSaveData
{
public PickupSaveData pickupData; // Base pickup state
public ItemSlotState slotState; // Current slot validation state
public string slottedItemSaveId; // Save ID of slotted item (if any)
public string slottedItemDataAssetPath; // Asset path to PickupItemData
public ItemSlotState slotState;
public string slottedItemSaveId;
}
// TODO: Remove this ridiculous inheritance from Pickup if possible
/// <summary>
/// Interaction requirement that allows slotting, swapping, or picking up items in a slot.
/// Interaction that allows slotting, swapping, or picking up items in a slot.
/// ItemSlot is a CONTAINER, not a Pickup itself.
/// </summary>
public class ItemSlot : Pickup
public class ItemSlot : SaveableInteractable
{
// Slot visual data (for the slot itself, not the item in it)
public PickupItemData itemData;
public SpriteRenderer iconRenderer;
// Slotted item tracking
private PickupItemData currentlySlottedItemData;
public SpriteRenderer slottedItemRenderer;
private GameObject currentlySlottedItemObject;
// Tracks the current state of the slotted item
private ItemSlotState _currentState = ItemSlotState.None;
private ItemSlotState currentState = ItemSlotState.None;
// Settings reference
private IInteractionSettings _interactionSettings;
private IPlayerFollowerSettings _playerFollowerSettings;
private IInteractionSettings interactionSettings;
private IPlayerFollowerSettings playerFollowerSettings;
/// <summary>
/// Read-only access to the current slotted item state.
/// </summary>
public ItemSlotState CurrentSlottedState => _currentState;
public ItemSlotState CurrentSlottedState => currentState;
public UnityEvent onItemSlotted;
public UnityEvent onItemSlotRemoved;
@@ -62,118 +69,189 @@ namespace Interactions
public UnityEvent onForbiddenItemSlotted;
// Native C# event alternative for code-only subscribers
public event Action<PickupItemData, PickupItemData> OnForbiddenItemSlotted;
private PickupItemData _currentlySlottedItemData;
public SpriteRenderer slottedItemRenderer;
private GameObject _currentlySlottedItemObject;
public GameObject GetSlottedObject()
{
return _currentlySlottedItemObject;
return currentlySlottedItemObject;
}
public void SetSlottedObject(GameObject obj)
{
_currentlySlottedItemObject = obj;
if (_currentlySlottedItemObject != null)
currentlySlottedItemObject = obj;
if (currentlySlottedItemObject != null)
{
_currentlySlottedItemObject.SetActive(false);
currentlySlottedItemObject.SetActive(false);
}
}
protected override void Awake()
{
base.Awake();
base.Awake(); // SaveableInteractable registration
// Setup visuals
if (iconRenderer == null)
iconRenderer = GetComponent<SpriteRenderer>();
ApplyItemData();
// Initialize settings references
_interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
_playerFollowerSettings = GameManager.GetSettingsObject<IPlayerFollowerSettings>();
interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
playerFollowerSettings = GameManager.GetSettingsObject<IPlayerFollowerSettings>();
}
protected override void OnCharacterArrived()
#if UNITY_EDITOR
/// <summary>
/// Unity OnValidate callback. Ensures icon and data are up to date in editor.
/// </summary>
void OnValidate()
{
Logging.Debug("[ItemSlot] OnCharacterArrived");
var heldItemData = _followerController.CurrentlyHeldItemData;
var heldItemObj = _followerController.GetHeldPickupObject();
var config = _interactionSettings?.GetSlotItemConfig(itemData);
var forbidden = config?.forbiddenItems ?? new List<PickupItemData>();
// Held item, slot empty -> try to slot item
if (heldItemData != null && _currentlySlottedItemObject == null)
{
// First check for forbidden items at the very start so we don't continue unnecessarily
if (PickupItemData.ListContainsEquivalent(forbidden, heldItemData))
{
DebugUIMessage.Show("Can't place that here.", Color.red);
onForbiddenItemSlotted?.Invoke();
OnForbiddenItemSlotted?.Invoke(itemData, heldItemData);
_currentState = ItemSlotState.Forbidden;
CompleteInteraction(false);
return;
}
if (iconRenderer == null)
iconRenderer = GetComponent<SpriteRenderer>();
ApplyItemData();
}
#endif
SlotItem(heldItemObj, heldItemData, true);
return;
/// <summary>
/// Applies the item data to the slot (icon, name, etc).
/// </summary>
public void ApplyItemData()
{
if (itemData != null)
{
if (iconRenderer != null && itemData.mapSprite != null)
{
iconRenderer.sprite = itemData.mapSprite;
}
gameObject.name = itemData.itemName + "_Slot";
}
}
#region Interaction Logic
/// <summary>
/// Validation: Check if interaction can proceed based on held item and slot state.
/// </summary>
protected override (bool canProceed, string errorMessage) CanProceedWithInteraction()
{
var heldItem = FollowerController?.CurrentlyHeldItemData;
// Scenario: Nothing held + Empty slot = Error
if (heldItem == null && currentlySlottedItemObject == null)
return (false, "This requires an item.");
// Check forbidden items if trying to slot into empty slot
if (heldItem != null && currentlySlottedItemObject == null)
{
var config = interactionSettings?.GetSlotItemConfig(itemData);
var forbidden = config?.forbiddenItems ?? new List<PickupItemData>();
if (PickupItemData.ListContainsEquivalent(forbidden, heldItem))
return (false, "Can't place that here.");
}
// Either pickup or swap items
if ((heldItemData == null && _currentlySlottedItemObject != null)
|| (heldItemData != null && _currentlySlottedItemObject != null))
return (true, null);
}
/// <summary>
/// Main interaction logic: Slot, pickup, swap, or combine items.
/// Returns true only if correct item was slotted.
/// </summary>
protected override bool DoInteraction()
{
Logging.Debug("[ItemSlot] DoInteraction");
var heldItemData = FollowerController.CurrentlyHeldItemData;
var heldItemObj = FollowerController.GetHeldPickupObject();
// Scenario 1: Held item + Empty slot = Slot it
if (heldItemData != null && currentlySlottedItemObject == null)
{
// If both held and slotted items exist, attempt combination via follower (reuse existing logic from Pickup)
if (heldItemData != null && _currentlySlottedItemData != null)
SlotItem(heldItemObj, heldItemData);
FollowerController.ClearHeldItem(); // Clear follower's hand after slotting
return IsSlottedItemCorrect();
}
// Scenario 2 & 3: Slot is full
if (currentlySlottedItemObject != null)
{
// Try combination if both items present
if (heldItemData != null)
{
var slottedPickup = _currentlySlottedItemObject?.GetComponent<Pickup>();
var slottedPickup = currentlySlottedItemObject.GetComponent<Pickup>();
if (slottedPickup != null)
{
var comboResult = _followerController.TryCombineItems(slottedPickup, out var combinationResultItem);
if (combinationResultItem != null && comboResult == FollowerController.CombinationResult.Successful)
var comboResult = FollowerController.TryCombineItems(slottedPickup, out var combinationResultItem);
if (comboResult == FollowerController.CombinationResult.Successful)
{
// Combination succeeded: fire slot-removed events and clear internals (don't call SlotItem to avoid duplicate events)
onItemSlotRemoved?.Invoke();
OnItemSlotRemoved?.Invoke(_currentlySlottedItemData);
_currentState = ItemSlotState.None;
// Clear internal references and visuals
_currentlySlottedItemObject = null;
_currentlySlottedItemData = null;
UpdateSlottedSprite();
CompleteInteraction(false);
return;
// Combination succeeded - clear slot and return false (not a "slot success")
ClearSlot();
return false;
}
}
}
// No combination (or not applicable) -> perform normal swap/pickup behavior
_followerController.TryPickupItem(_currentlySlottedItemObject, _currentlySlottedItemData, false);
onItemSlotRemoved?.Invoke();
OnItemSlotRemoved?.Invoke(_currentlySlottedItemData);
_currentState = ItemSlotState.None;
SlotItem(heldItemObj, heldItemData, _currentlySlottedItemObject == null);
return;
// No combination or unsuccessful - perform swap
// Step 1: Pickup from slot (follower now holds the old slotted item)
FollowerController.TryPickupItem(currentlySlottedItemObject, currentlySlottedItemData, dropItem: false);
ClearSlot();
// Step 2: If we had a held item, slot it (follower already holding picked up item, don't clear!)
if (heldItemData != null)
{
SlotItem(heldItemObj, heldItemData);
// Don't clear follower - they're holding the item they picked up from the slot
return IsSlottedItemCorrect();
}
// Just picked up from slot - not a success
return false;
}
// No held item, slot empty -> show warning
if (heldItemData == null && _currentlySlottedItemObject == null)
{
DebugUIMessage.Show("This requires an item.", Color.red);
return;
}
// Shouldn't reach here (validation prevents empty + no held)
return false;
}
/// <summary>
/// Helper: Check if the currently slotted item is correct.
/// </summary>
private bool IsSlottedItemCorrect()
{
return currentState == ItemSlotState.Correct;
}
/// <summary>
/// Helper: Clear the slot and fire removal events.
/// </summary>
private void ClearSlot()
{
var previousData = currentlySlottedItemData;
currentlySlottedItemObject = null;
currentlySlottedItemData = null;
currentState = ItemSlotState.None;
UpdateSlottedSprite();
// Fire removal events
onItemSlotRemoved?.Invoke();
OnItemSlotRemoved?.Invoke(previousData);
}
#endregion
#region Visual Updates
/// <summary>
/// Updates the sprite and scale for the currently slotted item.
/// </summary>
private void UpdateSlottedSprite()
{
if (slottedItemRenderer != null && _currentlySlottedItemData != null && _currentlySlottedItemData.mapSprite != null)
if (slottedItemRenderer != null && currentlySlottedItemData != null && currentlySlottedItemData.mapSprite != null)
{
slottedItemRenderer.sprite = _currentlySlottedItemData.mapSprite;
slottedItemRenderer.sprite = currentlySlottedItemData.mapSprite;
// Scale sprite to desired height, preserve aspect ratio, compensate for parent scale
float desiredHeight = _playerFollowerSettings?.HeldIconDisplayHeight ?? 2.0f;
var sprite = _currentlySlottedItemData.mapSprite;
float desiredHeight = playerFollowerSettings?.HeldIconDisplayHeight ?? 2.0f;
var sprite = currentlySlottedItemData.mapSprite;
float spriteHeight = sprite.bounds.size.y;
Vector3 parentScale = slottedItemRenderer.transform.parent != null
? slottedItemRenderer.transform.parent.localScale
@@ -191,18 +269,20 @@ namespace Interactions
}
}
#endregion
// Register with ItemManager when enabled
protected override void Start()
{
base.Start(); // This calls Pickup.Start() which registers with save system
base.Start(); // SaveableInteractable registration
// Additionally register as ItemSlot
// Register as ItemSlot
ItemManager.Instance?.RegisterItemSlot(this);
}
protected override void OnDestroy()
{
base.OnDestroy(); // Unregister from save system and pickup manager
base.OnDestroy(); // SaveableInteractable cleanup
// Unregister from slot manager
ItemManager.Instance?.UnregisterItemSlot(this);
@@ -212,35 +292,22 @@ namespace Interactions
protected override object GetSerializableState()
{
// Get base pickup state
PickupSaveData baseData = base.GetSerializableState() as PickupSaveData;
// Get slotted item save ID if there's a slotted item
string slottedSaveId = "";
string slottedAssetPath = "";
if (_currentlySlottedItemObject != null)
if (currentlySlottedItemObject != null)
{
var slottedPickup = _currentlySlottedItemObject.GetComponent<Pickup>();
var slottedPickup = currentlySlottedItemObject.GetComponent<Pickup>();
if (slottedPickup is SaveableInteractable saveablePickup)
{
slottedSaveId = saveablePickup.GetSaveId();
}
if (_currentlySlottedItemData != null)
{
#if UNITY_EDITOR
slottedAssetPath = UnityEditor.AssetDatabase.GetAssetPath(_currentlySlottedItemData);
#endif
}
}
return new ItemSlotSaveData
{
pickupData = baseData,
slotState = _currentState,
slottedItemSaveId = slottedSaveId,
slottedItemDataAssetPath = slottedAssetPath
slotState = currentState,
slottedItemSaveId = slottedSaveId
};
}
@@ -253,20 +320,13 @@ namespace Interactions
return;
}
// First restore base pickup state
if (data.pickupData != null)
{
string pickupJson = JsonUtility.ToJson(data.pickupData);
base.ApplySerializableState(pickupJson);
}
// Restore slot state
_currentState = data.slotState;
currentState = data.slotState;
// Restore slotted item if there was one
if (!string.IsNullOrEmpty(data.slottedItemSaveId))
{
RestoreSlottedItem(data.slottedItemSaveId, data.slottedItemDataAssetPath);
RestoreSlottedItem(data.slottedItemSaveId);
}
}
@@ -274,7 +334,7 @@ namespace Interactions
/// Restore a slotted item from save data.
/// This is called during load restoration and should NOT trigger events.
/// </summary>
private void RestoreSlottedItem(string slottedItemSaveId, string slottedItemDataAssetPath)
private void RestoreSlottedItem(string slottedItemSaveId)
{
// Try to find the item in the scene by its save ID via ItemManager
GameObject slottedObject = ItemManager.Instance?.FindPickupBySaveId(slottedItemSaveId);
@@ -285,106 +345,92 @@ namespace Interactions
return;
}
// Get the item data
// Get the item data from the pickup component
PickupItemData slottedData = null;
#if UNITY_EDITOR
if (!string.IsNullOrEmpty(slottedItemDataAssetPath))
var pickup = slottedObject.GetComponent<Pickup>();
if (pickup != null)
{
slottedData = UnityEditor.AssetDatabase.LoadAssetAtPath<PickupItemData>(slottedItemDataAssetPath);
}
#endif
if (slottedData == null)
{
var pickup = slottedObject.GetComponent<Pickup>();
if (pickup != null)
{
slottedData = pickup.itemData;
}
slottedData = pickup.itemData;
}
// Silently slot the item (no events, no interaction completion)
// Follower state is managed separately during save/load restoration
ApplySlottedItemState(slottedObject, slottedData, triggerEvents: false);
}
/// <summary>
/// Core logic for slotting an item. Can be used both for normal slotting and silent restoration.
/// NOTE: Does NOT call CompleteInteraction - the template method handles that via DoInteraction return value.
/// NOTE: Does NOT manage follower state - caller is responsible for clearing follower's hand if needed.
/// </summary>
/// <param name="itemToSlot">The item GameObject to slot (or null to clear)</param>
/// <param name="itemToSlotData">The PickupItemData for the item</param>
/// <param name="triggerEvents">Whether to fire events and complete interaction</param>
/// <param name="clearFollowerHeldItem">Whether to clear the follower's held item</param>
private void ApplySlottedItemState(GameObject itemToSlot, PickupItemData itemToSlotData, bool triggerEvents, bool clearFollowerHeldItem = true)
/// <param name="triggerEvents">Whether to fire events</param>
private void ApplySlottedItemState(GameObject itemToSlot, PickupItemData itemToSlotData, bool triggerEvents)
{
// Cache the previous item data before clearing, needed for OnItemSlotRemoved event
var previousItemData = _currentlySlottedItemData;
bool wasSlotCleared = _currentlySlottedItemObject != null && itemToSlot == null;
if (itemToSlot == null)
{
_currentlySlottedItemObject = null;
_currentlySlottedItemData = null;
_currentState = ItemSlotState.None;
// Clear slot
var previousData = currentlySlottedItemData;
currentlySlottedItemObject = null;
currentlySlottedItemData = null;
currentState = ItemSlotState.None;
// Fire native event for slot clearing (only if triggering events)
if (wasSlotCleared && triggerEvents)
if (previousData != null && triggerEvents)
{
OnItemSlotRemoved?.Invoke(previousItemData);
onItemSlotRemoved?.Invoke();
OnItemSlotRemoved?.Invoke(previousData);
}
}
else
{
// Slot the item
itemToSlot.SetActive(false);
itemToSlot.transform.SetParent(null);
SetSlottedObject(itemToSlot);
_currentlySlottedItemData = itemToSlotData;
}
if (clearFollowerHeldItem && _followerController != null)
{
_followerController.ClearHeldItem();
}
UpdateSlottedSprite();
// Only validate and trigger events if requested
if (triggerEvents)
{
// Once an item is slotted, we know it is not forbidden, so we can skip that check, but now check if it was
// the correct item we're looking for
var config = _interactionSettings?.GetSlotItemConfig(itemData);
currentlySlottedItemData = itemToSlotData;
// Determine if correct
var config = interactionSettings?.GetSlotItemConfig(itemData);
var allowed = config?.allowedItems ?? new List<PickupItemData>();
if (itemToSlotData != null && PickupItemData.ListContainsEquivalent(allowed, itemToSlotData))
{
if (itemToSlot != null)
currentState = ItemSlotState.Correct;
// Fire events if requested
if (triggerEvents)
{
DebugUIMessage.Show("You correctly slotted " + itemToSlotData.itemName + " into: " + itemData.itemName, Color.green);
DebugUIMessage.Show($"You correctly slotted {itemToSlotData.itemName} into: {itemData.itemName}", Color.green);
onCorrectItemSlotted?.Invoke();
OnCorrectItemSlotted?.Invoke(itemData, _currentlySlottedItemData);
_currentState = ItemSlotState.Correct;
OnCorrectItemSlotted?.Invoke(itemData, currentlySlottedItemData);
}
CompleteInteraction(true);
}
else
{
if (itemToSlot != null)
currentState = ItemSlotState.Incorrect;
// Fire events if requested
if (triggerEvents)
{
DebugUIMessage.Show("I'm not sure this works.", Color.yellow);
onIncorrectItemSlotted?.Invoke();
OnIncorrectItemSlotted?.Invoke(itemData, _currentlySlottedItemData);
_currentState = ItemSlotState.Incorrect;
OnIncorrectItemSlotted?.Invoke(itemData, currentlySlottedItemData);
}
CompleteInteraction(false);
}
}
UpdateSlottedSprite();
}
/// <summary>
/// Public API for slotting items during gameplay.
/// Caller is responsible for managing follower's held item state.
/// </summary>
public void SlotItem(GameObject itemToSlot, PickupItemData itemToSlotData, bool clearFollowerHeldItem = true)
public void SlotItem(GameObject itemToSlot, PickupItemData itemToSlotData)
{
ApplySlottedItemState(itemToSlot, itemToSlotData, triggerEvents: true, clearFollowerHeldItem);
ApplySlottedItemState(itemToSlot, itemToSlotData, triggerEvents: true);
}
#endregion

View File

@@ -1,8 +1,4 @@
using UnityEngine;
using Input;
using Interactions;
namespace Interactions
namespace Interactions
{
/// <summary>
/// Interactable that immediately completes when the character arrives at the interaction point.
@@ -11,11 +7,11 @@ namespace Interactions
public class OneClickInteraction : InteractableBase
{
/// <summary>
/// Override: Immediately completes the interaction with success when character arrives.
/// Main interaction logic: Simply return success.
/// </summary>
protected override void OnCharacterArrived()
protected override bool DoInteraction()
{
CompleteInteraction(true);
return true;
}
}
}

View File

@@ -1,20 +1,18 @@
using Input;
using UnityEngine;
using UnityEngine;
using System;
using System.Linq;
using Bootstrap; // added for Action<T>
using Core; // register with ItemManager
using Core;
namespace Interactions
{
/// <summary>
/// Saveable data for Pickup state
/// </summary>
[System.Serializable]
[Serializable]
public class PickupSaveData
{
public bool isPickedUp;
public bool wasHeldByFollower; // Track if held by follower for bilateral restoration
public bool wasHeldByFollower;
public Vector3 worldPosition;
public Quaternion worldRotation;
public bool isActive;
@@ -24,19 +22,11 @@ namespace Interactions
{
public PickupItemData itemData;
public SpriteRenderer iconRenderer;
// Track if the item has been picked up
public bool IsPickedUp { get; internal set; }
// Event: invoked when the item was picked up successfully
public event Action<PickupItemData> OnItemPickedUp;
// Event: invoked when this item is successfully combined with another
public event Action<PickupItemData> OnItemPickedUp;
public event Action<PickupItemData, PickupItemData, PickupItemData> OnItemsCombined;
/// <summary>
/// Unity Awake callback. Sets up icon and applies item data.
/// </summary>
protected override void Awake()
{
base.Awake(); // Register with save system
@@ -46,26 +36,16 @@ namespace Interactions
ApplyItemData();
}
/// <summary>
/// Register with ItemManager on Start
/// </summary>
protected override void Start()
{
base.Start(); // Register with save system
// Always register with ItemManager, even if picked up
// This allows the save/load system to find held items when restoring state
BootCompletionService.RegisterInitAction(() =>
{
ItemManager.Instance?.RegisterPickup(this);
});
ItemManager.Instance?.RegisterPickup(this);
}
/// <summary>
/// Unity OnDestroy callback. Unregisters from ItemManager.
/// </summary>
protected override void OnDestroy()
{
base.OnDestroy(); // Unregister from save system
@@ -103,64 +83,54 @@ namespace Interactions
}
}
/// <summary>
/// Override: Called when character arrives at the interaction point.
/// Handles item pickup and combination logic.
/// </summary>
protected override void OnCharacterArrived()
{
Logging.Debug("[Pickup] OnCharacterArrived");
var combinationResult = _followerController.TryCombineItems(this, out var combinationResultItem);
if (combinationResultItem != null)
{
CompleteInteraction(true);
// Fire the combination event when items are successfully combined
if (combinationResult == FollowerController.CombinationResult.Successful)
{
var resultPickup = combinationResultItem.GetComponent<Pickup>();
if (resultPickup != null && resultPickup.itemData != null)
{
// Get the combined item data
var resultItemData = resultPickup.itemData;
var heldItem = _followerController.GetHeldPickupObject();
if (heldItem != null)
{
var heldPickup = heldItem.GetComponent<Pickup>();
if (heldPickup != null && heldPickup.itemData != null)
{
// Trigger the combination event
OnItemsCombined?.Invoke(itemData, heldPickup.itemData, resultItemData);
}
}
}
}
return;
}
_followerController?.TryPickupItem(gameObject, itemData);
var step = GetComponent<PuzzleS.ObjectiveStepBehaviour>();
if (step != null && !step.IsStepUnlocked())
{
CompleteInteraction(false);
return;
}
bool wasPickedUp = (combinationResult == FollowerController.CombinationResult.NotApplicable
|| combinationResult == FollowerController.CombinationResult.Unsuccessful);
CompleteInteraction(wasPickedUp);
#region Interaction Logic
// Update pickup state and invoke event when the item was picked up successfully
if (wasPickedUp)
/// <summary>
/// Main interaction logic: Try combination, then try pickup.
/// </summary>
protected override bool DoInteraction()
{
Logging.Debug("[Pickup] DoInteraction");
// IMPORTANT: Capture held item data BEFORE combination
// TryCombineItems destroys the original items, so we need this data for the event
var heldItemObject = FollowerController?.GetHeldPickupObject();
var heldItemData = heldItemObject?.GetComponent<Pickup>()?.itemData;
// Try combination first
var combinationResult = FollowerController.TryCombineItems(this, out var resultItem);
if (combinationResult == FollowerController.CombinationResult.Successful)
{
IsPickedUp = true;
OnItemPickedUp?.Invoke(itemData);
// Combination succeeded - original items destroyed, result picked up by TryCombineItems
FireCombinationEvent(resultItem, heldItemData);
return true;
}
// No combination (or unsuccessful) - do regular pickup
FollowerController?.TryPickupItem(gameObject, itemData);
IsPickedUp = true;
OnItemPickedUp?.Invoke(itemData);
return true;
}
/// <summary>
/// Helper method to fire the combination event with correct item data.
/// </summary>
/// <param name="resultItem">The spawned result item</param>
/// <param name="originalHeldItemData">The ORIGINAL held item data (before destruction)</param>
private void FireCombinationEvent(GameObject resultItem, PickupItemData originalHeldItemData)
{
var resultPickup = resultItem?.GetComponent<Pickup>();
// Verify we have all required data
if (resultPickup?.itemData != null && originalHeldItemData != null && itemData != null)
{
OnItemsCombined?.Invoke(itemData, originalHeldItemData, resultPickup.itemData);
}
}
#endregion
#region Save/Load Implementation

View File

@@ -6,8 +6,6 @@ using Interactions;
using System.Threading.Tasks;
using UnityEngine;
// Added for IInteractionSettings
namespace Levels
{
/// <summary>
@@ -15,40 +13,46 @@ namespace Levels
/// </summary>
public class LevelSwitch : InteractableBase
{
/// <summary>
/// Data for this level switch (target scene, icon, etc).
/// </summary>
public LevelSwitchData switchData;
private SpriteRenderer _iconRenderer;
// Settings reference
private IInteractionSettings _interactionSettings;
private bool switchActive = true;
private SpriteRenderer iconRenderer;
private IInteractionSettings interactionSettings;
/// <summary>
/// Unity Awake callback. Sets up icon, interactable, and event handlers.
/// </summary>
void Awake()
protected override void Awake()
{
switchActive = true;
if (_iconRenderer == null)
_iconRenderer = GetComponent<SpriteRenderer>();
base.Awake();
Debug.Log($"[LevelSwitch] Awake called for {gameObject.name} in scene {gameObject.scene.name}");
if (iconRenderer == null)
iconRenderer = GetComponent<SpriteRenderer>();
// Initialize settings reference
_interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
ApplySwitchData();
}
protected override void OnManagedAwake()
{
Debug.Log($"[LevelSwitch] OnManagedAwake called for {gameObject.name}");
}
protected override void OnSceneReady()
{
Debug.Log($"[LevelSwitch] OnSceneReady called for {gameObject.name}");
}
#if UNITY_EDITOR
/// <summary>
/// Unity OnValidate callback. Ensures icon and data are up to date in editor.
/// </summary>
void OnValidate()
{
if (_iconRenderer == null)
_iconRenderer = GetComponent<SpriteRenderer>();
if (iconRenderer == null)
iconRenderer = GetComponent<SpriteRenderer>();
ApplySwitchData();
}
#endif
@@ -60,42 +64,48 @@ namespace Levels
{
if (switchData != null)
{
if (_iconRenderer != null)
_iconRenderer.sprite = switchData.mapSprite;
if (iconRenderer != null)
iconRenderer.sprite = switchData.mapSprite;
gameObject.name = switchData.targetLevelSceneName;
// Optionally update other fields, e.g. description
}
}
/// <summary>
/// Handles the start of an interaction (shows confirmation menu and switches the level if confirmed).
/// Main interaction logic: Spawn menu and switch input mode.
/// </summary>
protected override void OnCharacterArrived()
protected override bool DoInteraction()
{
if (switchData == null || string.IsNullOrEmpty(switchData.targetLevelSceneName) || !switchActive)
return;
if (switchData == null || string.IsNullOrEmpty(switchData.targetLevelSceneName))
{
Debug.LogWarning("LevelSwitch has no valid switchData!");
return false;
}
var menuPrefab = _interactionSettings?.LevelSwitchMenuPrefab;
var menuPrefab = interactionSettings?.LevelSwitchMenuPrefab;
if (menuPrefab == null)
{
Debug.LogError("LevelSwitchMenu prefab not assigned in InteractionSettings!");
return;
return false;
}
// Spawn the menu overlay (assume Canvas parent is handled in prefab setup)
// Spawn the menu overlay
var menuGo = Instantiate(menuPrefab);
var menu = menuGo.GetComponent<LevelSwitchMenu>();
if (menu == null)
{
Debug.LogError("LevelSwitchMenu component missing on prefab!");
Destroy(menuGo);
return;
return false;
}
// Setup menu with data and callbacks
menu.Setup(switchData, OnLevelSelectedWrapper, OnMinigameSelected, OnMenuCancel, OnRestartSelected);
switchActive = false; // Prevent re-triggering until menu is closed
// Switch input mode to UI only
InputManager.Instance.SetInputMode(InputMode.UI);
return true; // Menu spawned successfully
}
private void OnLevelSelectedWrapper()
@@ -123,7 +133,6 @@ namespace Levels
private void OnMenuCancel()
{
switchActive = true; // Allow interaction again if cancelled
InputManager.Instance.SetInputMode(InputMode.GameAndUI);
}
}

View File

@@ -4,19 +4,16 @@ using Core;
using Input;
using Interactions;
using System.Threading.Tasks;
using Bootstrap;
using PuzzleS;
using UnityEngine;
using Core.SaveLoad;
// Added for IInteractionSettings
namespace Levels
{
/// <summary>
/// Saveable data for MinigameSwitch state
/// </summary>
[System.Serializable]
[Serializable]
public class MinigameSwitchSaveData
{
public bool isUnlocked;
@@ -46,8 +43,6 @@ namespace Levels
{
base.Awake(); // Register with save system
BootCompletionService.RegisterInitAction(InitializePostBoot);
switchActive = true;
if (iconRenderer == null)
iconRenderer = GetComponent<SpriteRenderer>();
@@ -61,16 +56,25 @@ namespace Levels
{
base.Start(); // Register with save system
// Direct subscription - PuzzleManager available by this point
if (PuzzleManager.Instance != null)
{
PuzzleManager.Instance.OnAllPuzzlesComplete += HandleAllPuzzlesComplete;
}
// If not restoring from save, start inactive
if (!IsRestoringFromSave && !isUnlocked)
{
gameObject.SetActive(false);
}
}
protected override void OnDestroy()
private void OnDestroy()
{
base.OnDestroy(); // Unregister from save system
if (PuzzleManager.Instance != null)
{
PuzzleManager.Instance.OnAllPuzzlesComplete -= HandleAllPuzzlesComplete;
}
}
private void HandleAllPuzzlesComplete(PuzzleS.PuzzleLevelDataSO _)
@@ -109,34 +113,56 @@ namespace Levels
}
/// <summary>
/// Handles the start of an interaction (shows confirmation menu and switches the level if confirmed).
/// High-level validation: Only allow interaction if unlocked.
/// </summary>
protected override void OnCharacterArrived()
protected override bool CanBeClicked()
{
if (switchData == null || string.IsNullOrEmpty(switchData.targetLevelSceneName) || !switchActive)
return;
return base.CanBeClicked() && isUnlocked;
}
/// <summary>
/// Setup: Prevent re-entry while interaction is in progress.
/// </summary>
protected override void OnInteractionStarted()
{
switchActive = false;
}
/// <summary>
/// Main interaction logic: Spawn menu and switch input mode.
/// </summary>
protected override bool DoInteraction()
{
if (switchData == null || string.IsNullOrEmpty(switchData.targetLevelSceneName))
{
Debug.LogWarning("MinigameSwitch has no valid switchData!");
return false;
}
var menuPrefab = interactionSettings?.MinigameSwitchMenuPrefab;
if (menuPrefab == null)
{
Debug.LogError("MinigameSwitchMenu prefab not assigned in InteractionSettings!");
return;
return false;
}
// Spawn the menu overlay (assume Canvas parent is handled in prefab setup)
// Spawn the menu overlay
var menuGo = Instantiate(menuPrefab);
var menu = menuGo.GetComponent<MinigameSwitchMenu>();
if (menu == null)
{
Debug.LogError("MinigameSwitchMenu component missing on prefab!");
Destroy(menuGo);
return;
return false;
}
// Setup menu with data and callbacks
menu.Setup(switchData, OnLevelSelectedWrapper, OnMenuCancel);
switchActive = false; // Prevent re-triggering until menu is closed
// Switch input mode to UI only
InputManager.Instance.SetInputMode(InputMode.UI);
return true; // Menu spawned successfully
}
private void OnLevelSelectedWrapper()
@@ -156,10 +182,6 @@ namespace Levels
InputManager.Instance.SetInputMode(InputMode.GameAndUI);
}
private void InitializePostBoot()
{
PuzzleManager.Instance.OnAllPuzzlesComplete += HandleAllPuzzlesComplete;
}
#region Save/Load Implementation

View File

@@ -2,14 +2,13 @@
using AppleHills.Core.Settings;
using Cinematics;
using Core;
using Core.Lifecycle;
using Input;
using Minigames.DivingForPictures.PictureCamera;
using System;
using System.Collections;
using System.Collections.Generic;
using Bootstrap;
using Minigames.DivingForPictures.Bubbles;
using UI;
using UI.Core;
using UnityEngine;
using UnityEngine.Events;
@@ -17,7 +16,7 @@ using UnityEngine.Playables;
namespace Minigames.DivingForPictures
{
public class DivingGameManager : MonoBehaviour, IPausable
public class DivingGameManager : ManagedBehaviour, IPausable
{
[Header("Monster Prefabs")]
[Tooltip("Array of monster prefabs to spawn randomly")]
@@ -104,7 +103,10 @@ namespace Minigames.DivingForPictures
public static DivingGameManager Instance => _instance;
private void Awake()
public override int ManagedAwakePriority => 190;
public override bool AutoRegisterPausable => true; // Automatic GameManager registration
protected override void OnManagedAwake()
{
_settings = GameManager.GetSettingsObject<IDivingMinigameSettings>();
_currentSpawnProbability = _settings?.BaseSpawnProbability ?? 0.2f;
@@ -120,13 +122,18 @@ namespace Minigames.DivingForPictures
// Ensure any previous run state is reset when this manager awakes
_isGameOver = false;
Logging.Debug("[DivingGameManager] Initialized");
}
protected override void OnSceneReady()
{
InitializeGame();
CinematicsManager.Instance.OnCinematicStopped += EndGame;
}
private void Start()
{
// Register for post-boot initialization
BootCompletionService.RegisterInitAction(InitializePostBoot);
// Subscribe to player damage events (this doesn't depend on initialization)
PlayerCollisionBehavior.OnDamageTaken += OnPlayerDamageTaken;
@@ -155,29 +162,14 @@ namespace Minigames.DivingForPictures
OnMonsterSpawned += DoMonsterSpawned;
}
private void InitializePostBoot()
protected override void OnDestroy()
{
// Register this manager with the global GameManager
if (GameManager.Instance != null)
{
GameManager.Instance.RegisterPausableComponent(this);
}
base.OnDestroy(); // Handles auto-unregister from GameManager
InitializeGame();
CinematicsManager.Instance.OnCinematicStopped += EndGame;
}
private void OnDestroy()
{
// Unsubscribe from events when the manager is destroyed
PlayerCollisionBehavior.OnDamageTaken -= OnPlayerDamageTaken;
// Unregister from GameManager
if (GameManager.Instance != null)
{
GameManager.Instance.UnregisterPausableComponent(this);
}
// Unregister all pausable components
_pausableComponents.Clear();

View File

@@ -5,8 +5,8 @@ using UnityEngine.SceneManagement;
using Utils;
using AppleHills.Core.Settings;
using Core;
using Core.Lifecycle;
using Core.SaveLoad;
using Bootstrap;
using UnityEngine.Events;
/// <summary>
@@ -24,7 +24,7 @@ public class FollowerSaveData
/// <summary>
/// Controls the follower character, including following the player, handling pickups, and managing held items.
/// </summary>
public class FollowerController : MonoBehaviour, ISaveParticipant
public class FollowerController : ManagedBehaviour, ISaveParticipant
{
private static readonly int CombineTrigger = Animator.StringToHash("Combine");
@@ -103,7 +103,9 @@ public class FollowerController : MonoBehaviour, ISaveParticipant
private bool _hasRestoredHeldItem; // Track if held item restoration completed
private string _expectedHeldItemSaveId; // Expected saveId during restoration
void Awake()
public override int ManagedAwakePriority => 110; // Follower after player
protected override void OnManagedAwake()
{
_aiPath = GetComponent<AIPath>();
// Find art prefab and animator
@@ -123,13 +125,7 @@ public class FollowerController : MonoBehaviour, ISaveParticipant
_settings = GameManager.GetSettingsObject<IPlayerFollowerSettings>();
_interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
// Register for post-boot initialization
BootCompletionService.RegisterInitAction(InitializePostBoot);
}
private void InitializePostBoot()
{
// Register with save system after boot
// Register with save system
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.RegisterParticipant(this);
@@ -140,7 +136,7 @@ public class FollowerController : MonoBehaviour, ISaveParticipant
Logging.Warning("[FollowerController] SaveLoadManager not available for registration");
}
}
void OnEnable()
{
SceneManager.sceneLoaded += OnSceneLoaded;
@@ -583,19 +579,31 @@ public class FollowerController : MonoBehaviour, ISaveParticipant
#endregion StationaryAnimations
#region ItemInteractions
// TODO: Move TryCombineItems to ItemManager/InteractionHelpers
// This is currently interaction logic living in a movement controller.
// Pros of moving: Separates game logic from character logic, easier to test
// Cons: More coordination needed, follower still needs animation callbacks
/// <summary>
/// Try to pickup an item. If already holding something, optionally drop it first.
/// </summary>
/// <param name="itemObject">The GameObject to pick up (must have Pickup component)</param>
/// <param name="itemData">The item data (redundant - can be extracted from GameObject)</param>
/// <param name="dropItem">Whether to drop currently held item before picking up new one</param>
public void TryPickupItem(GameObject itemObject, PickupItemData itemData, bool dropItem = true)
{
if (itemObject == null) return;
// Drop current item if holding something
if (_currentlyHeldItemData != null && _cachedPickupObject != null && dropItem)
{
// Drop the currently held item at the current position
DropHeldItemAt(transform.position);
}
// Pick up the new item
SetHeldItem(itemData, itemObject.GetComponent<SpriteRenderer>());
_animator.SetBool("IsCarrying", true);
_cachedPickupObject = itemObject;
_cachedPickupObject.SetActive(false);
// Use helper to set held item (handles data extraction, caching, animator)
SetHeldItemFromObject(itemObject);
itemObject.SetActive(false);
}
public enum CombinationResult
@@ -609,41 +617,41 @@ public class FollowerController : MonoBehaviour, ISaveParticipant
{
_animator.ResetTrigger(CombineTrigger);
newItem = null;
// Validation
if (_cachedPickupObject == null)
{
return CombinationResult.NotApplicable;
}
Pickup pickupB = _cachedPickupObject.GetComponent<Pickup>();
if (pickupA == null || pickupB == null)
{
return CombinationResult.NotApplicable;
}
// Use the InteractionSettings directly instead of GameManager
// Find combination rule
CombinationRule matchingRule = _interactionSettings.GetCombinationRule(pickupA.itemData, pickupB.itemData);
Vector3 spawnPos = pickupA.gameObject.transform.position;
if (matchingRule != null && matchingRule.resultPrefab != null)
{
newItem = Instantiate(matchingRule.resultPrefab, spawnPos, Quaternion.identity);
var resultPickup = newItem.GetComponent<Pickup>();
PickupItemData itemData = resultPickup.itemData;
// Mark the base items as picked up before destroying them
// (This ensures they save correctly if the game is saved during the combination animation)
pickupA.IsPickedUp = true;
pickupB.IsPickedUp = true;
Destroy(pickupA.gameObject);
Destroy(pickupB.gameObject);
TryPickupItem(newItem, itemData);
PlayAnimationStationary("Combine", 10.0f);
PulverIsCombining.Invoke();
return CombinationResult.Successful;
}
if (matchingRule == null || matchingRule.resultPrefab == null)
return CombinationResult.Unsuccessful;
// If no combination found, return Unsuccessful
return CombinationResult.Unsuccessful;
// Execute combination
Vector3 spawnPos = pickupA.gameObject.transform.position;
newItem = Instantiate(matchingRule.resultPrefab, spawnPos, Quaternion.identity);
var resultPickup = newItem.GetComponent<Pickup>();
// Mark items as picked up before destroying (for save system)
pickupA.IsPickedUp = true;
pickupB.IsPickedUp = true;
Destroy(pickupA.gameObject);
Destroy(pickupB.gameObject);
// Pickup the result (don't drop it!)
TryPickupItem(newItem, resultPickup.itemData, dropItem: false);
// Visual feedback
PlayAnimationStationary("Combine", 10.0f);
PulverIsCombining.Invoke();
return CombinationResult.Successful;
}
/// <summary>
@@ -673,6 +681,10 @@ public class FollowerController : MonoBehaviour, ISaveParticipant
return _cachedPickupObject;
}
/// <summary>
/// Set held item from a GameObject. Extracts Pickup component and sets up visuals.
/// Centralizes held item state management including animator.
/// </summary>
public void SetHeldItemFromObject(GameObject obj)
{
if (obj == null)
@@ -680,11 +692,13 @@ public class FollowerController : MonoBehaviour, ISaveParticipant
ClearHeldItem();
return;
}
var pickup = obj.GetComponent<Pickup>();
if (pickup != null)
{
SetHeldItem(pickup.itemData, pickup.iconRenderer);
_cachedPickupObject = obj;
_animator.SetBool("IsCarrying", true); // Centralized animator management
}
else
{
@@ -692,11 +706,15 @@ public class FollowerController : MonoBehaviour, ISaveParticipant
}
}
/// <summary>
/// Clear the currently held item. Centralizes state cleanup including animator.
/// </summary>
public void ClearHeldItem()
{
_cachedPickupObject = null;
_currentlyHeldItemData = null;
_animator.SetBool("IsCarrying", false);
_animator.SetBool("IsCarrying", false); // Centralized animator management
if (heldObjectRenderer != null)
{
heldObjectRenderer.sprite = null;
@@ -704,29 +722,28 @@ public class FollowerController : MonoBehaviour, ISaveParticipant
}
}
public void DropItem(FollowerController follower, Vector3 position)
/// <summary>
/// Drop the currently held item at the specified position.
/// </summary>
public void DropHeldItemAt(Vector3 position)
{
var item = follower.GetHeldPickupObject();
var item = GetHeldPickupObject();
if (item == null) return;
// Place item in world
item.transform.position = position;
item.transform.SetParent(null);
item.SetActive(true);
// Reset the pickup state so it can be picked up again and saves correctly
// Reset pickup state so it can be picked up again
var pickup = item.GetComponent<Pickup>();
if (pickup != null)
{
pickup.ResetPickupState();
}
follower.ClearHeldItem();
_animator.SetBool("IsCarrying", false);
// Optionally: fire event, update UI, etc.
}
public void DropHeldItemAt(Vector3 position)
{
DropItem(this, position);
// Clear held item state (includes animator)
ClearHeldItem();
}

View File

@@ -5,8 +5,8 @@ using System.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
using AppleHills.Core.Settings;
using Bootstrap;
using Core;
using Core.Lifecycle;
using Core.SaveLoad;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
@@ -28,7 +28,7 @@ namespace PuzzleS
/// <summary>
/// Manages puzzle step registration, dependency management, and step completion for the puzzle system.
/// </summary>
public class PuzzleManager : MonoBehaviour, ISaveParticipant
public class PuzzleManager : ManagedBehaviour, ISaveParticipant
{
private static PuzzleManager _instance;
@@ -81,29 +81,20 @@ namespace PuzzleS
/// </summary>
public bool HasBeenRestored => _hasBeenRestored;
void Awake()
public override int ManagedAwakePriority => 80; // Puzzle systems
private new void Awake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
_instance = this;
// Initialize settings reference
_interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
// Register for post-boot initialization
BootCompletionService.RegisterInitAction(InitializePostBoot);
}
private void InitializePostBoot()
protected override void OnManagedAwake()
{
// Subscribe to SceneManagerService events after boot is complete
SceneManagerService.Instance.SceneLoadCompleted += OnSceneLoadCompleted;
SceneManagerService.Instance.SceneLoadStarted += OnSceneLoadStarted;
// Register with save/load system
BootCompletionService.RegisterInitAction(() =>
{
SaveLoadManager.Instance.RegisterParticipant(this);
Logging.Debug("[PuzzleManager] Registered with SaveLoadManager");
});
// Initialize settings reference
_interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
// Find player transform
_playerTransform = GameObject.FindGameObjectWithTag("Player")?.transform;
@@ -117,46 +108,44 @@ namespace PuzzleS
LoadPuzzleDataForCurrentScene();
}
Logging.Debug("[PuzzleManager] Subscribed to SceneManagerService events");
// Register with save/load system
SaveLoadManager.Instance.RegisterParticipant(this);
Logging.Debug("[PuzzleManager] Registered with SaveLoadManager");
// Subscribe to scene load events from SceneManagerService
// This is necessary because PuzzleManager is in DontDestroyOnLoad and won't receive OnSceneReady() callbacks
if (SceneManagerService.Instance != null)
{
SceneManagerService.Instance.SceneLoadCompleted += OnSceneLoadCompleted;
}
Logging.Debug("[PuzzleManager] Initialized");
}
void OnDestroy()
/// <summary>
/// Called when any scene finishes loading. Loads puzzles for the new scene.
/// </summary>
private void OnSceneLoadCompleted(string sceneName)
{
StopProximityChecks();
Logging.Debug($"[Puzzles] Scene loaded: {sceneName}, loading puzzle data");
LoadPuzzlesForScene(sceneName);
}
protected override void OnDestroy()
{
base.OnDestroy();
// Unsubscribe from scene manager events
// Unsubscribe from SceneManagerService events
if (SceneManagerService.Instance != null)
{
SceneManagerService.Instance.SceneLoadCompleted -= OnSceneLoadCompleted;
SceneManagerService.Instance.SceneLoadStarted -= OnSceneLoadStarted;
}
// Unregister from save/load system
SaveLoadManager.Instance.UnregisterParticipant(GetSaveId());
Logging.Debug("[PuzzleManager] Unregistered from SaveLoadManager");
// Release addressable handle if needed
if (_levelDataLoadOperation.IsValid())
{
Addressables.Release(_levelDataLoadOperation);
}
}
/// <summary>
/// Called when a scene is starting to load
/// Loads puzzle data for the specified scene
/// </summary>
public void OnSceneLoadStarted(string sceneName)
{
// Reset data loaded state when changing scenes to avoid using stale data
_isDataLoaded = false;
Logging.Debug($"[Puzzles] Scene load started: {sceneName}, marked puzzle data as not loaded");
}
/// <summary>
/// Called when a scene is loaded
/// </summary>
public void OnSceneLoadCompleted(string sceneName)
private void LoadPuzzlesForScene(string sceneName)
{
// Skip for non-gameplay scenes
if (sceneName == "BootstrapScene" || string.IsNullOrEmpty(sceneName))

View File

@@ -1,7 +1,3 @@
using AppleHills.Core.Settings;
using Bootstrap;
using Core;
using PuzzleS;
using UnityEngine;
using UnityEngine.Audio;
using AppleHills.Core;
@@ -9,8 +5,9 @@ using AppleHills.Core.Interfaces;
using System.Collections.Generic;
using AudioSourceEvents;
using System;
using Core.Lifecycle;
public class AudioManager : MonoBehaviour, IPausable
public class AudioManager : ManagedBehaviour, IPausable
{
/// <summary>
/// Play all audio, just music or no audio at all when the game is paused.
@@ -42,18 +39,21 @@ public class AudioManager : MonoBehaviour, IPausable
/// </summary>
public static AudioManager Instance => _instance;
void Awake()
{
_instance = this;
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 30; // Audio infrastructure
public override bool AutoRegisterPausable => true; // Auto-register as IPausable
// Register for post-boot initialization
BootCompletionService.RegisterInitAction(InitializePostBoot);
GameManager.Instance.RegisterPausableComponent(this);
private new void Awake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
_instance = this;
}
private void InitializePostBoot()
protected override void OnManagedAwake()
{
// Auto-registration with GameManager handled by ManagedBehaviour
}
// Start is called once before the first execution of Update after the MonoBehaviour is created

View File

@@ -1,6 +1,6 @@
using AppleHills.Data.CardSystem;
using Bootstrap;
using Core;
using Core.Lifecycle;
using Data.CardSystem;
using Pixelplacement;
using UI.Core;
@@ -13,7 +13,7 @@ namespace UI.CardSystem
/// Main UI controller for the card album system.
/// Manages the backpack icon and navigation between card system pages.
/// </summary>
public class CardAlbumUI : MonoBehaviour
public class CardAlbumUI : ManagedBehaviour
{
[Header("UI References")]
[SerializeField] private GameObject backpackIcon;
@@ -34,7 +34,9 @@ namespace UI.CardSystem
private CardSystemManager _cardManager;
private bool _hasUnseenCards;
private void Awake()
public override int ManagedAwakePriority => 65; // UI card systems
protected override void OnManagedAwake()
{
// Set up backpack button
if (backpackButton != null)
@@ -45,13 +47,7 @@ namespace UI.CardSystem
// Hide notification dot initially
if (boosterNotificationDot != null)
boosterNotificationDot.gameObject.SetActive(false);
// Register for post-boot initialization
BootCompletionService.RegisterInitAction(InitializePostBoot);
}
private void InitializePostBoot()
{
// Initially show only the backpack icon
ShowOnlyBackpackIcon();

View File

@@ -1,5 +1,5 @@
using Bootstrap;
using Core;
using Core;
using Core.Lifecycle;
using UnityEngine;
@@ -11,49 +11,34 @@ namespace UI.CardSystem
/// Attach this to your Card System root GameObject. It subscribes to SceneManagerService.SceneLoadCompleted
/// and applies visibility: hidden in "StartingScene" (configurable), visible in all other gameplay scenes.
/// </summary>
public class CardSystemSceneVisibility : MonoBehaviour
public class CardSystemSceneVisibility : ManagedBehaviour
{
[Header("Target Root")]
[Tooltip("The GameObject to show/hide. Defaults to this GameObject if not assigned.")]
[SerializeField] private GameObject targetRoot;
private void Awake()
public override int ManagedAwakePriority => 95; // Scene-specific UI visibility
protected override void OnManagedAwake()
{
if (targetRoot == null)
targetRoot = gameObject;
// Defer subscription to after boot so SceneManagerService is guaranteed ready.
BootCompletionService.RegisterInitAction(InitializePostBoot, priority: 95, name: "CardSystem Scene Visibility Init");
}
private void InitializePostBoot()
{
var sceneSvc = SceneManagerService.Instance;
if (sceneSvc == null)
{
Debug.LogWarning("[CardSystemSceneVisibility] SceneManagerService.Instance is null post-boot.");
return;
}
// Subscribe to scene load completion notifications
sceneSvc.SceneLoadCompleted += OnSceneLoaded;
// Apply initial state based on current gameplay scene
ApplyVisibility(sceneSvc.CurrentGameplayScene);
}
private void OnDestroy()
protected override void OnSceneReady()
{
// Replaces SceneLoadCompleted subscription
var sceneSvc = SceneManagerService.Instance;
if (sceneSvc != null)
{
sceneSvc.SceneLoadCompleted -= OnSceneLoaded;
ApplyVisibility(sceneSvc.CurrentGameplayScene);
}
}
private void OnSceneLoaded(string sceneName)
protected override void OnDestroy()
{
ApplyVisibility(sceneName);
base.OnDestroy();
// No additional cleanup needed
}
private void ApplyVisibility(string sceneName)

View File

@@ -1,4 +1,5 @@
using System;
using Core.Lifecycle;
using UnityEngine;
namespace UI.Core
@@ -6,12 +7,17 @@ namespace UI.Core
/// <summary>
/// Base class for UI pages that can transition in and out.
/// Extended by specific UI page implementations for the card system.
/// Now inherits from ManagedBehaviour for lifecycle support.
/// Children can override lifecycle hooks if they need boot-dependent initialization.
/// </summary>
public abstract class UIPage : MonoBehaviour
public abstract class UIPage : ManagedBehaviour
{
[Header("Page Settings")]
public string PageName;
// UI pages load after UI infrastructure (UIPageController is priority 50)
public override int ManagedAwakePriority => 200;
// Events using System.Action instead of UnityEvents
public event Action OnTransitionInStarted;
public event Action OnTransitionInCompleted;

View File

@@ -1,8 +1,7 @@
using System;
using System.Collections.Generic;
using Bootstrap;
using Core;
using UnityEngine;
using Core.Lifecycle;
using UnityEngine.InputSystem;
namespace UI.Core
@@ -11,7 +10,7 @@ namespace UI.Core
/// Manages UI page transitions and maintains a stack of active pages.
/// Pages are pushed onto a stack for navigation and popped when going back.
/// </summary>
public class UIPageController : MonoBehaviour
public class UIPageController : ManagedBehaviour
{
private static UIPageController _instance;
public static UIPageController Instance => _instance;
@@ -30,36 +29,25 @@ namespace UI.Core
private PlayerInput _playerInput;
private InputAction _cancelAction;
private void Awake()
public override int ManagedAwakePriority => 50; // UI infrastructure
private new void Awake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
_instance = this;
// TODO: Handle generic "cancel" action
// _playerInput = FindFirstObjectByType<PlayerInput>();
// if (_playerInput == null)
// {
// Logging.Warning("[UIPageController] No PlayerInput found in the scene. Cancel action might not work.");
// }
// else
// {
// // Get the Cancel action from the UI action map
// _cancelAction = _playerInput.actions.FindAction("UI/Cancel");
// if (_cancelAction != null)
// {
// _cancelAction.performed += OnCancelActionPerformed;
// }
// else
// {
// Logging.Warning("[UIPageController] Cancel action not found in the input actions asset.");
// }
// }
// Register for post-boot initialization
BootCompletionService.RegisterInitAction(InitializePostBoot);
}
private void OnDestroy()
protected override void OnManagedAwake()
{
Logging.Debug("[UIPageController] Initialized");
}
protected override void OnDestroy()
{
base.OnDestroy();
// Clean up event subscription when the controller is destroyed
if (_cancelAction != null)
{
@@ -74,12 +62,6 @@ namespace UI.Core
_pageStack.Peek().OnBackPressed();
}
}
private void InitializePostBoot()
{
// Initialize any dependencies that require other services to be ready
Logging.Debug("[UIPageController] Post-boot initialization complete");
}
/// <summary>
/// Pushes a new page onto the stack, hiding the current page and showing the new one.

View File

@@ -1,16 +1,16 @@
using System.Collections;
using System;
using Bootstrap;
using Core;
using Core.Lifecycle;
using UnityEngine;
using UnityEngine.UI;
using Core;
namespace UI
{
/// <summary>
/// Controls the loading screen UI display, progress updates, and timing
/// </summary>
public class LoadingScreenController : MonoBehaviour
public class LoadingScreenController : ManagedBehaviour
{
[Header("UI References")]
[SerializeField] private GameObject loadingScreenContainer;
@@ -53,10 +53,17 @@ namespace UI
/// </summary>
public static LoadingScreenController Instance => _instance;
private void Awake()
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 45; // UI infrastructure, before UIPageController
private new void Awake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
_instance = this;
// Set up container reference early
if (loadingScreenContainer == null)
loadingScreenContainer = gameObject;
@@ -65,15 +72,11 @@ namespace UI
{
loadingScreenContainer.SetActive(false);
}
// Register for post-boot initialization
BootCompletionService.RegisterInitAction(InitializePostBoot);
}
private void InitializePostBoot()
protected override void OnManagedAwake()
{
// Initialize any dependencies that require other services to be ready
Logging.Debug("[LoadingScreenController] Post-boot initialization complete");
Logging.Debug("[LoadingScreenController] Initialized");
}
/// <summary>

View File

@@ -2,7 +2,6 @@ using System;
using Core;
using UnityEngine;
using UnityEngine.SceneManagement;
using Bootstrap;
using UI.Core;
using Pixelplacement;
@@ -22,9 +21,14 @@ namespace UI
[SerializeField] private GameObject pauseButton;
[SerializeField] private CanvasGroup canvasGroup;
// After UIPageController (50)
public override int ManagedAwakePriority => 55;
private void Awake()
private new void Awake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
_instance = this;
// Ensure we have a CanvasGroup for transitions
@@ -32,18 +36,22 @@ namespace UI
canvasGroup = GetComponent<CanvasGroup>();
if (canvasGroup == null)
canvasGroup = gameObject.AddComponent<CanvasGroup>();
// Set initial state
canvasGroup.alpha = 0f;
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
gameObject.SetActive(false);
// Register for post-boot initialization
BootCompletionService.RegisterInitAction(InitializePostBoot);
}
protected override void OnManagedAwake()
{
// Component setup already done in Awake
}
private void InitializePostBoot()
protected override void OnSceneReady()
{
// Subscribe to scene loaded events
// Subscribe to scene-dependent events
SceneManagerService.Instance.SceneLoadCompleted += SetPauseMenuByLevel;
// Also react to global UI hide/show events from the page controller
@@ -53,16 +61,16 @@ namespace UI
UIPageController.Instance.OnAllUIShown += HandleAllUIShown;
}
// SceneManagerService subscription moved to InitializePostBoot
// Set initial state based on current scene
SetPauseMenuByLevel(SceneManager.GetActiveScene().name);
Logging.Debug("[PauseMenu] Subscribed to SceneManagerService events");
}
private void OnDestroy()
protected override void OnDestroy()
{
base.OnDestroy();
// Unsubscribe when destroyed
if (SceneManagerService.Instance != null)
{

View File

@@ -1,6 +1,6 @@
using System.Collections;
using Bootstrap;
using Core;
using Core.Lifecycle;
using Core.SaveLoad;
using Input;
using Pixelplacement;
@@ -9,7 +9,7 @@ using UnityEngine;
namespace UI.Tutorial
{
public class DivingTutorial : MonoBehaviour, ITouchInputConsumer
public class DivingTutorial : ManagedBehaviour, ITouchInputConsumer
{
public enum ProgressType
{
@@ -27,18 +27,14 @@ namespace UI.Tutorial
private bool _canAcceptInput;
private Coroutine _waitLoopCoroutine;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
BootCompletionService.RegisterInitAction(InitializeTutorial);
public override int ManagedAwakePriority => 200; // Tutorial runs late, after other systems
protected override void OnManagedAwake()
{
// Ensure prompt is hidden initially (even before tutorial initialization)
if (tapPrompt != null)
tapPrompt.SetActive(false);
}
void InitializeTutorial()
{
if (playTutorial && !SaveLoadManager.Instance.currentSaveData.playedDivingTutorial)
{
// TODO: Possibly do it better, but for now just mark tutorial as played immediately
@@ -221,7 +217,7 @@ namespace UI.Tutorial
// Manual mode: enable input and wait for player tap
SetInputEnabled(true);
}
_waitLoopCoroutine = null;
}
}