Rework of base interactables and managed behaviors
This commit is contained in:
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aa0228cf33a64515bc166b7a9bc8c0b9
|
||||
timeCreated: 1760606319
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
3
Assets/Scripts/Core/Lifecycle.meta
Normal file
3
Assets/Scripts/Core/Lifecycle.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 06a2c07342e5422eae1eb613f614ed61
|
||||
timeCreated: 1762206473
|
||||
48
Assets/Scripts/Core/Lifecycle/LifecycleEnums.cs
Normal file
48
Assets/Scripts/Core/Lifecycle/LifecycleEnums.cs
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
2
Assets/Scripts/Core/Lifecycle/LifecycleEnums.cs.meta
Normal file
2
Assets/Scripts/Core/Lifecycle/LifecycleEnums.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5f5f0f19f08240d4d9863b6be6a3cf03
|
||||
420
Assets/Scripts/Core/Lifecycle/LifecycleManager.cs
Normal file
420
Assets/Scripts/Core/Lifecycle/LifecycleManager.cs
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
2
Assets/Scripts/Core/Lifecycle/LifecycleManager.cs.meta
Normal file
2
Assets/Scripts/Core/Lifecycle/LifecycleManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db6d4743867a3a44381d511cea39218d
|
||||
224
Assets/Scripts/Core/Lifecycle/ManagedBehaviour.cs
Normal file
224
Assets/Scripts/Core/Lifecycle/ManagedBehaviour.cs
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
2
Assets/Scripts/Core/Lifecycle/ManagedBehaviour.cs.meta
Normal file
2
Assets/Scripts/Core/Lifecycle/ManagedBehaviour.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af776ef1493d6e543aa3cbe2601f4ef2
|
||||
102
Assets/Scripts/Core/Lifecycle/ManagedEventSubscription.cs
Normal file
102
Assets/Scripts/Core/Lifecycle/ManagedEventSubscription.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 63e107279fdbf1542a9d93d57e60285c
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user