Refactor interactions, introduce template-method lifecycle management, work on save-load system (#51)

# Lifecycle Management & Save System Revamp

## Overview
Complete overhaul of game lifecycle management, interactable system, and save/load architecture. Introduces centralized `ManagedBehaviour` base class for consistent initialization ordering and lifecycle hooks across all systems.

## Core Architecture

### New Lifecycle System
- **`LifecycleManager`**: Centralized coordinator for all managed objects
- **`ManagedBehaviour`**: Base class replacing ad-hoc initialization patterns
  - `OnManagedAwake()`: Priority-based initialization (0-100, lower = earlier)
  - `OnSceneReady()`: Scene-specific setup after managers ready
  - Replaces `BootCompletionService` (deleted)
- **Priority groups**: Infrastructure (0-20) → Game Systems (30-50) → Data (60-80) → UI/Gameplay (90-100)
- **Editor support**: `EditorLifecycleBootstrap` ensures lifecycle works in editor mode

### Unified SaveID System
- Consistent format: `{ParentName}_{ComponentType}`
- Auto-registration via `AutoRegisterForSave = true`
- New `DebugSaveIds` editor tool for inspection

## Save/Load Improvements

### Enhanced State Management
- **Extended SaveLoadData**: Unlocked minigames, card collection states, combination items, slot occupancy
- **Async loading**: `ApplyCardCollectionState()` waits for card definitions before restoring
- **New `SaveablePlayableDirector`**: Timeline sequences save/restore playback state
- **Fixed race conditions**: Proper initialization ordering prevents data corruption

## Interactable & Pickup System

- Migrated to `OnManagedAwake()` for consistent initialization
- Template method pattern for state restoration (`RestoreInteractionState()`)
- Fixed combination item save/load bugs (items in slots vs. follower hand)
- Dynamic spawning support for combined items on load
- **Breaking**: `Interactable.Awake()` now sealed, use `OnManagedAwake()` instead

##  UI System Changes

- **AlbumViewPage** and **BoosterNotificationDot**: Migrated to `ManagedBehaviour`
- **Fixed menu persistence bug**: Menus no longer reappear after scene transitions
- **Pause Menu**: Now reacts to all scene loads (not just first scene)
- **Orientation Enforcer**: Enforces per-scene via `SceneManagementService`
- **Loading Screen**: Integrated with new lifecycle

## ⚠️ Breaking Changes

1. **`BootCompletionService` removed** → Use `ManagedBehaviour.OnManagedAwake()` with priority
2. **`Interactable.Awake()` sealed** → Override `OnManagedAwake()` instead
3. **SaveID format changed** → Now `{ParentName}_{ComponentType}` consistently
4. **MonoBehaviours needing init ordering** → Must inherit from `ManagedBehaviour`

Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com>
Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com>
Reviewed-on: #51
This commit is contained in:
2025-11-07 15:38:31 +00:00
parent dfa42b2296
commit e27bb7bfb6
93 changed files with 7900 additions and 4347 deletions

View File

@@ -1,5 +0,0 @@
You are an expert code architect and developer. YOu prioritize clean, efficient, and maintainable code.
You priotize up-front though out planning before writing code.
You will always present implementaiton plan first and always ask for permission to implement it.
Never insert zero-width spaces or non-breaking spaces in my code.
DOn't produce .MD documentation unless i ask you to.

View File

@@ -122,6 +122,32 @@ TextureImporter:
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WindowsStoreApps
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:

View File

@@ -122,6 +122,32 @@ TextureImporter:
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WindowsStoreApps
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c5d626da49844592981ef14524e3a308
timeCreated: 1762332131

View File

@@ -0,0 +1,144 @@
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;
using Core.Lifecycle;
using Core.SaveLoad;
using AppleHills.Core.Settings;
using Bootstrap;
namespace Editor.Lifecycle
{
/// <summary>
/// Editor-only bootstrap that ensures OnSceneReady is triggered when playing directly from a scene in Unity Editor.
///
/// PROBLEM: When you press Play in the editor without going through the scene manager:
/// - CustomBoot runs and triggers OnBootCompletionTriggered (which broadcasts OnManagedAwake)
/// - But BroadcastSceneReady is NEVER called for the initial scene
/// - Components in the scene never receive their OnSceneReady() callback
///
/// SOLUTION: After boot completes, detect the active scene and broadcast OnSceneReady for it.
/// This only runs in editor mode and mimics what SceneManagerService does during normal scene transitions.
/// </summary>
[InitializeOnLoad]
public static class EditorLifecycleBootstrap
{
private static bool hasTriggeredInitialSceneReady = false;
private static int framesSincePlayMode = 0;
private const int MaxFramesToWait = 300; // 5 seconds at 60fps
static EditorLifecycleBootstrap()
{
// Subscribe to play mode state changes
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
}
private static void OnPlayModeStateChanged(PlayModeStateChange state)
{
// Reset flag when exiting play mode
if (state == PlayModeStateChange.ExitingPlayMode || state == PlayModeStateChange.EnteredEditMode)
{
hasTriggeredInitialSceneReady = false;
framesSincePlayMode = 0;
return;
}
// When we enter play mode, wait for boot to complete then trigger scene ready
if (state == PlayModeStateChange.EnteredPlayMode)
{
hasTriggeredInitialSceneReady = false;
framesSincePlayMode = 0;
// Use EditorApplication.update to poll until boot completes
EditorApplication.update += WaitForBootAndTriggerSceneReady;
}
}
private static void WaitForBootAndTriggerSceneReady()
{
framesSincePlayMode++;
// Safety timeout - if boot hasn't completed after 5 seconds, something is wrong
if (framesSincePlayMode > MaxFramesToWait)
{
Debug.LogError($"[EditorLifecycleBootstrap] Timed out waiting for boot completion after {MaxFramesToWait} frames. " +
"CustomBoot may have failed to initialize properly.");
EditorApplication.update -= WaitForBootAndTriggerSceneReady;
return;
}
// Check if boot has completed
if (!CustomBoot.Initialised)
return;
// Check if LifecycleManager exists
if (LifecycleManager.Instance == null)
{
Debug.LogWarning("[EditorLifecycleBootstrap] LifecycleManager instance not found. " +
"Lifecycle may not be properly initialized.");
EditorApplication.update -= WaitForBootAndTriggerSceneReady;
return;
}
// Only trigger once per play session
if (hasTriggeredInitialSceneReady)
{
EditorApplication.update -= WaitForBootAndTriggerSceneReady;
return;
}
hasTriggeredInitialSceneReady = true;
EditorApplication.update -= WaitForBootAndTriggerSceneReady;
// Get the active scene
Scene activeScene = SceneManager.GetActiveScene();
if (!activeScene.isLoaded)
{
Debug.LogWarning($"[EditorLifecycleBootstrap] Active scene '{activeScene.name}' is not loaded.");
return;
}
// Skip bootstrap scene - it doesn't need scene ready
// Note: BootstrapScene is the infrastructure scene, not a gameplay scene
if (activeScene.name == "BootstrapScene" || activeScene.name == "Bootstrap")
{
Debug.Log($"[EditorLifecycleBootstrap] Skipping OnSceneReady for infrastructure scene: {activeScene.name}");
return;
}
Debug.Log($"<color=cyan>[EditorLifecycleBootstrap] Triggering lifecycle for initial scene: {activeScene.name}</color>");
// Broadcast scene ready for the initial scene
// This mimics what SceneManagerService does during scene transitions (Phase 10)
try
{
LifecycleManager.Instance.BroadcastSceneReady(activeScene.name);
}
catch (System.Exception ex)
{
Debug.LogError($"[EditorLifecycleBootstrap] Error broadcasting SceneReady: {ex.Message}\n{ex.StackTrace}");
return;
}
// Restore scene-specific data via SaveLoadManager
// This mimics SceneManagerService Phase 11
if (SaveLoadManager.Instance != null)
{
var debugSettings = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>();
if (debugSettings.useSaveLoadSystem)
{
try
{
Debug.Log($"[EditorLifecycleBootstrap] Restoring scene data for: {activeScene.name}");
SaveLoadManager.Instance.RestoreSceneData();
}
catch (System.Exception ex)
{
Debug.LogError($"[EditorLifecycleBootstrap] Error restoring scene data: {ex.Message}\n{ex.StackTrace}");
}
}
}
}
}
}

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 7f3e8a9c4d5b6e7f8a9b0c1d2e3f4a5b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -19,6 +19,7 @@ namespace Editor.Tools
private string searchTypeName = "Select a Component...";
private string replaceTypeName = "Select a Component...";
private List<Type> allMonoBehaviourTypes = new List<Type>();
private bool includeDerivedTypes = true;
[MenuItem("Tools/Component Search & Replace")]
public static void ShowWindow()
@@ -102,6 +103,15 @@ namespace Editor.Tools
GUILayout.Space(5);
// Include Derived Types checkbox
includeDerivedTypes = EditorGUILayout.Toggle(
new GUIContent("Include Derived Types",
"When enabled, searches for the selected type and all types that inherit from it. " +
"When disabled, searches only for the exact type."),
includeDerivedTypes);
GUILayout.Space(5);
EditorGUI.BeginDisabledGroup(selectedSearchType == null);
if (GUILayout.Button("Search Scene", GUILayout.Height(30)))
{
@@ -242,7 +252,20 @@ namespace Editor.Tools
foreach (var go in allObjects)
{
var component = go.GetComponent(selectedSearchType);
Component component = null;
if (includeDerivedTypes)
{
// Search for the type and all derived types
component = go.GetComponent(selectedSearchType);
}
else
{
// Search for exact type only
var components = go.GetComponents<Component>();
component = components.FirstOrDefault(c => c != null && c.GetType() == selectedSearchType);
}
if (component != null)
{
foundComponents.Add(new ComponentInfo
@@ -256,7 +279,8 @@ namespace Editor.Tools
foundComponents = foundComponents.OrderBy(c => c.hierarchyPath).ToList();
Debug.Log($"Found {foundComponents.Count} objects with component type '{selectedSearchType.Name}'");
string searchMode = includeDerivedTypes ? "including derived types" : "exact type only";
Debug.Log($"Found {foundComponents.Count} objects with component type '{selectedSearchType.Name}' ({searchMode})");
Repaint();
}

View File

@@ -0,0 +1,31 @@
using UnityEngine;
using UnityEditor;
using Core.Lifecycle;
namespace Editor.Tools
{
/// <summary>
/// Editor utility to debug SaveIds for all ManagedBehaviours in the scene
/// </summary>
public class DebugSaveIds : EditorWindow
{
[MenuItem("Tools/Debug/Log All SaveIds")]
public static void LogAllSaveIds()
{
var allManaged = FindObjectsByType<ManagedBehaviour>(FindObjectsInactive.Include, FindObjectsSortMode.None);
Debug.Log($"=== Found {allManaged.Length} ManagedBehaviours ===");
foreach (var managed in allManaged)
{
if (managed.AutoRegisterForSave)
{
Debug.Log($"GameObject: {managed.gameObject.name} | Component: {managed.GetType().Name} | SaveID: {managed.SaveId}");
}
}
Debug.Log("=== End SaveIds ===");
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a34fbba4efbb4acd85d79a99abf00a08
timeCreated: 1762358959

View File

@@ -1978,11 +1978,11 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 3484825090253933040, guid: a8b0a1c6cf21352439dc24d3b03182db, type: 3}
propertyPath: m_AnchoredPosition.x
value: 1.85
value: 0.09
objectReference: {fileID: 0}
- target: {fileID: 3484825090253933040, guid: a8b0a1c6cf21352439dc24d3b03182db, type: 3}
propertyPath: m_AnchoredPosition.y
value: 5.14
value: 3.44
objectReference: {fileID: 0}
- target: {fileID: 3484825090253933040, guid: a8b0a1c6cf21352439dc24d3b03182db, type: 3}
propertyPath: m_LocalEulerAnglesHint.x

View File

@@ -105,7 +105,7 @@ GameObject:
- component: {fileID: 3487003259787903584}
- component: {fileID: 2277261512137882881}
m_Layer: 10
m_Name: LureSpotA
m_Name: LureSpotA_Slot
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
@@ -260,9 +260,9 @@ MonoBehaviour:
interactionComplete:
m_PersistentCalls:
m_Calls: []
customSaveId:
itemData: {fileID: 11400000, guid: aaf36cd26cf74334e9c7db6c1b03b3fb, type: 2}
iconRenderer: {fileID: 6258593095132504700}
slottedItemRenderer: {fileID: 4110666412151536905}
onItemSlotted:
m_PersistentCalls:
m_Calls: []
@@ -314,7 +314,6 @@ MonoBehaviour:
onForbiddenItemSlotted:
m_PersistentCalls:
m_Calls: []
slottedItemRenderer: {fileID: 4110666412151536905}
--- !u!114 &3487003259787903584
MonoBehaviour:
m_ObjectHideFlags: 0

View File

@@ -1069,7 +1069,7 @@ GameObject:
- component: {fileID: 3093816592344978065}
- component: {fileID: 8758136668472096799}
m_Layer: 10
m_Name: LureSpotB
m_Name: LureSpotB_Slot
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
@@ -1168,9 +1168,9 @@ MonoBehaviour:
interactionComplete:
m_PersistentCalls:
m_Calls: []
customSaveId:
itemData: {fileID: 11400000, guid: f97b9e24d6dceb145b56426c1152ebeb, type: 2}
iconRenderer: {fileID: 2343214996212089369}
slottedItemRenderer: {fileID: 7990414055343410434}
onItemSlotted:
m_PersistentCalls:
m_Calls: []
@@ -1234,7 +1234,6 @@ MonoBehaviour:
onForbiddenItemSlotted:
m_PersistentCalls:
m_Calls: []
slottedItemRenderer: {fileID: 7990414055343410434}
--- !u!114 &8758136668472096799
MonoBehaviour:
m_ObjectHideFlags: 0

View File

@@ -247,7 +247,7 @@ GameObject:
- component: {fileID: 3169137887822749614}
- component: {fileID: 8370367816617117734}
m_Layer: 10
m_Name: LureSpotC
m_Name: LureSpotC_Slot
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
@@ -346,9 +346,9 @@ MonoBehaviour:
interactionComplete:
m_PersistentCalls:
m_Calls: []
customSaveId:
itemData: {fileID: 11400000, guid: c68dea945fecbf44094359769db04f31, type: 2}
iconRenderer: {fileID: 2825253017896168654}
slottedItemRenderer: {fileID: 3806274462998212361}
onItemSlotted:
m_PersistentCalls:
m_Calls: []
@@ -412,7 +412,6 @@ MonoBehaviour:
onForbiddenItemSlotted:
m_PersistentCalls:
m_Calls: []
slottedItemRenderer: {fileID: 3806274462998212361}
--- !u!114 &6535246856440349519
MonoBehaviour:
m_ObjectHideFlags: 0

View File

@@ -44,10 +44,10 @@ GameObject:
- component: {fileID: 5057760771402457000}
- component: {fileID: 2433130051631076285}
- component: {fileID: 7290110366808972859}
- component: {fileID: 4831635791684479552}
- component: {fileID: 9196152289301358918}
- component: {fileID: 2596311128101197840}
m_Layer: 10
m_Name: SoundBird
m_Name: SoundBird_Slot
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
@@ -201,9 +201,9 @@ MonoBehaviour:
interactionComplete:
m_PersistentCalls:
m_Calls: []
customSaveId:
itemData: {fileID: 11400000, guid: d28f5774afad9d14f823601707150700, type: 2}
iconRenderer: {fileID: 8875860401447896107}
slottedItemRenderer: {fileID: 6941190210788968874}
onItemSlotted:
m_PersistentCalls:
m_Calls: []
@@ -231,7 +231,6 @@ MonoBehaviour:
onForbiddenItemSlotted:
m_PersistentCalls:
m_Calls: []
slottedItemRenderer: {fileID: 6941190210788968874}
--- !u!114 &7290110366808972859
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -246,18 +245,6 @@ MonoBehaviour:
m_EditorClassIdentifier:
luredBird: {fileID: 4624889622840393752}
annaLiseSpot: {fileID: 22512726373136855}
--- !u!114 &4831635791684479552
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 588897581313790951}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: eaefd3d5a2a864ca5b5d9ec5f2a7040f, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!95 &9196152289301358918
Animator:
serializedVersion: 7
@@ -280,6 +267,18 @@ Animator:
m_AllowConstantClipSamplingOptimization: 1
m_KeepAnimatorStateOnDisable: 0
m_WriteDefaultValuesOnDisable: 0
--- !u!114 &2596311128101197840
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 588897581313790951}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 95e46aacea5b42888ee7881894193c11, type: 3}
m_Name:
m_EditorClassIdentifier: AppleHillsScripts::Core.SaveLoad.AppleState
--- !u!1 &4624889622840393752
GameObject:
m_ObjectHideFlags: 0

View File

@@ -166,7 +166,6 @@ MonoBehaviour:
interactionComplete:
m_PersistentCalls:
m_Calls: []
customSaveId:
itemData: {fileID: 11400000, guid: 0c6986639ca176a419c92f5a327d95ce, type: 2}
iconRenderer: {fileID: 7494677664706785084}
--- !u!1001 &8589202998731622905

View File

@@ -140,6 +140,5 @@ MonoBehaviour:
interactionComplete:
m_PersistentCalls:
m_Calls: []
customSaveId:
itemData: {fileID: 11400000, guid: 43f22dbbb4c0eec4f8108d0f0eea43c2, type: 2}
iconRenderer: {fileID: 4055726361761331703}

View File

@@ -140,6 +140,5 @@ MonoBehaviour:
interactionComplete:
m_PersistentCalls:
m_Calls: []
customSaveId:
itemData: {fileID: 11400000, guid: a8baa800efa25a344a95b190cf349e2d, type: 2}
iconRenderer: {fileID: 4774534086162962138}

View File

@@ -140,6 +140,5 @@ MonoBehaviour:
interactionComplete:
m_PersistentCalls:
m_Calls: []
customSaveId:
itemData: {fileID: 11400000, guid: 560ba2059ce14dc4da580e2f43b2e65f, type: 2}
iconRenderer: {fileID: 4986096986936361008}

View File

@@ -140,6 +140,5 @@ MonoBehaviour:
interactionComplete:
m_PersistentCalls:
m_Calls: []
customSaveId:
itemData: {fileID: 11400000, guid: 3b1f3472171abc943bb099ce31d6fc7c, type: 2}
iconRenderer: {fileID: 4266110216568578813}

View File

@@ -221,12 +221,12 @@ GameObject:
serializedVersion: 6
m_Component:
- component: {fileID: 2071071585578300598}
- component: {fileID: 1454372124634854912}
- component: {fileID: 4122067414526815177}
- component: {fileID: 2314863751758196186}
- component: {fileID: 2741639361616064442}
- component: {fileID: 4903273501345439385}
- component: {fileID: 1054459649399154791}
- component: {fileID: 7319925080429004531}
m_Layer: 10
m_Name: Hidden
m_TagString: Untagged
@@ -252,18 +252,6 @@ Transform:
- {fileID: 852327051512792946}
m_Father: {fileID: 8259693476957892150}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1454372124634854912
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1011363502278351410}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: eaefd3d5a2a864ca5b5d9ec5f2a7040f, type: 3}
m_Name:
m_EditorClassIdentifier: PixelplacementAssembly::Pixelplacement.State
--- !u!61 &4122067414526815177
BoxCollider2D:
m_ObjectHideFlags: 0
@@ -463,6 +451,18 @@ MonoBehaviour:
audioSource: {fileID: 0}
clipPriority: 0
sourcePriority: 1
--- !u!114 &7319925080429004531
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1011363502278351410}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 95e46aacea5b42888ee7881894193c11, type: 3}
m_Name:
m_EditorClassIdentifier: AppleHillsScripts::Core.SaveLoad.AppleState
--- !u!1 &1674229500073894281
GameObject:
m_ObjectHideFlags: 0
@@ -777,11 +777,11 @@ GameObject:
m_Component:
- component: {fileID: 8259693476957892150}
- component: {fileID: 2995561023563842343}
- component: {fileID: 7053055077639234121}
- component: {fileID: 578146208477020881}
- component: {fileID: 1193493154550576580}
- component: {fileID: 7652960462502122104}
- component: {fileID: 989520896849684110}
- component: {fileID: 5862718108034728596}
m_Layer: 0
m_Name: AnneLiseBaseBush
m_TagString: Untagged
@@ -818,42 +818,6 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 55938fb1577dd4ad3af7e994048c86f6, type: 3}
m_Name:
m_EditorClassIdentifier: PixelplacementAssembly::Pixelplacement.Initialization
--- !u!114 &7053055077639234121
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5943355783477523754}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9e0b24e2f2ad54cc09940c320ed3cf4b, type: 3}
m_Name:
m_EditorClassIdentifier: PixelplacementAssembly::Pixelplacement.StateMachine
defaultState: {fileID: 1011363502278351410}
currentState: {fileID: 0}
_unityEventsFolded: 0
verbose: 0
allowReentry: 0
returnToDefaultOnDisable: 1
OnStateExited:
m_PersistentCalls:
m_Calls: []
OnStateEntered:
m_PersistentCalls:
m_Calls: []
OnFirstStateEntered:
m_PersistentCalls:
m_Calls: []
OnFirstStateExited:
m_PersistentCalls:
m_Calls: []
OnLastStateEntered:
m_PersistentCalls:
m_Calls: []
OnLastStateExited:
m_PersistentCalls:
m_Calls: []
--- !u!114 &578146208477020881
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -1001,6 +965,43 @@ MonoBehaviour:
audioSource: {fileID: 0}
clipPriority: 0
sourcePriority: 0
--- !u!114 &5862718108034728596
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5943355783477523754}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 6f56763d30b94bf6873d395a6c116eb5, type: 3}
m_Name:
m_EditorClassIdentifier: AppleHillsScripts::Core.SaveLoad.AppleMachine
defaultState: {fileID: 1011363502278351410}
currentState: {fileID: 0}
_unityEventsFolded: 0
verbose: 0
allowReentry: 0
returnToDefaultOnDisable: 1
OnStateExited:
m_PersistentCalls:
m_Calls: []
OnStateEntered:
m_PersistentCalls:
m_Calls: []
OnFirstStateEntered:
m_PersistentCalls:
m_Calls: []
OnFirstStateExited:
m_PersistentCalls:
m_Calls: []
OnLastStateEntered:
m_PersistentCalls:
m_Calls: []
OnLastStateExited:
m_PersistentCalls:
m_Calls: []
customSaveId:
--- !u!1 &6948354193133336628
GameObject:
m_ObjectHideFlags: 0

View File

@@ -11,7 +11,7 @@ GameObject:
- component: {fileID: 2326086342663433936}
- component: {fileID: 243176356944356711}
- component: {fileID: 6657093817085841540}
- component: {fileID: 7932498922414502976}
- component: {fileID: 2239999147194587249}
m_Layer: 0
m_Name: BirdEyes
m_TagString: Untagged
@@ -48,6 +48,8 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 13d59d3c42170824b8f92557822d9bf0, type: 3}
m_Name:
m_EditorClassIdentifier:
correctItemIsIn: 0
bushAnimator: {fileID: 0}
--- !u!114 &6657093817085841540
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -60,7 +62,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 55938fb1577dd4ad3af7e994048c86f6, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &7932498922414502976
--- !u!114 &2239999147194587249
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -69,9 +71,9 @@ MonoBehaviour:
m_GameObject: {fileID: 1370564349707122423}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9e0b24e2f2ad54cc09940c320ed3cf4b, type: 3}
m_Script: {fileID: 11500000, guid: 6f56763d30b94bf6873d395a6c116eb5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_EditorClassIdentifier: AppleHillsScripts::Core.SaveLoad.AppleMachine
defaultState: {fileID: 3532512445619884959}
currentState: {fileID: 0}
_unityEventsFolded: 0
@@ -96,6 +98,7 @@ MonoBehaviour:
OnLastStateExited:
m_PersistentCalls:
m_Calls: []
customSaveId:
--- !u!1 &3532512445619884959
GameObject:
m_ObjectHideFlags: 0
@@ -107,7 +110,7 @@ GameObject:
- component: {fileID: 4477179922705334961}
- component: {fileID: 3013218424693156287}
- component: {fileID: 7343439013600968102}
- component: {fileID: 3842054004304041864}
- component: {fileID: 4451815010323250894}
m_Layer: 0
m_Name: BirdHiding
m_TagString: Untagged
@@ -150,6 +153,8 @@ SpriteRenderer:
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_ForceMeshLod: -1
m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
@@ -171,6 +176,7 @@ SpriteRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 2
@@ -207,7 +213,7 @@ Animator:
m_AllowConstantClipSamplingOptimization: 1
m_KeepAnimatorStateOnDisable: 0
m_WriteDefaultValuesOnDisable: 0
--- !u!114 &3842054004304041864
--- !u!114 &4451815010323250894
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -216,9 +222,9 @@ MonoBehaviour:
m_GameObject: {fileID: 3532512445619884959}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: eaefd3d5a2a864ca5b5d9ec5f2a7040f, type: 3}
m_Script: {fileID: 11500000, guid: 95e46aacea5b42888ee7881894193c11, type: 3}
m_Name:
m_EditorClassIdentifier:
m_EditorClassIdentifier: AppleHillsScripts::Core.SaveLoad.AppleState
--- !u!1 &8828658103663197825
GameObject:
m_ObjectHideFlags: 0
@@ -230,7 +236,7 @@ GameObject:
- component: {fileID: 7698905571408300091}
- component: {fileID: 5210033153524231666}
- component: {fileID: 4408373410605328204}
- component: {fileID: 3873868413538144635}
- component: {fileID: 2709364368411520279}
m_Layer: 0
m_Name: BirdSpawned
m_TagString: Untagged
@@ -273,6 +279,8 @@ SpriteRenderer:
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_ForceMeshLod: -1
m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
@@ -294,6 +302,7 @@ SpriteRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 2
@@ -330,7 +339,7 @@ Animator:
m_AllowConstantClipSamplingOptimization: 1
m_KeepAnimatorStateOnDisable: 0
m_WriteDefaultValuesOnDisable: 0
--- !u!114 &3873868413538144635
--- !u!114 &2709364368411520279
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -339,6 +348,6 @@ MonoBehaviour:
m_GameObject: {fileID: 8828658103663197825}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: eaefd3d5a2a864ca5b5d9ec5f2a7040f, type: 3}
m_Script: {fileID: 11500000, guid: 95e46aacea5b42888ee7881894193c11, type: 3}
m_Name:
m_EditorClassIdentifier:
m_EditorClassIdentifier: AppleHillsScripts::Core.SaveLoad.AppleState

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 97f767ded753d524086106f3c39a645f
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,447 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &1439929750438628637
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1966378914653314124}
- component: {fileID: 1175208421330333144}
m_Layer: 0
m_Name: MiniGameBoosterGiver
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1966378914653314124
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1439929750438628637}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 2499229096808986326}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1175208421330333144
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1439929750438628637}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7d8f3e9a2b4c5f6d1a8e9c0b3d4f5a6b, type: 3}
m_Name:
m_EditorClassIdentifier: AppleHillsScripts::UI.CardSystem.MinigameBoosterGiver
visualContainer: {fileID: 8617171489468030463}
boosterImage: {fileID: 3680365639323743419}
glowImage: {fileID: 4006246129058447062}
continueButton: {fileID: 2988510625873934392}
hoverAmount: 20
hoverDuration: 1.5
glowPulseMin: 0.9
glowPulseMax: 1.1
glowPulseDuration: 1.2
targetBottomLeftOffset: {x: 100, y: 100}
disappearDuration: 0.8
disappearScale: 0.2
--- !u!1 &4323719263405703996
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6841858894429745291}
- component: {fileID: 1590188508543769496}
- component: {fileID: 4489841151491567959}
- component: {fileID: 2988510625873934392}
m_Layer: 0
m_Name: Button
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &6841858894429745291
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4323719263405703996}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 2499229096808986326}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0}
m_AnchorMax: {x: 0.5, y: 0}
m_AnchoredPosition: {x: 0, y: 160}
m_SizeDelta: {x: 250, y: 250}
m_Pivot: {x: 0.5, y: 0}
--- !u!222 &1590188508543769496
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4323719263405703996}
m_CullTransparentMesh: 1
--- !u!114 &4489841151491567959
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4323719263405703996}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 2636902231072113825, guid: ee014bd71cac2bc4ab845f435726f383, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!114 &2988510625873934392
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4323719263405703996}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Button
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 4489841151491567959}
m_OnClick:
m_PersistentCalls:
m_Calls: []
--- !u!1 &5931931042366245593
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4006246129058447062}
- component: {fileID: 5796229481733252802}
- component: {fileID: 6215049078676414306}
m_Layer: 0
m_Name: Glow
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &4006246129058447062
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5931931042366245593}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 2499229096808986326}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 1}
m_AnchorMax: {x: 0.5, y: 1}
m_AnchoredPosition: {x: 0, y: -250}
m_SizeDelta: {x: 800, y: 800}
m_Pivot: {x: 0.5, y: 1}
--- !u!222 &5796229481733252802
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5931931042366245593}
m_CullTransparentMesh: 1
--- !u!114 &6215049078676414306
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5931931042366245593}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: -8836962644236845764, guid: c5cc7367a37a7944abb3876352b0e0ff, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &8617171489468030463
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2499229096808986326}
m_Layer: 0
m_Name: VisualContainer
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &2499229096808986326
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8617171489468030463}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1338508664922812659}
- {fileID: 4006246129058447062}
- {fileID: 3680365639323743419}
- {fileID: 6841858894429745291}
m_Father: {fileID: 1966378914653314124}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!1 &8914844459546715980
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1338508664922812659}
- component: {fileID: 4173866009683612467}
- component: {fileID: 570826085774513514}
m_Layer: 0
m_Name: Background
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1338508664922812659
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8914844459546715980}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 2499229096808986326}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &4173866009683612467
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8914844459546715980}
m_CullTransparentMesh: 1
--- !u!114 &570826085774513514
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8914844459546715980}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
m_Material: {fileID: 0}
m_Color: {r: 0, g: 0, b: 0, a: 0.49411765}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 0}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &9035675646436554328
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3680365639323743419}
- component: {fileID: 8906420622179058179}
- component: {fileID: 3765065913677958559}
m_Layer: 0
m_Name: BoosterPack
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &3680365639323743419
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9035675646436554328}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 2499229096808986326}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 1}
m_AnchorMax: {x: 0.5, y: 1}
m_AnchoredPosition: {x: 0, y: -250}
m_SizeDelta: {x: 411, y: 729}
m_Pivot: {x: 0.5, y: 1}
--- !u!222 &8906420622179058179
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9035675646436554328}
m_CullTransparentMesh: 1
--- !u!114 &3765065913677958559
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9035675646436554328}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 4365544765984126881, guid: 9dac643e78ad86e4988c11a92f9c7a6d, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a855ba60e86bf1e449197f1f5f9b9b73
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1,43 +1,82 @@
using Core.Lifecycle;
using Core.SaveLoad;
using UnityEngine;
using Pixelplacement;
public class BirdEyesBehavior : MonoBehaviour
public class BirdEyesBehavior : ManagedBehaviour
{
private StateMachine statemachine;
private Animator animator;
// Animator Hashes
private static readonly int RightGuess = Animator.StringToHash("RightGuess");
private static readonly int WrongGuess = Animator.StringToHash("WrongGuess");
private static readonly int NoGuess = Animator.StringToHash("NoGuess");
private static readonly int Wolterisout = Animator.StringToHash("wolterisout");
private AppleMachine _statemachine;
private Animator _animator;
public bool correctItemIsIn;
[SerializeField] private Animator bushAnimator; // Assign in Inspector
// Save state
private bool _wolterisoutTriggered;
// Enable save/load participation
public override bool AutoRegisterForSave => true;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
statemachine = GetComponent<StateMachine>();
animator = GetComponentInChildren<Animator>();
_statemachine = GetComponent<AppleMachine>();
_animator = GetComponentInChildren<Animator>();
}
public void CorrectItem()
{
correctItemIsIn = true;
animator.SetTrigger("RightGuess");
_animator.SetTrigger(RightGuess);
BirdReveal();
}
public void IncorrectItem()
{
correctItemIsIn = false;
animator.SetTrigger("WrongGuess");
_animator.SetTrigger(WrongGuess);
}
public void NoItem()
{
animator.SetTrigger("NoGuess");
_animator.SetTrigger(NoGuess);
}
public void BirdReveal()
{
if (bushAnimator != null)
{
bushAnimator.SetTrigger("wolterisout");
statemachine.ChangeState("BirdSpawned");
return;
bushAnimator.SetTrigger(Wolterisout);
_wolterisoutTriggered = true;
}
statemachine.ChangeState ("BirdSpawned");
_statemachine.ChangeState("BirdSpawned");
}
protected override void OnSceneRestoreRequested(string serializedData)
{
base.OnSceneRestoreRequested(serializedData);
if (!string.IsNullOrEmpty(serializedData))
{
if (bool.TryParse(serializedData, out bool wasTriggered))
{
_wolterisoutTriggered = wasTriggered;
// If it was triggered before, set it again on restore
if (_wolterisoutTriggered && bushAnimator != null)
{
bushAnimator.SetTrigger(Wolterisout);
}
}
}
}
protected override string OnSceneSaveRequested()
{
return _wolterisoutTriggered.ToString();
}
}

View File

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

View File

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

View File

@@ -1,18 +1,18 @@
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;
using Core.SaveLoad;
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,34 +30,31 @@ namespace Bootstrap
private float _sceneLoadingProgress = 0f;
private LogVerbosity _logVerbosity = LogVerbosity.Warning;
private void Start()
{
LogDebugMessage("Boot scene started");
// Run very early - need to set up loading screen before other systems initialize
public override int ManagedAwakePriority => 5;
// Ensure the initial loading screen exists
protected override void Awake()
{
base.Awake(); // Register with LifecycleManager
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;
// 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"
);
// Subscribe to boot progress for real-time updates during bootstrap
CustomBoot.OnBootProgressChanged += OnBootProgressChanged;
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().bootstrapLogVerbosity;
@@ -68,6 +65,32 @@ namespace Bootstrap
}
}
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()
{
base.OnDestroy();
// Manual cleanup for events
if (initialLoadingScreen != null)
{
initialLoadingScreen.OnLoadingScreenFullyHidden -= OnInitialLoadingComplete;
}
CustomBoot.OnBootProgressChanged -= OnBootProgressChanged;
}
/// <summary>
/// Called when the initial loading screen is fully hidden
/// </summary>
@@ -94,23 +117,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,18 +151,6 @@ 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()
{
@@ -207,6 +201,17 @@ 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);
// Restore scene data for the main menu
if (SaveLoadManager.Instance != null)
{
LogDebugMessage($"Restoring scene data for: {mainSceneName}");
SaveLoadManager.Instance.RestoreSceneData();
}
// Step 2: Scene is fully loaded, now hide the loading screen
// This will trigger OnInitialLoadingComplete via the event when animation completes
initialLoadingScreen.HideLoadingScreen();

View File

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

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic;
using Bootstrap;
using Core;
using Core.Lifecycle;
using UI;
using UnityEngine;
using UnityEngine.AddressableAssets;
@@ -14,7 +14,7 @@ namespace Cinematics
/// <summary>
/// Handles loading, playing and unloading cinematics
/// </summary>
public class CinematicsManager : MonoBehaviour
public class CinematicsManager : ManagedBehaviour
{
public event System.Action OnCinematicStarted;
public event System.Action OnCinematicStopped;
@@ -37,21 +37,22 @@ namespace Cinematics
public PlayableDirector playableDirector;
private void Awake()
public override int ManagedAwakePriority => 170; // Cinematic 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()
{
// 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()
{
// Subscribe to application quit event to ensure cleanup

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
using System.Collections.Generic;
using UnityEngine;
using Interactions;
using Bootstrap;
using Core.Lifecycle;
using Core.SaveLoad;
namespace Core
@@ -11,7 +11,7 @@ namespace Core
/// Central registry for pickups and item slots.
/// Mirrors the singleton pattern used by PuzzleManager.
/// </summary>
public class ItemManager : MonoBehaviour
public class ItemManager : ManagedBehaviour
{
private static ItemManager _instance;
@@ -48,35 +48,32 @@ namespace Core
// Args: first item data, second item data, result item data
public event Action<PickupItemData, PickupItemData, PickupItemData> OnItemsCombined;
void Awake()
public override int ManagedAwakePriority => 75; // Item registry
private new void Awake()
{
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()
{
// 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();
}
@@ -269,7 +266,7 @@ namespace Core
// Search through all registered pickups
foreach (var pickup in _pickups)
{
if (pickup is SaveableInteractable saveable && saveable.GetSaveId() == saveId)
if (pickup is SaveableInteractable saveable && saveable.SaveId == saveId)
{
return pickup.gameObject;
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,637 @@
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 = "";
// Scene loading state tracking
private bool isLoadingScene = false;
private string sceneBeingLoaded = "";
private List<ManagedBehaviour> pendingSceneComponents = new List<ManagedBehaviour>();
[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;
// ALWAYS add to managedAwakeList - this is the master list used for save/load
InsertSorted(managedAwakeList, component, component.ManagedAwakePriority);
// Handle ManagedAwake timing based on boot state
if (isBootComplete)
{
// Check if we're currently loading a scene
if (isLoadingScene && sceneName == sceneBeingLoaded)
{
// Batch this component - will be processed in priority order when scene load completes
pendingSceneComponents.Add(component);
LogDebug($"Batched component for scene load: {component.gameObject.name} (Scene: {sceneName})");
}
else
{
// Truly late registration (component enabled after scene is ready)
// Call OnManagedAwake immediately since boot already completed
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}");
}
}
}
// If boot not complete, component stays in list and will be processed by BroadcastManagedAwake()
// 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 (and we're not in loading mode), call OnSceneReady immediately
if (!isLoadingScene && currentSceneReady == sceneName)
{
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");
// Create a copy to avoid collection modification during iteration
var componentsCopy = new List<ManagedBehaviour>(managedAwakeList);
foreach (var component in componentsCopy)
{
if (component == null) continue;
try
{
component.InvokeManagedAwake();
HandleAutoRegistrations(component);
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Error in OnManagedAwake for {component.gameObject.name}: {ex}");
}
}
// NOTE: We do NOT clear managedAwakeList here!
// This list is reused for save/load broadcasts and must persist for the lifetime of the game.
// Components are added during registration and removed during Unregister (OnDestroy).
}
/// <summary>
/// Begins scene loading mode for the specified scene.
/// Components that register during this time will be batched and processed in priority order.
/// Call this BEFORE starting to load a scene.
/// </summary>
public void BeginSceneLoad(string sceneName)
{
isLoadingScene = true;
sceneBeingLoaded = sceneName;
pendingSceneComponents.Clear();
LogDebug($"Began scene loading mode for: {sceneName}");
}
/// <summary>
/// Processes all batched components from the scene load in priority order.
/// Called automatically by BroadcastSceneReady.
/// </summary>
private void ProcessBatchedSceneComponents()
{
if (pendingSceneComponents.Count == 0)
{
isLoadingScene = false;
sceneBeingLoaded = "";
return;
}
LogDebug($"Processing {pendingSceneComponents.Count} batched components for scene: {sceneBeingLoaded}");
// Sort by ManagedAwake priority (lower values first)
pendingSceneComponents.Sort((a, b) => a.ManagedAwakePriority.CompareTo(b.ManagedAwakePriority));
// Call OnManagedAwake in priority order
foreach (var component in pendingSceneComponents)
{
if (component == null) continue;
try
{
component.InvokeManagedAwake();
HandleAutoRegistrations(component);
LogDebug($"Processed batched component: {component.gameObject.name} (Priority: {component.ManagedAwakePriority})");
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Error in OnManagedAwake for batched component {component.gameObject.name}: {ex}");
}
}
// Clear state
pendingSceneComponents.Clear();
isLoadingScene = false;
sceneBeingLoaded = "";
}
/// <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 OnSceneReady to components in the specified scene (priority order).
/// If scene loading mode is active, processes batched components first.
/// </summary>
public void BroadcastSceneReady(string sceneName)
{
LogDebug($"Broadcasting SceneReady for scene: {sceneName}");
currentSceneReady = sceneName;
// If we were in scene loading mode for this scene, process batched components first
if (isLoadingScene && sceneBeingLoaded == sceneName)
{
ProcessBatchedSceneComponents();
}
// Create a copy to avoid collection modification during iteration
var componentsCopy = new List<ManagedBehaviour>(sceneReadyList);
foreach (var component in componentsCopy)
{
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>
/// Broadcasts scene save request to all registered components that opt-in.
/// Collects and returns serialized data from components that return non-null values.
/// Called by SaveLoadManager during scene transitions.
/// </summary>
public Dictionary<string, string> BroadcastSceneSaveRequested()
{
var saveData = new Dictionary<string, string>();
// Create a copy to avoid collection modification during iteration
var componentsCopy = new List<ManagedBehaviour>(managedAwakeList);
foreach (var component in componentsCopy)
{
if (component == null || !component.AutoRegisterForSave) continue;
try
{
string serializedData = component.InvokeSceneSaveRequested();
if (!string.IsNullOrEmpty(serializedData))
{
string saveId = component.SaveId;
saveData[saveId] = serializedData;
LogDebug($"Collected scene save data from: {saveId} (Type: {component.GetType().Name})");
}
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Exception during scene save for {component.SaveId}: {ex}");
}
}
LogDebug($"Collected scene save data from {saveData.Count} components");
return saveData;
}
/// <summary>
/// Broadcasts global save request to all registered components that opt-in.
/// Collects and returns serialized data from components that return non-null values.
/// Called by SaveLoadManager when writing save file to disk (quit, manual save).
/// </summary>
public Dictionary<string, string> BroadcastGlobalSaveRequested()
{
var saveData = new Dictionary<string, string>();
// Create a copy to avoid collection modification during iteration
var componentsCopy = new List<ManagedBehaviour>(managedAwakeList);
foreach (var component in componentsCopy)
{
if (component == null || !component.AutoRegisterForSave) continue;
try
{
string serializedData = component.InvokeGlobalSaveRequested();
if (!string.IsNullOrEmpty(serializedData))
{
saveData[component.SaveId] = serializedData;
LogDebug($"Collected global save data from: {component.SaveId}");
}
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Exception during global save for {component.SaveId}: {ex}");
}
}
LogDebug($"Collected global save data from {saveData.Count} components");
return saveData;
}
/// <summary>
/// Broadcasts scene restore request to all registered components that opt-in.
/// Distributes serialized data to matching components by SaveId.
/// Called by SaveLoadManager during scene load.
/// </summary>
public void BroadcastSceneRestoreRequested(Dictionary<string, string> saveData)
{
if (saveData == null) return;
int restoredCount = 0;
// Create a copy to avoid collection modification during iteration
// (components might destroy themselves during restoration)
var componentsCopy = new List<ManagedBehaviour>(managedAwakeList);
foreach (var component in componentsCopy)
{
if (component == null || !component.AutoRegisterForSave) continue;
if (saveData.TryGetValue(component.SaveId, out string serializedData))
{
try
{
component.InvokeSceneRestoreRequested(serializedData);
restoredCount++;
LogDebug($"Restored scene data to: {component.SaveId}");
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Exception during scene restore for {component.SaveId}: {ex}");
}
}
}
LogDebug($"Restored scene data to {restoredCount} components");
}
/// <summary>
/// Broadcasts global restore request to all registered components that opt-in.
/// Distributes serialized data to matching components by SaveId.
/// Called by SaveLoadManager during initial boot load.
/// </summary>
public void BroadcastGlobalRestoreRequested(Dictionary<string, string> saveData)
{
if (saveData == null) return;
int restoredCount = 0;
// Create a copy to avoid collection modification during iteration
var componentsCopy = new List<ManagedBehaviour>(managedAwakeList);
foreach (var component in componentsCopy)
{
if (component == null || !component.AutoRegisterForSave) continue;
if (saveData.TryGetValue(component.SaveId, out string serializedData))
{
try
{
component.InvokeGlobalRestoreRequested(serializedData);
restoredCount++;
LogDebug($"Restored global data to: {component.SaveId}");
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Exception during global restore for {component.SaveId}: {ex}");
}
}
}
LogDebug($"Restored global data to {restoredCount} components");
}
/// <summary>
/// Broadcasts global load completed event to all registered components that opt-in.
/// Called ONCE after save file is successfully loaded on game boot.
/// NOT called during scene transitions.
/// </summary>
public void BroadcastGlobalLoadCompleted()
{
LogDebug("Broadcasting GlobalLoadCompleted");
// Create a copy to avoid collection modification during iteration
var componentsCopy = new List<ManagedBehaviour>(managedAwakeList);
foreach (var component in componentsCopy)
{
if (component == null || !component.AutoRegisterForSave) continue;
try
{
component.InvokeGlobalLoadCompleted();
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Exception during global load for {component.name}: {ex}");
}
}
}
/// <summary>
/// Broadcasts global save started event to all registered components that opt-in.
/// Called ONCE before save file is written to disk.
/// NOT called during scene transitions.
/// </summary>
public void BroadcastGlobalSaveStarted()
{
LogDebug("Broadcasting GlobalSaveStarted");
// Create a copy to avoid collection modification during iteration
var componentsCopy = new List<ManagedBehaviour>(managedAwakeList);
foreach (var component in componentsCopy)
{
if (component == null || !component.AutoRegisterForSave) continue;
try
{
component.InvokeGlobalSaveStarted();
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Exception during global save for {component.name}: {ex}");
}
}
}
#endregion
#region Auto-Registration
/// <summary>
/// Handle automatic registration with GameManager.
/// </summary>
private void HandleAutoRegistrations(ManagedBehaviour component)
{
// Auto-register IPausable
if (component.AutoRegisterPausable && component is AppleHills.Core.Interfaces.IPausable pausable)
{
if (GameManager.Instance != null)
{
GameManager.Instance.RegisterPausableComponent(pausable);
LogDebug($"Auto-registered IPausable: {component.gameObject.name}");
}
}
}
#endregion
#region Helper Methods
/// <summary>
/// Insert component into list maintaining sorted order by priority.
/// Uses binary search for efficient insertion.
/// </summary>
private void InsertSorted(List<ManagedBehaviour> list, ManagedBehaviour component, int priority)
{
// Simple linear insertion for now (can optimize with binary search later if needed)
int index = 0;
for (int i = 0; i < list.Count; i++)
{
int existingPriority = GetPriorityForList(list[i], list);
if (priority < existingPriority)
{
index = i;
break;
}
index = i + 1;
}
list.Insert(index, component);
}
/// <summary>
/// Get the priority value for a component based on which list it's in.
/// </summary>
private int GetPriorityForList(ManagedBehaviour component, List<ManagedBehaviour> list)
{
if (list == managedAwakeList) return component.ManagedAwakePriority;
if (list == sceneUnloadingList) return component.SceneUnloadingPriority;
if (list == sceneReadyList) return component.SceneReadyPriority;
if (list == saveRequestedList) return component.SavePriority;
if (list == restoreRequestedList) return component.RestorePriority;
if (list == destroyList) return component.DestroyPriority;
return 100;
}
/// <summary>
/// Log debug message if debug logging is enabled.
/// </summary>
private void LogDebug(string message)
{
if (enableDebugLogging)
{
Debug.Log($"[LifecycleManager] {message}");
}
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,288 @@
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;
/// <summary>
/// If true, this component participates in the save/load system.
/// Components should override OnSaveRequested() and OnRestoreRequested().
/// Default: false
/// </summary>
public virtual bool AutoRegisterForSave => false;
/// <summary>
/// Unique identifier for this component in the save system.
/// Default: "SceneName/GameObjectName/ComponentType"
/// Override ONLY for special cases (e.g., singletons like "PlayerController", or custom IDs).
/// </summary>
public virtual string SaveId
{
get
{
string sceneName = gameObject.scene.IsValid() ? gameObject.scene.name : "UnknownScene";
string componentType = GetType().Name;
return $"{sceneName}/{gameObject.name}/{componentType}";
}
}
#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 string InvokeSceneSaveRequested() => OnSceneSaveRequested();
public void InvokeSceneRestoreRequested(string data) => OnSceneRestoreRequested(data);
public string InvokeGlobalSaveRequested() => OnGlobalSaveRequested();
public void InvokeGlobalRestoreRequested(string data) => OnGlobalRestoreRequested(data);
public void InvokeManagedDestroy() => OnManagedDestroy();
public void InvokeGlobalLoadCompleted() => OnGlobalLoadCompleted();
public void InvokeGlobalSaveStarted() => OnGlobalSaveStarted();
#endregion
#region Private Fields
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()
{
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-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 during scene transitions to save scene-specific state.
/// Return serialized data (e.g., JsonUtility.ToJson(myData)).
/// Return null if component has no scene-specific state to save.
///
/// TIMING:
/// - Called BEFORE scene unload during scene transitions
/// - Frequency: Every scene transition
/// - Use for: Level progress, object positions, puzzle states
/// </summary>
protected virtual string OnSceneSaveRequested()
{
return null; // Default: no data to save
}
/// <summary>
/// Called during scene transitions to restore scene-specific state.
/// Receives previously serialized data (from OnSceneSaveRequested).
///
/// TIMING:
/// - Called AFTER scene load, during OnSceneReady phase
/// - Frequency: Every scene transition
/// - Use for: Restoring level progress, object positions, puzzle states
/// </summary>
protected virtual void OnSceneRestoreRequested(string serializedData)
{
// Default: no-op
}
/// <summary>
/// Called once on game boot to restore global persistent state.
/// Receives data that was saved via OnGlobalSaveRequested.
///
/// TIMING:
/// - Called ONCE on game boot after save file is read
/// - NOT called during scene transitions
/// - Use for: Player inventory, unlocked features, card collections
/// </summary>
protected virtual void OnGlobalRestoreRequested(string serializedData)
{
// Default: no-op
}
/// <summary>
/// Called once before game save file is written to disk.
/// Return serialized data for global persistent state.
/// Return null if component has no global state to save.
///
/// TIMING:
/// - Called ONCE before save file is written (on quit, manual save, etc.)
/// - NOT called during scene transitions
/// - Use for: Player inventory, unlocked features, card collections
/// </summary>
protected virtual string OnGlobalSaveRequested()
{
return null; // Default: no data to save
}
/// <summary>
/// Called once when game save data is initially loaded from disk.
/// Use for global managers that need to react to load completion.
/// Does NOT receive data - use OnGlobalRestoreRequested for that.
///
/// TIMING:
/// - Called ONCE on game boot after all restore operations complete
/// - NOT called during scene transitions
/// - Use for: Triggering UI updates, broadcasting load events
/// </summary>
protected virtual void OnGlobalLoadCompleted()
{
// Default: no-op
}
/// <summary>
/// Called once before save file is written to disk.
/// Use for global managers that need to perform cleanup before save.
/// Does NOT return data - use OnGlobalSaveRequested for that.
///
/// TIMING:
/// - Called ONCE before save file is written
/// - NOT called during scene transitions
/// - Use for: Final validation, cleanup operations
/// </summary>
protected virtual void OnGlobalSaveStarted()
{
// Default: no-op
}
/// <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
}
}

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
using UnityEngine;
using Pixelplacement;
using Bootstrap;
namespace Core.SaveLoad
{
@@ -82,18 +81,15 @@ namespace Core.SaveLoad
private void Start()
{
// Register with save system (no validation needed - we auto-generate ID)
BootCompletionService.RegisterInitAction(() =>
{
// Direct registration - SaveLoadManager guaranteed available (priority 25)
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.RegisterParticipant(this);
}
else
{
Debug.LogWarning($"[SaveableStateMachine] SaveLoadManager.Instance is null, cannot register '{name}'", this);
Debug.LogWarning($"[AppleMachine] SaveLoadManager not available for '{name}'", this);
}
});
}
#if UNITY_EDITOR
@@ -127,9 +123,8 @@ namespace Core.SaveLoad
return $"{sceneName}/{customSaveId}";
}
// Auto-generate from hierarchy path
string hierarchyPath = GetHierarchyPath();
return $"{sceneName}/StateMachine_{hierarchyPath}";
// Match ManagedBehaviour convention: SceneName/GameObjectName/ComponentType
return $"{sceneName}/{gameObject.name}/AppleMachine";
}
private string GetSceneName()
@@ -137,19 +132,6 @@ namespace Core.SaveLoad
return gameObject.scene.name;
}
private string GetHierarchyPath()
{
string path = gameObject.name;
Transform parent = transform.parent;
while (parent != null)
{
path = parent.name + "/" + path;
parent = parent.parent;
}
return path;
}
public string SerializeState()
{

View File

@@ -4,20 +4,20 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using AppleHills.Core.Settings;
using Bootstrap;
using Core.Lifecycle;
using UnityEngine;
namespace Core.SaveLoad
{
/// <summary>
/// Save/Load manager that follows the project's bootstrap pattern.
/// Save/Load manager that follows the project's lifecycle pattern.
/// - Singleton instance
/// - Registers a post-boot init action with BootCompletionService
/// - Inherits from ManagedBehaviour for lifecycle integration
/// - Manages participant registration for save/load operations
/// - Exposes simple async Save/Load methods
/// - Fires events on completion
/// </summary>
public class SaveLoadManager : MonoBehaviour
public class SaveLoadManager : ManagedBehaviour
{
private static SaveLoadManager _instance;
public static SaveLoadManager Instance => _instance;
@@ -43,25 +43,50 @@ 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()
{
#if UNITY_EDITOR
OnSceneLoadCompleted("RestoreInEditor");
#endif
Logging.Debug("[SaveLoadManager] Initialized");
// Load save data if save system is enabled (depends on settings from GameManager)
if (DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().useSaveLoadSystem)
{
Load();
}
}
protected override void OnSceneReady()
{
// SaveableInteractables now auto-register via ManagedBehaviour lifecycle
// No need to discover and register them manually
}
protected override string OnSceneSaveRequested()
{
// SaveLoadManager orchestrates saves, doesn't participate in them
return null;
}
protected override string OnGlobalSaveRequested()
{
// SaveLoadManager orchestrates saves, doesn't participate in them
return null;
}
private void OnApplicationQuit()
{
if (DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().useSaveLoadSystem)
@@ -70,30 +95,14 @@ namespace Core.SaveLoad
}
}
private void InitializePostBoot()
{
Logging.Debug("[SaveLoadManager] Post-boot initialization complete");
// ...existing code...
// Subscribe to scene lifecycle events if SceneManagerService is available
if (SceneManagerService.Instance != null)
protected override void OnDestroy()
{
SceneManagerService.Instance.SceneLoadCompleted += OnSceneLoadCompleted;
SceneManagerService.Instance.SceneUnloadStarted += OnSceneUnloadStarted;
Logging.Debug("[SaveLoadManager] Subscribed to SceneManagerService events");
}
}
base.OnDestroy(); // Important: call base to unregister from LifecycleManager
void OnDestroy()
{
if (_instance == this)
{
// Unsubscribe from scene events
if (SceneManagerService.Instance != null)
{
SceneManagerService.Instance.SceneLoadCompleted -= OnSceneLoadCompleted;
SceneManagerService.Instance.SceneUnloadStarted -= OnSceneUnloadStarted;
}
_instance = null;
}
}
@@ -173,40 +182,93 @@ namespace Core.SaveLoad
#endregion
#region Scene Lifecycle
#region Unlocked Minigames Management
private void OnSceneLoadCompleted(string sceneName)
/// <summary>
/// Marks a minigame as unlocked in the global save data.
/// This is separate from scene-specific participant states and persists across all saves.
/// </summary>
/// <param name="minigameName">The name/identifier of the minigame (typically scene name)</param>
public void UnlockMinigame(string minigameName)
{
Logging.Debug($"[SaveLoadManager] Scene '{sceneName}' loaded. Discovering inactive SaveableInteractables...");
if (string.IsNullOrEmpty(minigameName))
{
Logging.Warning("[SaveLoadManager] Attempted to unlock minigame with null or empty name");
return;
}
// Find ONLY INACTIVE SaveableInteractables (active ones will register themselves via Start())
var inactiveSaveables = FindObjectsByType(
typeof(Interactions.SaveableInteractable),
FindObjectsInactive.Include,
FindObjectsSortMode.None
);
if (currentSaveData == null)
{
Logging.Warning("[SaveLoadManager] Cannot unlock minigame - no save data loaded");
return;
}
int registeredCount = 0;
foreach (var obj in inactiveSaveables)
if (currentSaveData.unlockedMinigames == null)
{
var saveable = obj as Interactions.SaveableInteractable;
if (saveable != null && !saveable.gameObject.activeInHierarchy)
currentSaveData.unlockedMinigames = new System.Collections.Generic.List<string>();
}
if (!currentSaveData.unlockedMinigames.Contains(minigameName))
{
// Only register if it's actually inactive
RegisterParticipant(saveable);
registeredCount++;
currentSaveData.unlockedMinigames.Add(minigameName);
Logging.Debug($"[SaveLoadManager] Unlocked minigame: {minigameName}");
}
}
Logging.Debug($"[SaveLoadManager] Discovered and registered {registeredCount} inactive SaveableInteractables");
/// <summary>
/// Checks if a minigame has been unlocked.
/// </summary>
/// <param name="minigameName">The name/identifier of the minigame</param>
/// <returns>True if the minigame is unlocked, false otherwise</returns>
public bool IsMinigameUnlocked(string minigameName)
{
if (string.IsNullOrEmpty(minigameName))
return false;
if (currentSaveData == null || currentSaveData.unlockedMinigames == null)
return false;
return currentSaveData.unlockedMinigames.Contains(minigameName);
}
private void OnSceneUnloadStarted(string sceneName)
/// <summary>
/// Gets a read-only list of all unlocked minigames.
/// </summary>
public System.Collections.Generic.IReadOnlyList<string> GetUnlockedMinigames()
{
Logging.Debug($"[SaveLoadManager] Scene '{sceneName}' unloading. Note: Participants should unregister themselves.");
if (currentSaveData == null || currentSaveData.unlockedMinigames == null)
return new System.Collections.Generic.List<string>();
// We don't force-clear here because participants should manage their own lifecycle
// This allows for proper cleanup in OnDestroy
return currentSaveData.unlockedMinigames.AsReadOnly();
}
/// <summary>
/// Clears all save data for a specific level/scene.
/// Removes all participant states that belong to the specified scene.
/// Useful for "restart level" functionality to wipe progress.
/// </summary>
/// <param name="sceneName">The name of the scene to clear data for</param>
public void ClearLevelData(string sceneName)
{
if (string.IsNullOrEmpty(sceneName))
{
Logging.Warning("[SaveLoadManager] Cannot clear level data - scene name is null or empty");
return;
}
if (currentSaveData == null || currentSaveData.participantStates == null)
{
Logging.Warning("[SaveLoadManager] Cannot clear level data - no save data loaded");
return;
}
// SaveId format is "SceneName/ObjectName/ComponentType"
// Remove all entries that start with "sceneName/"
string scenePrefix = $"{sceneName}/";
int removedCount = currentSaveData.participantStates.RemoveAll(entry =>
entry.saveId.StartsWith(scenePrefix));
Logging.Debug($"[SaveLoadManager] Cleared {removedCount} save entries for level: {sceneName}");
}
#endregion
@@ -254,6 +316,29 @@ namespace Core.SaveLoad
return;
IsRestoringState = true;
// Build dictionary for efficient lookup
var saveDataDict = new Dictionary<string, string>();
foreach (var entry in currentSaveData.participantStates)
{
saveDataDict[entry.saveId] = entry.serializedState;
}
// NEW: Restore GLOBAL data via LifecycleManager (called ONCE on boot)
if (Lifecycle.LifecycleManager.Instance != null)
{
Lifecycle.LifecycleManager.Instance.BroadcastGlobalRestoreRequested(saveDataDict);
Logging.Debug($"[SaveLoadManager] Broadcast GLOBAL restore to LifecycleManager");
}
// NEW: Restore SCENE data via LifecycleManager (for currently loaded scenes)
if (Lifecycle.LifecycleManager.Instance != null)
{
Lifecycle.LifecycleManager.Instance.BroadcastSceneRestoreRequested(saveDataDict);
Logging.Debug($"[SaveLoadManager] Broadcast SCENE restore to LifecycleManager");
}
// EXISTING: Restore ISaveParticipants (backward compatibility)
int restoredCount = 0;
// Clear pending queue at the start
@@ -266,19 +351,17 @@ namespace Core.SaveLoad
string saveId = kvp.Key;
ISaveParticipant participant = kvp.Value;
// Find the participant state in the list
var entry = currentSaveData.participantStates.Find(e => e.saveId == saveId);
if (entry != null && !string.IsNullOrEmpty(entry.serializedState))
if (saveDataDict.TryGetValue(saveId, out string serializedState))
{
try
{
participant.RestoreState(entry.serializedState);
participant.RestoreState(serializedState);
restoredCount++;
Logging.Debug($"[SaveLoadManager] Restored state for participant: {saveId}");
Logging.Debug($"[SaveLoadManager] Restored ISaveParticipant: {saveId}");
}
catch (Exception ex)
{
Logging.Warning($"[SaveLoadManager] Exception while restoring state for '{saveId}': {ex}");
Logging.Warning($"[SaveLoadManager] Exception while restoring '{saveId}': {ex}");
}
}
}
@@ -324,7 +407,7 @@ namespace Core.SaveLoad
pendingParticipants.Clear();
IsRestoringState = false;
Logging.Debug($"[SaveLoadManager] Restored state for {restoredCount} participants + {totalPendingRestored} pending participants");
Logging.Debug($"[SaveLoadManager] Restored {restoredCount} ISaveParticipants + {totalPendingRestored} pending participants");
OnParticipantStatesRestored?.Invoke();
}
@@ -335,6 +418,115 @@ namespace Core.SaveLoad
return Path.Combine(DefaultSaveFolder, $"save_{slot}.json");
}
/// <summary>
/// Saves scene-specific data during scene transitions.
/// This updates the in-memory save data but does NOT write to disk.
/// Call Save() to persist to disk.
/// </summary>
public void SaveSceneData()
{
if (currentSaveData == null)
{
Logging.Warning("[SaveLoadManager] Cannot save scene data - no save data loaded");
return;
}
Logging.Debug("[SaveLoadManager] Saving scene-specific data...");
// Build a dictionary of all data to save
var allSceneData = new Dictionary<string, string>();
// Collect scene data from ManagedBehaviours via LifecycleManager
if (Lifecycle.LifecycleManager.Instance != null)
{
var sceneData = Lifecycle.LifecycleManager.Instance.BroadcastSceneSaveRequested();
foreach (var kvp in sceneData)
{
allSceneData[kvp.Key] = kvp.Value;
}
Logging.Debug($"[SaveLoadManager] Collected {sceneData.Count} ManagedBehaviour scene states");
}
// Collect data from ISaveParticipants (all currently registered, identified by SaveId)
foreach (var kvp in participants.ToList())
{
string saveId = kvp.Key;
ISaveParticipant participant = kvp.Value;
try
{
string serializedState = participant.SerializeState();
allSceneData[saveId] = serializedState;
Logging.Debug($"[SaveLoadManager] Captured state for ISaveParticipant: {saveId}");
}
catch (Exception ex)
{
Logging.Warning($"[SaveLoadManager] Exception while serializing ISaveParticipant '{saveId}': {ex}");
}
}
// Update existing entries or add new ones (matches SaveAsync() pattern)
if (currentSaveData.participantStates != null)
{
int updatedCount = 0;
foreach (var kvp in allSceneData)
{
var existingEntry = currentSaveData.participantStates.Find(e => e.saveId == kvp.Key);
if (existingEntry != null)
{
// Update existing entry in place
existingEntry.serializedState = kvp.Value;
}
else
{
// Add new entry
currentSaveData.participantStates.Add(new ParticipantStateEntry
{
saveId = kvp.Key,
serializedState = kvp.Value
});
}
updatedCount++;
}
Logging.Debug($"[SaveLoadManager] Updated {updatedCount} scene data entries in memory");
}
else
{
Logging.Warning("[SaveLoadManager] participantStates list is null, cannot save scene data");
}
}
/// <summary>
/// Restores scene-specific data after scene load.
/// Distributes data to components in the newly loaded scene.
/// </summary>
public void RestoreSceneData()
{
if (currentSaveData == null || currentSaveData.participantStates == null)
{
Logging.Debug("[SaveLoadManager] No scene data to restore");
return;
}
Logging.Debug("[SaveLoadManager] Restoring scene-specific data...");
// Build dictionary for efficient lookup
var saveDataDict = new Dictionary<string, string>();
foreach (var entry in currentSaveData.participantStates)
{
saveDataDict[entry.saveId] = entry.serializedState;
}
// Restore scene data via LifecycleManager
if (Lifecycle.LifecycleManager.Instance != null)
{
Lifecycle.LifecycleManager.Instance.BroadcastSceneRestoreRequested(saveDataDict);
Logging.Debug($"[SaveLoadManager] Broadcast scene restore to LifecycleManager");
}
}
/// <summary>
/// Entry point to save to a named slot. Starts an async coroutine that writes to disk.
/// Fires OnSaveCompleted when finished.
@@ -391,14 +583,44 @@ namespace Core.SaveLoad
{
currentSaveData.participantStates = new List<ParticipantStateEntry>();
}
else
// NOTE: We do NOT clear participantStates here!
// We preserve data from all scenes and update/add as needed.
// This allows Level A data to persist when saving from Level B.
int savedCount = 0;
// NEW: Broadcast global save started event (ONCE)
if (Lifecycle.LifecycleManager.Instance != null)
{
currentSaveData.participantStates.Clear();
Lifecycle.LifecycleManager.Instance.BroadcastGlobalSaveStarted();
}
// Capture state from all registered participants directly into the list
// Create a snapshot to avoid collection modification during iteration
int savedCount = 0;
// Build a dictionary of all new data to save
var allNewData = new Dictionary<string, string>();
// NEW: Collect GLOBAL data from ManagedBehaviours via LifecycleManager
if (Lifecycle.LifecycleManager.Instance != null)
{
var globalData = Lifecycle.LifecycleManager.Instance.BroadcastGlobalSaveRequested();
foreach (var kvp in globalData)
{
allNewData[kvp.Key] = kvp.Value;
}
Logging.Debug($"[SaveLoadManager] Collected {globalData.Count} GLOBAL save states");
}
// NEW: Collect SCENE data from all loaded scenes
if (Lifecycle.LifecycleManager.Instance != null)
{
var sceneData = Lifecycle.LifecycleManager.Instance.BroadcastSceneSaveRequested();
foreach (var kvp in sceneData)
{
allNewData[kvp.Key] = kvp.Value;
}
Logging.Debug($"[SaveLoadManager] Collected {sceneData.Count} SCENE save states");
}
// EXISTING: Collect data from ISaveParticipants (backward compatibility)
foreach (var kvp in participants.ToList())
{
string saveId = kvp.Key;
@@ -407,13 +629,8 @@ namespace Core.SaveLoad
try
{
string serializedState = participant.SerializeState();
currentSaveData.participantStates.Add(new ParticipantStateEntry
{
saveId = saveId,
serializedState = serializedState
});
savedCount++;
Logging.Debug($"[SaveLoadManager] Captured state for participant: {saveId}");
allNewData[saveId] = serializedState;
Logging.Debug($"[SaveLoadManager] Captured state for ISaveParticipant: {saveId}");
}
catch (Exception ex)
{
@@ -421,7 +638,28 @@ namespace Core.SaveLoad
}
}
Logging.Debug($"[SaveLoadManager] Captured state from {savedCount} participants");
// Update existing entries or add new ones (preserves data from unloaded scenes)
foreach (var kvp in allNewData)
{
var existingEntry = currentSaveData.participantStates.Find(e => e.saveId == kvp.Key);
if (existingEntry != null)
{
// Update existing entry
existingEntry.serializedState = kvp.Value;
}
else
{
// Add new entry
currentSaveData.participantStates.Add(new ParticipantStateEntry
{
saveId = kvp.Key,
serializedState = kvp.Value
});
}
savedCount++;
}
Logging.Debug($"[SaveLoadManager] Captured state from {savedCount} total participants");
json = JsonUtility.ToJson(currentSaveData, true);
@@ -540,6 +778,12 @@ namespace Core.SaveLoad
// Restore state for any already-registered participants
RestoreAllParticipantStates();
// NEW: Broadcast global load completed event (ONCE, on boot)
if (Lifecycle.LifecycleManager.Instance != null)
{
Lifecycle.LifecycleManager.Instance.BroadcastGlobalLoadCompleted();
}
OnLoadCompleted?.Invoke(slot);
Logging.Debug($"[SaveLoadManager] Load completed for slot '{slot}'");
}

View File

@@ -0,0 +1,148 @@
using UnityEngine;
using UnityEngine.Playables;
using Core.Lifecycle;
namespace Core
{
/// <summary>
/// Saveable data for PlayableDirector state
/// </summary>
[System.Serializable]
public class PlayableDirectorSaveData
{
public bool wasPlayed; // Has the timeline been played?
public bool wasCompleted; // Did it complete playback?
public double playbackTime; // Current playback position
}
/// <summary>
/// Makes a PlayableDirector (Timeline) saveable.
/// On load, if the timeline was completed, it seeks to the end to ensure
/// all timeline-activated objects are in their final state.
/// </summary>
[RequireComponent(typeof(PlayableDirector))]
public class SaveablePlayableDirector : ManagedBehaviour
{
private PlayableDirector _director;
private bool _hasPlayed = false;
private bool _hasCompleted = false;
// Enable save/load participation
public override bool AutoRegisterForSave => true;
protected override void Awake()
{
base.Awake();
_director = GetComponent<PlayableDirector>();
if (_director != null)
{
_director.stopped += OnDirectorStopped;
_director.played += OnDirectorPlayed;
}
}
protected override void OnDestroy()
{
base.OnDestroy();
if (_director != null)
{
_director.stopped -= OnDirectorStopped;
_director.played -= OnDirectorPlayed;
}
}
private void OnDirectorPlayed(PlayableDirector director)
{
_hasPlayed = true;
}
private void OnDirectorStopped(PlayableDirector director)
{
_hasCompleted = true;
}
#region Save/Load Implementation
protected override string OnSceneSaveRequested()
{
var saveData = new PlayableDirectorSaveData
{
wasPlayed = _hasPlayed,
wasCompleted = _hasCompleted,
playbackTime = _director != null ? _director.time : 0
};
return JsonUtility.ToJson(saveData);
}
protected override void OnSceneRestoreRequested(string serializedData)
{
if (string.IsNullOrEmpty(serializedData))
{
Debug.LogWarning($"[SaveablePlayableDirector] No save data to restore for {gameObject.name}");
return;
}
var saveData = JsonUtility.FromJson<PlayableDirectorSaveData>(serializedData);
if (saveData == null || _director == null)
return;
_hasPlayed = saveData.wasPlayed;
_hasCompleted = saveData.wasCompleted;
if (_hasCompleted)
{
// Seek to the end of the timeline to apply all final states
// This ensures objects activated/deactivated by the timeline are in correct state
_director.time = _director.duration;
_director.Evaluate(); // Force evaluation to apply the state
Debug.Log($"[SaveablePlayableDirector] Restored completed timeline '{gameObject.name}' - seeked to end");
}
else if (_hasPlayed && saveData.playbackTime > 0)
{
// If it was playing but not completed, restore the playback position
_director.time = saveData.playbackTime;
_director.Evaluate();
Debug.Log($"[SaveablePlayableDirector] Restored timeline '{gameObject.name}' at time {saveData.playbackTime}");
}
else
{
// Timeline hasn't been played yet, ensure it's at the start
_director.time = 0;
_director.Evaluate();
Debug.Log($"[SaveablePlayableDirector] Timeline '{gameObject.name}' not yet played - at start");
}
}
#endregion
#region Public API
/// <summary>
/// Check if this timeline has been played
/// </summary>
public bool HasPlayed => _hasPlayed;
/// <summary>
/// Check if this timeline completed playback
/// </summary>
public bool HasCompleted => _hasCompleted;
/// <summary>
/// Manually mark the timeline as completed (useful for triggering completion via code)
/// </summary>
public void MarkAsCompleted()
{
_hasCompleted = true;
_hasPlayed = true;
}
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a5c5614fc04140cb81e5bda7451f7b14
timeCreated: 1762360145

View File

@@ -2,17 +2,18 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using AppleHills.Core.Settings;
using Core.Lifecycle;
using Core.SaveLoad;
using UI;
using UnityEngine;
using UnityEngine.SceneManagement;
using Bootstrap;
namespace Core
{
/// <summary>
/// Singleton service for loading and unloading Unity scenes asynchronously, with events for progress and completion.
/// </summary>
public class SceneManagerService : MonoBehaviour
public class SceneManagerService : ManagedBehaviour
{
private LoadingScreenController _loadingScreen;
private static SceneManagerService _instance;
@@ -23,29 +24,39 @@ namespace Core
public static SceneManagerService Instance => _instance;
// Events for scene lifecycle
// NOTE: Most components should use lifecycle hooks (OnSceneReady, OnSceneUnloading)
// instead of subscribing to these events. Events are primarily for orchestration.
/// <summary>
/// Fired when a scene starts loading. Used by loading screen orchestration.
/// </summary>
public event Action<string> SceneLoadStarted;
public event Action<string, float> SceneLoadProgress;
/// <summary>
/// Fired when a scene finishes loading.
/// Used by loading screen orchestration and cross-scene components (e.g., PauseMenu).
/// For component initialization, use OnSceneReady() lifecycle hook instead.
/// </summary>
public event Action<string> SceneLoadCompleted;
public event Action<string> SceneUnloadStarted;
public event Action<string, float> SceneUnloadProgress;
public event Action<string> SceneUnloadCompleted;
private readonly Dictionary<string, AsyncOperation> _activeLoads = new();
private readonly Dictionary<string, AsyncOperation> _activeUnloads = new();
private LogVerbosity _logVerbosity = LogVerbosity.Debug;
private const string BootstrapSceneName = "BootstrapScene";
void Awake()
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 15; // Core infrastructure, after GameManager
private new void Awake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
_instance = this;
// DontDestroyOnLoad(gameObject);
// Initialize current scene tracking immediately in Awake
// Initialize current scene tracking - critical for scene management
InitializeCurrentSceneTracking();
// Register for post-boot initialization
BootCompletionService.RegisterInitAction(InitializePostBoot);
// Ensure BootstrapScene is loaded at startup
var bootstrap = SceneManager.GetSceneByName(BootstrapSceneName);
if (!bootstrap.isLoaded)
@@ -54,9 +65,17 @@ namespace Core
}
}
private void Start()
protected override void OnManagedAwake()
{
// Set up loading screen reference and events
// This must happen in ManagedAwake because LoadingScreenController instance needs to be set first
_loadingScreen = LoadingScreenController.Instance;
SetupLoadingScreenEvents();
// Load verbosity settings
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().sceneLogVerbosity;
LogDebugMessage($"SceneManagerService initialized, current scene is: {CurrentGameplayScene}");
}
/// <summary>
@@ -90,17 +109,6 @@ namespace Core
}
}
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()
{
if (_loadingScreen == null) return;
@@ -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
@@ -280,9 +283,9 @@ namespace Core
// Tracks the currently loaded gameplay scene (not persistent/bootstrapper)
public string CurrentGameplayScene { get; set; } = "AppleHillsOverworld";
public async Task ReloadCurrentScene(IProgress<float> progress = null, bool autoHideLoadingScreen = true)
public async Task ReloadCurrentScene(IProgress<float> progress = null, bool autoHideLoadingScreen = true, bool skipSave = false)
{
await SwitchSceneAsync(CurrentGameplayScene, progress, autoHideLoadingScreen);
await SwitchSceneAsync(CurrentGameplayScene, progress, autoHideLoadingScreen, skipSave);
}
/// <summary>
@@ -291,15 +294,45 @@ namespace Core
/// <param name="newSceneName">Name of the scene to load</param>
/// <param name="progress">Optional progress reporter</param>
/// <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)
/// <param name="skipSave">If true, skips saving scene data during transition. Useful for level restart to prevent re-saving cleared data.</param>
public async Task SwitchSceneAsync(string newSceneName, IProgress<float> progress = null, bool autoHideLoadingScreen = true, bool skipSave = false)
{
// 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: Save scene-specific data via SaveLoadManager (unless skipSave is true)
if (!skipSave && SaveLoadManager.Instance != null)
{
var debugSettings = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>();
if (debugSettings.useSaveLoadSystem)
{
LogDebugMessage($"Saving scene data for: {oldSceneName}");
SaveLoadManager.Instance.SaveSceneData();
}
}
else if (skipSave)
{
LogDebugMessage($"Skipping save for: {oldSceneName} (skipSave=true)");
}
// PHASE 4: Clear PuzzleManager state before scene transition
if (PuzzleS.PuzzleManager.Instance != null)
{
LogDebugMessage($"Clearing puzzle state before scene transition");
PuzzleS.PuzzleManager.Instance.ClearPuzzleState();
}
// 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 +341,56 @@ 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: Begin scene loading mode - enables priority-ordered component initialization
LogDebugMessage($"Beginning scene load for: {newSceneName}");
LifecycleManager.Instance?.BeginSceneLoad(newSceneName);
// PHASE 9: Load new gameplay scene
await LoadSceneAsync(newSceneName, progress);
// Update tracker
CurrentGameplayScene = newSceneName;
// Only hide the loading screen if autoHideLoadingScreen is true
// PHASE 10: Broadcast scene ready - processes batched components in priority order, then calls OnSceneReady
LogDebugMessage($"Broadcasting OnSceneReady for: {newSceneName}");
LifecycleManager.Instance?.BroadcastSceneReady(newSceneName);
// PHASE 11: Restore scene-specific data via SaveLoadManager
if (!skipSave && SaveLoadManager.Instance != null)
{
var debugSettings = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>();
if (debugSettings.useSaveLoadSystem)
{
LogDebugMessage($"Restoring scene data for: {newSceneName}");
SaveLoadManager.Instance.RestoreSceneData();
}
}
else if (skipSave)
{
LogDebugMessage($"Skipping restore for: {newSceneName} (skipSave=true)");
}
// PHASE 12: Only hide the loading screen if autoHideLoadingScreen is true
if (autoHideLoadingScreen && _loadingScreen != null)
{
_loadingScreen.HideLoadingScreen();

View File

@@ -1,15 +1,13 @@
using System;
using System.Collections;
using System.Collections;
using AppleHills.Core.Settings;
using Bootstrap;
using Input;
using Core.Lifecycle;
using Settings;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Core
{
public class SceneOrientationEnforcer : MonoBehaviour
public class SceneOrientationEnforcer : ManagedBehaviour
{
// Singleton instance
private static SceneOrientationEnforcer _instance;
@@ -20,40 +18,56 @@ 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");
}
protected override void OnManagedAwake()
{
// Subscribe to SceneManagerService to enforce orientation on every scene load
if (SceneManagerService.Instance != null)
{
SceneManagerService.Instance.SceneLoadCompleted += OnSceneLoadCompleted;
}
#if UNITY_EDITOR
// When playing in the editor, manually invoke OnSceneLoaded for the currently active scene
// When playing in the editor, manually invoke orientation check for the currently active scene
if (Application.isPlaying)
{
OnSceneLoaded(SceneManager.GetActiveScene(), LoadSceneMode.Single);
HandleSceneOrientation(SceneManager.GetActiveScene().name);
}
#endif
}
private void Start()
protected override void OnSceneReady()
{
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().sceneLogVerbosity;
// Handle orientation when scene is ready (initial scene)
// Note: This fires for the scene that just loaded, LifecycleManager tracks which scene
string sceneName = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name;
HandleSceneOrientation(sceneName);
}
private void InitializePostBoot()
private void OnSceneLoadCompleted(string sceneName)
{
// Initialize any dependencies that require other services to be ready
LogDebugMessage("Post-boot initialization complete");
// Subscribe to sceneLoaded event
SceneManager.sceneLoaded += OnSceneLoaded;
// Enforce orientation every time a scene is loaded via SceneManagerService
HandleSceneOrientation(sceneName);
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
private void HandleSceneOrientation(string sceneName)
{
// Determine desired orientation for this scene
string sceneName = scene.name;
ScreenOrientationRequirement requirement = ScreenOrientationRequirement.NotApplicable;
if (sceneName.ToLower().Contains("bootstrap"))
@@ -91,9 +105,15 @@ namespace Core
}
}
void OnDestroy()
protected override void OnDestroy()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
// Unsubscribe from events to prevent memory leaks
if (SceneManagerService.Instance != null)
{
SceneManagerService.Instance.SceneLoadCompleted -= OnSceneLoadCompleted;
}
base.OnDestroy(); // Important: call base
}
/// <summary>

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using UnityEngine;
using Interactions;
namespace AppleHills.Core.Settings
{
@@ -88,5 +89,31 @@ namespace AppleHills.Core.Settings
}
return null;
}
/// <summary>
/// Finds a pickup prefab by its itemData.itemId.
/// Searches through combination rules to find result prefabs.
/// Used to spawn dynamically created items during save/load.
/// </summary>
public GameObject FindPickupPrefabByItemId(string itemId)
{
if (string.IsNullOrEmpty(itemId) || combinationRules == null)
return null;
// Search through combination rules to find a result prefab with matching itemId
foreach (var rule in combinationRules)
{
if (rule.resultPrefab != null)
{
var pickup = rule.resultPrefab.GetComponent<Pickup>();
if (pickup != null && pickup.itemData != null && pickup.itemData.itemId == itemId)
{
return rule.resultPrefab;
}
}
}
return null;
}
}
}

View File

@@ -57,6 +57,7 @@ namespace AppleHills.Core.Settings
// Methods to query item configurations
CombinationRule GetCombinationRule(PickupItemData item1, PickupItemData item2);
SlotItemConfig GetSlotItemConfig(PickupItemData slotItem);
GameObject FindPickupPrefabByItemId(string itemId);
}
/// <summary>

View File

@@ -1,9 +1,9 @@
using UnityEngine;
using System;
using System.Collections;
using Core;
using Pathfinding;
// TODO: Remove movement based logic
public class AnneLiseBehaviour : MonoBehaviour
{
[SerializeField] public float moveSpeed;

View File

@@ -1,5 +1,6 @@
using UnityEngine;
// TODO: Remove this
public class LureSpot : MonoBehaviour
{
[SerializeField] public GameObject luredBird;

View File

@@ -2,6 +2,7 @@ using UnityEngine;
using Pixelplacement;
using System.Collections;
using Core;
using Core.SaveLoad;
using UnityEngine.Audio;
public class PicnicBehaviour : MonoBehaviour
@@ -12,7 +13,7 @@ public class PicnicBehaviour : MonoBehaviour
public float getFlirtyMin = 1f;
public float getFlirtyMax = 3f;
private StateMachine stateMachine;
private AppleMachine stateMachine;
private Animator animator;
[Header("The FakeChocolate to destroy!")]
@@ -32,7 +33,7 @@ public class PicnicBehaviour : MonoBehaviour
void Awake()
{
stateMachine = GetComponent<StateMachine>();
stateMachine = GetComponent<AppleMachine>();
animator = GetComponent<Animator>();
_audioSource = GetComponent<AppleAudioSource>();
}

View File

@@ -97,90 +97,7 @@ AnimationClip:
- time: 1.5833334
value: {fileID: -1414182512, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
attribute: m_Sprite
path: SoundBird
classID: 212
script: {fileID: 0}
flags: 2
- serializedVersion: 2
curve:
- time: 0
value: {fileID: -1035714051, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 0.033333335
value: {fileID: -740831527, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 0.05
value: {fileID: -648204482, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 0.11666667
value: {fileID: -960280295, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 0.13333334
value: {fileID: -1144832505, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 0.2
value: {fileID: -1860215682, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 0.25
value: {fileID: 519773293, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 0.26666668
value: {fileID: -1067281986, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 0.33333334
value: {fileID: -36811272, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 0.38333333
value: {fileID: -1592089404, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 0.41666666
value: {fileID: -1729322987, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 0.45
value: {fileID: -91858778, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 0.5
value: {fileID: -26124593, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 0.53333336
value: {fileID: 259088195, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 0.6
value: {fileID: 1746085375, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 0.6166667
value: {fileID: -182272111, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 0.68333334
value: {fileID: 1436667360, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 0.73333335
value: {fileID: 545467259, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 0.75
value: {fileID: 121392657, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 0.8
value: {fileID: 938631806, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 0.8333333
value: {fileID: 1943282875, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 0.8833333
value: {fileID: -1918772169, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 0.93333334
value: {fileID: -1252794517, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 0.96666664
value: {fileID: -927331073, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 1.0166667
value: {fileID: -1038168376, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 1.0833334
value: {fileID: 1855149249, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 1.1
value: {fileID: -2116798272, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 1.1666666
value: {fileID: 2078607702, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 1.1833333
value: {fileID: -633261939, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 1.2333333
value: {fileID: -86103801, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 1.2833333
value: {fileID: 1380056380, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 1.3166667
value: {fileID: 1797284751, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 1.3666667
value: {fileID: 2004539437, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 1.4166666
value: {fileID: 1984933759, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 1.45
value: {fileID: -89013944, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 1.5
value: {fileID: 1990407029, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 1.5166667
value: {fileID: 1094948637, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- time: 1.5833334
value: {fileID: -1414182512, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
attribute: m_Sprite
path: SoundBirdTakeoff/SoundBirdTakeOffAnim
path:
classID: 212
script: {fileID: 0}
flags: 2
@@ -192,16 +109,7 @@ AnimationClip:
m_ClipBindingConstant:
genericBindings:
- serializedVersion: 2
path: 1707885837
attribute: 0
script: {fileID: 0}
typeID: 212
customType: 23
isPPtrCurve: 1
isIntCurve: 0
isSerializeReferenceCurve: 0
- serializedVersion: 2
path: 631576921
path: 0
attribute: 0
script: {fileID: 0}
typeID: 212
@@ -248,44 +156,6 @@ AnimationClip:
- {fileID: 1990407029, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: 1094948637, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: -1414182512, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: -1035714051, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: -740831527, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: -648204482, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: -960280295, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: -1144832505, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: -1860215682, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: 519773293, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: -1067281986, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: -36811272, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: -1592089404, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: -1729322987, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: -91858778, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: -26124593, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: 259088195, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: 1746085375, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: -182272111, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: 1436667360, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: 545467259, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: 121392657, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: 938631806, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: 1943282875, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: -1918772169, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: -1252794517, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: -927331073, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: -1038168376, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: 1855149249, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: -2116798272, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: 2078607702, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: -633261939, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: -86103801, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: 1380056380, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: 1797284751, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: 2004539437, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: 1984933759, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: -89013944, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: 1990407029, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: 1094948637, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
- {fileID: -1414182512, guid: 9d1670b18fc5fa8459596f1ddd4a4bd7, type: 3}
m_AnimationClipSettings:
serializedVersion: 2
m_AdditiveReferencePoseClip: {fileID: 0}

View File

@@ -1,4 +1,5 @@
using Core;
using Core.SaveLoad;
using Pixelplacement;
using UnityEngine;
@@ -8,7 +9,7 @@ public class SoundGenerator : MonoBehaviour
[SerializeField] private Sprite exitSprite;
[SerializeField] private AudioClip enterSound;
[SerializeField] private AppleAudioSource audioSource;
[SerializeField] private StateMachine soundBirdSMRef;
[SerializeField] private AppleMachine soundBirdSMRef;
[SerializeField] private soundBird_CanFly soundbirdHearingCheck;
private bool playerInside = false;
@@ -37,7 +38,7 @@ public class SoundGenerator : MonoBehaviour
{
audioSource.audioSource.PlayOneShot(enterSound);
}
if (soundBirdSMRef != null && soundBirdSMRef.currentState.name == "SoundBird" && soundbirdHearingCheck.canFly == true)
if (soundBirdSMRef != null && soundBirdSMRef.currentState.name.ToLower().Contains("soundbird_slot") && soundbirdHearingCheck.canFly == true)
{
soundBirdSMRef.ChangeState("SoundBirdTakeoff");

View File

@@ -1,6 +1,7 @@
using UnityEngine;
using Unity.Cinemachine;
using System.Collections;
using Core.SaveLoad;
using Pixelplacement;
public class cameraSwitcher : MonoBehaviour
@@ -12,7 +13,7 @@ public class cameraSwitcher : MonoBehaviour
[SerializeField] private float transitionDuration = 0.5f; // Duration of the transition
[SerializeField] private soundBird_FlyingBehaviour flyingBehaviour;
[SerializeField] private soundBird_TakeOffBehaviour takeOffBehaviour; // New reference
[SerializeField] private StateMachine birdStateMachine;
[SerializeField] private AppleMachine birdStateMachine;
private int playerInsideCount = 0;
private Coroutine zoomCoroutine;
@@ -32,6 +33,9 @@ public class cameraSwitcher : MonoBehaviour
private void OnTriggerExit2D(Collider2D other)
{
if (!gameObject.activeInHierarchy)
return;
if (other.CompareTag("Player"))
{
playerInsideCount--;

View File

@@ -1,23 +1,51 @@
using UnityEngine;
using Core.Lifecycle;
public class soundBird_CanFly : MonoBehaviour
[System.Serializable]
public class SoundBirdSaveData
{
public bool canFly;
}
public class soundBird_CanFly : ManagedBehaviour
{
public bool canFly = true;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
}
// Enable save/load participation
public override bool AutoRegisterForSave => true;
public void birdCanHear(bool canhear)
{
if (canhear)
canFly = canhear;
}
#region Save/Load Implementation
protected override string OnSceneSaveRequested()
{
canFly = true;
}
else
var saveData = new SoundBirdSaveData
{
canFly = false;
canFly = this.canFly
};
return JsonUtility.ToJson(saveData);
}
protected override void OnSceneRestoreRequested(string serializedData)
{
if (string.IsNullOrEmpty(serializedData))
{
Debug.LogWarning($"[soundBird_CanFly] No save data to restore for {gameObject.name}");
return;
}
var saveData = JsonUtility.FromJson<SoundBirdSaveData>(serializedData);
if (saveData != null)
{
canFly = saveData.canFly;
Debug.Log($"[soundBird_CanFly] Restored canFly state: {canFly}");
}
}
#endregion
}

View File

@@ -1,3 +1,4 @@
using Core.SaveLoad;
using Pixelplacement;
using Pixelplacement.TweenSystem;
using UnityEngine;
@@ -10,7 +11,7 @@ public class soundBird_FlyingBehaviour : MonoBehaviour
public float flightDelay;
public float cooldownTime;
private StateMachine stateMachine;
private AppleMachine stateMachine;
private Animator animator;
private TweenBase objectTween;
//private Coroutine cooldownCoroutine;
@@ -21,7 +22,7 @@ public class soundBird_FlyingBehaviour : MonoBehaviour
void Awake()
{
stateMachine = GetComponentInParent<StateMachine>();
stateMachine = GetComponentInParent<AppleMachine>();
animator = GetComponentInParent<Animator>();
}

View File

@@ -1,3 +1,4 @@
using Core.SaveLoad;
using Pixelplacement;
using Pixelplacement.TweenSystem;
using UnityEngine;
@@ -9,7 +10,7 @@ public class soundBird_LandingBehaviour1 : MonoBehaviour
public float flightDuration;
public float flightDelay;
public soundBird_FlyingBehaviour flyingBehaviour;
private StateMachine stateMachine;
private AppleMachine stateMachine;
private Animator animator;
private TweenBase objectTween;
@@ -18,7 +19,7 @@ public class soundBird_LandingBehaviour1 : MonoBehaviour
void Awake()
{
stateMachine = GetComponentInParent<StateMachine>();
stateMachine = GetComponentInParent<AppleMachine>();
animator = GetComponentInParent<Animator>();
}
@@ -52,7 +53,7 @@ public class soundBird_LandingBehaviour1 : MonoBehaviour
if (stateMachine != null)
{
animator.SetBool("isScared", false);
stateMachine.ChangeState("SoundBird"); // Change to the desired state name
stateMachine.ChangeState(0); // Change to the desired state name
}
}

View File

@@ -1,3 +1,4 @@
using Core.SaveLoad;
using Pixelplacement;
using Pixelplacement.TweenSystem;
using UnityEngine;
@@ -9,7 +10,7 @@ public class soundBird_TakeOffBehaviour : MonoBehaviour
public Transform SoundBirdObject;
public float flightDuration;
public float flightDelay;
private StateMachine stateMachine;
private AppleMachine stateMachine;
private Animator animator;
private TweenBase objectTween;
public soundBird_FlyingBehaviour flyingBehaviour;
@@ -18,7 +19,7 @@ public class soundBird_TakeOffBehaviour : MonoBehaviour
void Awake()
{
stateMachine = GetComponentInParent<StateMachine>();
stateMachine = GetComponentInParent<AppleMachine>();
animator = GetComponentInParent<Animator>();
}
// Start is called once before the first execution of Update after the MonoBehaviour is created

View File

@@ -1,9 +1,9 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using AppleHills.Data.CardSystem;
using Bootstrap;
using Core;
using Core.Lifecycle;
using Core.SaveLoad;
using UnityEngine;
@@ -11,14 +11,19 @@ namespace Data.CardSystem
{
/// <summary>
/// Manages the player's card collection, booster packs, and related operations.
/// Uses a singleton pattern for global access.
/// Implements ISaveParticipant to integrate with the save/load system.
/// Manages the card collection system for the game.
/// Handles unlocking cards, tracking collections, and integrating with the save/load system.
/// </summary>
public class CardSystemManager : MonoBehaviour, ISaveParticipant
public class CardSystemManager : ManagedBehaviour
{
private static CardSystemManager _instance;
public static CardSystemManager Instance => _instance;
// Save system configuration
public override bool AutoRegisterForSave => true;
public override string SaveId => "CardSystemManager";
[Header("Card Collection")]
[SerializeField] private List<CardDefinition> availableCards = new List<CardDefinition>();
@@ -30,7 +35,8 @@ namespace Data.CardSystem
private HashSet<string> _placedInAlbumCardIds = new HashSet<string>();
// Dictionary to quickly look up card definitions by ID
private Dictionary<string, CardDefinition> _definitionLookup = new Dictionary<string, CardDefinition>();
private Dictionary<string, CardDefinition> _definitionLookup;
private bool _lookupInitialized = false;
// Event callbacks using System.Action
public event Action<List<CardData>> OnBoosterOpened;
@@ -40,20 +46,22 @@ namespace Data.CardSystem
public event Action<CardData> OnPendingCardAdded;
public event Action<CardData> OnCardPlacedInAlbum;
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()
{
// Load card definitions from Addressables, then register with save system
LoadCardDefinitionsFromAddressables();
}
Logging.Debug("[CardSystemManager] Post-boot initialization complete");
protected override void OnManagedAwake()
{
Logging.Debug("[CardSystemManager] Initialized");
}
/// <summary>
@@ -88,17 +96,6 @@ namespace Data.CardSystem
BuildDefinitionLookup();
Logging.Debug($"[CardSystemManager] Loaded {availableCards.Count} card definitions from Addressables");
// NOW register with save/load system (definitions are ready for state restoration)
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.RegisterParticipant(this);
Logging.Debug("[CardSystemManager] Registered with SaveLoadManager after definitions loaded");
}
else
{
Logging.Warning("[CardSystemManager] SaveLoadManager not available for registration");
}
}
else
{
@@ -106,20 +103,17 @@ namespace Data.CardSystem
}
}
private void OnDestroy()
{
// Unregister from save/load system
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.UnregisterParticipant(GetSaveId());
}
}
/// <summary>
/// Builds a lookup dictionary for quick access to card definitions by ID
/// </summary>
private void BuildDefinitionLookup()
{
if (_definitionLookup == null)
{
_definitionLookup = new Dictionary<string, CardDefinition>();
}
_definitionLookup.Clear();
foreach (var cardDef in availableCards)
@@ -139,6 +133,8 @@ namespace Data.CardSystem
cardData.SetDefinition(def);
}
}
_lookupInitialized = true;
}
/// <summary>
@@ -647,10 +643,16 @@ namespace Data.CardSystem
/// <summary>
/// Apply a previously saved snapshot to the runtime inventory
/// </summary>
public void ApplyCardCollectionState(CardCollectionState state)
public async void ApplyCardCollectionState(CardCollectionState state)
{
if (state == null) return;
// Wait for lookup to be initialized before loading
while (!_lookupInitialized)
{
await System.Threading.Tasks.Task.Yield();
}
playerInventory.ClearAllCards();
_pendingRevealCards.Clear();
_placedInAlbumCardIds.Clear();
@@ -701,42 +703,19 @@ namespace Data.CardSystem
}
}
#region ISaveParticipant Implementation
#region Save/Load Lifecycle Hooks
private bool hasBeenRestored;
/// <summary>
/// Returns true if this participant has already had its state restored.
/// </summary>
public bool HasBeenRestored => hasBeenRestored;
/// <summary>
/// Returns the unique save ID for the CardSystemManager.
/// Since this is a singleton global system, the ID is constant.
/// </summary>
public string GetSaveId()
{
return "CardSystemManager";
}
/// <summary>
/// Serializes the current card collection state to JSON.
/// </summary>
public string SerializeState()
protected override string OnGlobalSaveRequested()
{
var state = ExportCardCollectionState();
return JsonUtility.ToJson(state);
}
/// <summary>
/// Restores the card collection state from serialized JSON data.
/// </summary>
public void RestoreState(string serializedData)
protected override void OnGlobalRestoreRequested(string serializedData)
{
if (string.IsNullOrEmpty(serializedData))
{
Logging.Debug("[CardSystemManager] No saved state to restore, using defaults");
hasBeenRestored = true;
return;
}
@@ -746,7 +725,6 @@ namespace Data.CardSystem
if (state != null)
{
ApplyCardCollectionState(state);
hasBeenRestored = true;
Logging.Debug("[CardSystemManager] Successfully restored card collection state");
}
else

View File

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

View File

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

View File

@@ -2,8 +2,8 @@
using Pathfinding;
using AppleHills.Core.Settings;
using Core;
using Core.Lifecycle;
using Core.SaveLoad;
using Bootstrap;
namespace Input
{
@@ -21,7 +21,7 @@ namespace Input
/// Handles player movement in response to tap and hold input events.
/// Supports both direct and pathfinding movement modes, and provides event/callbacks for arrival/cancellation.
/// </summary>
public class PlayerTouchController : MonoBehaviour, ITouchInputConsumer, ISaveParticipant
public class PlayerTouchController : ManagedBehaviour, ITouchInputConsumer
{
// --- Movement State ---
private Vector3 targetPosition;
@@ -67,10 +67,13 @@ namespace Input
private bool interruptMoveTo;
private LogVerbosity _logVerbosity = LogVerbosity.Warning;
// Save system tracking
private bool hasBeenRestored;
// Save system configuration
public override bool AutoRegisterForSave => true;
// Scene-specific SaveId - each level has its own player state
public override string SaveId => $"{gameObject.scene.name}/PlayerController";
public override int ManagedAwakePriority => 100; // Player controller
void Awake()
protected override void OnManagedAwake()
{
aiPath = GetComponent<AIPath>();
artTransform = transform.Find("CharacterArt");
@@ -87,39 +90,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
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.RegisterParticipant(this);
Logging.Debug("[PlayerTouchController] Registered with SaveLoadManager");
}
else
{
Logging.Warning("[PlayerTouchController] SaveLoadManager not available for registration");
}
}
void OnDestroy()
{
// Unregister from save system
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.UnregisterParticipant(GetSaveId());
}
}
/// <summary>
/// Handles tap input. Always uses pathfinding to move to the tapped location.
/// Cancels any in-progress MoveToAndNotify.
@@ -457,16 +433,9 @@ namespace Input
}
}
#region ISaveParticipant Implementation
#region Save/Load Lifecycle Hooks
public bool HasBeenRestored => hasBeenRestored;
public string GetSaveId()
{
return "PlayerController";
}
public string SerializeState()
protected override string OnSceneSaveRequested()
{
var saveData = new PlayerSaveData
{
@@ -476,12 +445,11 @@ namespace Input
return JsonUtility.ToJson(saveData);
}
public void RestoreState(string serializedData)
protected override void OnSceneRestoreRequested(string serializedData)
{
if (string.IsNullOrEmpty(serializedData))
{
Logging.Debug("[PlayerTouchController] No saved state to restore");
hasBeenRestored = true;
return;
}
@@ -492,7 +460,6 @@ namespace Input
{
transform.position = saveData.worldPosition;
transform.rotation = saveData.worldRotation;
hasBeenRestored = true;
Logging.Debug($"[PlayerTouchController] Restored position: {saveData.worldPosition}");
}
}

View File

@@ -5,6 +5,7 @@ using System.Collections.Generic;
using UnityEngine.Events;
using System.Threading.Tasks;
using Core;
using Core.Lifecycle;
namespace Interactions
{
@@ -20,7 +21,7 @@ namespace Interactions
/// Base class for interactable objects that can respond to tap input events.
/// Subclasses should override OnCharacterArrived() to implement interaction-specific logic.
/// </summary>
public class InteractableBase : MonoBehaviour, ITouchInputConsumer
public class InteractableBase : ManagedBehaviour, ITouchInputConsumer
{
[Header("Interaction Settings")]
public bool isOneTime;
@@ -33,21 +34,16 @@ namespace Interactions
public UnityEvent characterArrived;
public UnityEvent<bool> interactionComplete;
// Helpers for managing interaction state
private bool _interactionInProgress;
protected PlayerTouchController _playerRef;
protected FollowerController _followerController;
private bool _isActive = true;
private InteractionEventType _currentEventType;
private PlayerTouchController playerRef;
protected FollowerController FollowerController;
private bool isActive = true;
// Action component system
private List<InteractionActionBase> _registeredActions = new List<InteractionActionBase>();
private void Awake()
{
// Subscribe to interactionComplete event
interactionComplete.AddListener(OnInteractionComplete);
}
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 100; // Gameplay base classes
/// <summary>
/// Register an action component with this interactable
@@ -73,14 +69,12 @@ namespace Interactions
/// </summary>
private async Task DispatchEventAsync(InteractionEventType eventType)
{
_currentEventType = eventType;
// Collect all tasks from actions that want to respond
List<Task<bool>> tasks = new List<Task<bool>>();
foreach (var action in _registeredActions)
{
Task<bool> task = action.OnInteractionEvent(eventType, _playerRef, _followerController);
Task<bool> task = action.OnInteractionEvent(eventType, playerRef, FollowerController);
if (task != null)
{
tasks.Add(task);
@@ -97,39 +91,178 @@ namespace Interactions
/// <summary>
/// Handles tap input. Triggers interaction logic.
/// Can be overridden for fully custom interaction logic.
/// </summary>
public void OnTap(Vector2 worldPosition)
public virtual void OnTap(Vector2 worldPosition)
{
if (!_isActive)
// 1. High-level validation
if (!CanBeClicked())
{
Logging.Debug($"[Interactable] Is disabled!");
return;
return; // Silent failure
}
Logging.Debug($"[Interactable] OnTap at {worldPosition} on {gameObject.name}");
// Start the interaction process asynchronously
_ = TryInteractAsync();
_ = StartInteractionFlowAsync();
}
private async Task TryInteractAsync()
/// <summary>
/// Template method that orchestrates the entire interaction flow.
/// </summary>
private async Task StartInteractionFlowAsync()
{
_interactionInProgress = true;
// 2. Find characters
playerRef = FindFirstObjectByType<PlayerTouchController>();
FollowerController = FindFirstObjectByType<FollowerController>();
_playerRef = FindFirstObjectByType<PlayerTouchController>();
_followerController = FindFirstObjectByType<FollowerController>();
// 3. Virtual hook: Setup
OnInteractionStarted();
interactionStarted?.Invoke(_playerRef, _followerController);
// Dispatch the InteractionStarted event to action components
// 4. Fire events
interactionStarted?.Invoke(playerRef, FollowerController);
await DispatchEventAsync(InteractionEventType.InteractionStarted);
// After all InteractionStarted actions complete, proceed to player movement
await StartPlayerMovementAsync();
// 5. Orchestrate character movement
await MoveCharactersAsync();
// 6. Virtual hook: Arrival reaction
OnInteractingCharacterArrived();
// 7. Fire arrival events
characterArrived?.Invoke();
await DispatchEventAsync(InteractionEventType.InteractingCharacterArrived);
// 8. Validation (base + child)
var (canProceed, errorMessage) = ValidateInteraction();
if (!canProceed)
{
if (!string.IsNullOrEmpty(errorMessage))
{
DebugUIMessage.Show(errorMessage, Color.yellow);
}
FinishInteraction(false);
return;
}
private async Task StartPlayerMovementAsync()
// 9. Virtual main logic: Do the thing!
bool success = DoInteraction();
// 10. Finish up
FinishInteraction(success);
}
#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));
// Wait for player to arrive
var tcs = new TaskCompletionSource<bool>();
void OnPlayerArrivedLocal()
{
if (playerRef != null)
{
playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal;
playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal;
}
tcs.TrySetResult(true);
}
void OnPlayerMoveCancelledLocal()
{
// First remove both event handlers to prevent memory leaks
if (_playerRef != null)
if (playerRef != null)
{
_playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal;
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal;
playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal;
playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal;
}
_ = HandleInteractionCancelledAsync();
tcs.TrySetResult(false);
}
// Then handle the cancellation
OnPlayerMoveCancelledAsync().ContinueWith(_ => tcs.TrySetResult(false));
}
playerRef.OnArrivedAtTarget += OnPlayerArrivedLocal;
playerRef.OnMoveToCancelled += OnPlayerMoveCancelledLocal;
playerRef.MoveToAndNotify(stopPoint);
// 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;
}
/// <summary>
/// Moves the follower to the interaction point or custom target.
/// </summary>
private async Task MoveFollowerAsync()
{
if (FollowerController == null)
return;
// 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;
}
}
private async Task OnPlayerMoveCancelledAsync()
// Wait for follower to arrive
var tcs = new TaskCompletionSource<bool>();
void OnFollowerArrivedLocal()
{
if (FollowerController != null)
{
FollowerController.OnPickupArrived -= OnFollowerArrivedLocal;
}
// Tell follower to return to player
if (FollowerController != null && playerRef != null)
{
FollowerController.ReturnToPlayer(playerRef.transform);
}
tcs.TrySetResult(true);
}
FollowerController.OnPickupArrived += OnFollowerArrivedLocal;
FollowerController.GoToPoint(targetPosition);
await tcs.Task;
}
/// <summary>
/// Handles interaction being cancelled (player stopped moving).
/// </summary>
private async Task HandleInteractionCancelledAsync()
{
_interactionInProgress = false;
interactionInterrupted?.Invoke();
await DispatchEventAsync(InteractionEventType.InteractionInterrupted);
}
private async Task OnPlayerArrivedAsync()
{
if (!_interactionInProgress)
return;
#endregion
// Dispatch PlayerArrived event
await DispatchEventAsync(InteractionEventType.PlayerArrived);
// After all PlayerArrived actions complete, proceed to character interaction
await HandleCharacterInteractionAsync();
}
private async Task HandleCharacterInteractionAsync()
{
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)
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)
{
if (action is InteractionTimelineAction timelineAction &&
timelineAction.respondToEvents.Contains(InteractionEventType.InteractingCharacterArrived) &&
timelineAction.pauseInteractionFlow)
{
break;
}
}
// Tell the follower to return to the player
if (_followerController != null && _playerRef != null)
{
_followerController.ReturnToPlayer(_playerRef.transform);
}
// 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);
// 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;
}
#region Finalization
/// <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.
/// Finalizes the interaction after DoInteraction completes.
/// </summary>
protected virtual void OnCharacterArrived()
private async void FinishInteraction(bool success)
{
// Default implementation does nothing - subclasses should override
// and call CompleteInteraction when their logic is complete
}
// Virtual hook: Cleanup
OnInteractionFinished(success);
private async void OnInteractionComplete(bool success)
{
// Dispatch InteractionComplete event
// 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();
@@ -496,24 +501,7 @@ namespace Interactions
throw new NotImplementedException();
}
/// <summary>
/// Call this from subclasses to mark the interaction as complete.
/// </summary>
/// <param name="success">Whether the interaction was successful</param>
protected void CompleteInteraction(bool success)
{
interactionComplete?.Invoke(success);
}
/// <summary>
/// Legacy method for backward compatibility. Use CompleteInteraction instead.
/// </summary>
/// TODO: Remove this method in future versions
[Obsolete("Use CompleteInteraction instead")]
public void BroadcastInteractionComplete(bool success)
{
CompleteInteraction(success);
}
#endregion
#if UNITY_EDITOR
/// <summary>

View File

@@ -19,32 +19,40 @@ 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;
public string slottedItemDataId; // ItemId of the PickupItemData (for verification)
}
// 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;
@@ -63,117 +71,198 @@ namespace Interactions
// 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");
if (iconRenderer == null)
iconRenderer = GetComponent<SpriteRenderer>();
ApplyItemData();
}
#endif
var heldItemData = _followerController.CurrentlyHeldItemData;
var heldItemObj = _followerController.GetHeldPickupObject();
var config = _interactionSettings?.GetSlotItemConfig(itemData);
/// <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>();
// 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 (PickupItemData.ListContainsEquivalent(forbidden, heldItem))
return (false, "Can't place that here.");
}
SlotItem(heldItemObj, heldItemData, true);
return;
return (true, null);
}
// Either pickup or swap items
if ((heldItemData == null && _currentlySlottedItemObject != null)
|| (heldItemData != null && _currentlySlottedItemObject != null))
/// <summary>
/// Main interaction logic: Slot, pickup, swap, or combine items.
/// Returns true only if correct item was slotted.
/// </summary>
protected override bool DoInteraction()
{
// If both held and slotted items exist, attempt combination via follower (reuse existing logic from Pickup)
if (heldItemData != null && _currentlySlottedItemData != null)
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)
{
var slottedPickup = _currentlySlottedItemObject?.GetComponent<Pickup>();
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>();
if (slottedPickup != null)
{
var comboResult = _followerController.TryCombineItems(slottedPickup, out var combinationResultItem);
if (combinationResultItem != null && 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;
var comboResult = FollowerController.TryCombineItems(slottedPickup, out var combinationResultItem);
// Clear internal references and visuals
_currentlySlottedItemObject = null;
_currentlySlottedItemData = null;
if (comboResult == FollowerController.CombinationResult.Successful)
{
// Combination succeeded - clear slot and return false (not a "slot success")
ClearSlot();
return false;
}
}
}
// 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;
}
// 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;
// Clear the pickup's OwningSlot reference
if (currentlySlottedItemObject != null)
{
var pickup = currentlySlottedItemObject.GetComponent<Pickup>();
if (pickup != null)
{
pickup.OwningSlot = null;
}
}
currentlySlottedItemObject = null;
currentlySlottedItemData = null;
currentState = ItemSlotState.None;
UpdateSlottedSprite();
CompleteInteraction(false);
return;
}
}
}
// No combination (or not applicable) -> perform normal swap/pickup behavior
_followerController.TryPickupItem(_currentlySlottedItemObject, _currentlySlottedItemData, false);
// Fire removal events
onItemSlotRemoved?.Invoke();
OnItemSlotRemoved?.Invoke(_currentlySlottedItemData);
_currentState = ItemSlotState.None;
SlotItem(heldItemObj, heldItemData, _currentlySlottedItemObject == null);
return;
OnItemSlotRemoved?.Invoke(previousData);
}
// No held item, slot empty -> show warning
if (heldItemData == null && _currentlySlottedItemObject == null)
{
DebugUIMessage.Show("This requires an item.", Color.red);
return;
}
}
#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 +280,18 @@ namespace Interactions
}
}
// Register with ItemManager when enabled
protected override void Start()
{
base.Start(); // This calls Pickup.Start() which registers with save system
#endregion
// Additionally register as ItemSlot
// Register with ItemManager when enabled
private void OnEnable()
{
// Register as ItemSlot
ItemManager.Instance?.RegisterItemSlot(this);
}
protected override void OnDestroy()
{
base.OnDestroy(); // Unregister from save system and pickup manager
base.OnDestroy();
// Unregister from slot manager
ItemManager.Instance?.UnregisterItemSlot(this);
@@ -212,35 +301,30 @@ 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 = "";
string slottedDataId = "";
if (_currentlySlottedItemObject != null)
if (currentlySlottedItemObject != null)
{
var slottedPickup = _currentlySlottedItemObject.GetComponent<Pickup>();
var slottedPickup = currentlySlottedItemObject.GetComponent<Pickup>();
if (slottedPickup is SaveableInteractable saveablePickup)
{
slottedSaveId = saveablePickup.GetSaveId();
slottedSaveId = saveablePickup.SaveId;
}
}
if (_currentlySlottedItemData != null)
// Also save the itemData ID for verification
if (currentlySlottedItemData != null)
{
#if UNITY_EDITOR
slottedAssetPath = UnityEditor.AssetDatabase.GetAssetPath(_currentlySlottedItemData);
#endif
}
slottedDataId = currentlySlottedItemData.itemId;
}
return new ItemSlotSaveData
{
pickupData = baseData,
slotState = _currentState,
slotState = currentState,
slottedItemSaveId = slottedSaveId,
slottedItemDataAssetPath = slottedAssetPath
slottedItemDataId = slottedDataId
};
}
@@ -253,20 +337,14 @@ 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);
Debug.Log($"[ItemSlot] Restoring slotted item: {data.slottedItemSaveId} (itemId: {data.slottedItemDataId})");
RestoreSlottedItem(data.slottedItemSaveId, data.slottedItemDataId);
}
}
@@ -274,117 +352,193 @@ 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, string expectedItemDataId)
{
// Try to find the item in the scene by its save ID via ItemManager
GameObject slottedObject = ItemManager.Instance?.FindPickupBySaveId(slottedItemSaveId);
if (slottedObject == null)
if (slottedObject == null && !string.IsNullOrEmpty(expectedItemDataId))
{
// Item not found in scene - it might be a dynamically spawned combined item
// Try to spawn it from the itemDataId
Debug.Log($"[ItemSlot] Slotted item not found in scene: {slottedItemSaveId}, attempting to spawn from itemId: {expectedItemDataId}");
GameObject prefab = interactionSettings?.FindPickupPrefabByItemId(expectedItemDataId);
if (prefab != null)
{
// Spawn the item (inactive, since it will be slotted)
slottedObject = Instantiate(prefab, transform.position, Quaternion.identity);
slottedObject.SetActive(false);
Debug.Log($"[ItemSlot] Successfully spawned combined item for slot: {expectedItemDataId}");
}
else
{
Debug.LogWarning($"[ItemSlot] Could not find prefab for itemId: {expectedItemDataId}");
return;
}
}
else if (slottedObject == null)
{
Debug.LogWarning($"[ItemSlot] Could not find slotted item with save ID: {slottedItemSaveId}");
return;
}
// Get the item data
// Get the item data from the pickup component
PickupItemData slottedData = null;
#if UNITY_EDITOR
if (!string.IsNullOrEmpty(slottedItemDataAssetPath))
{
slottedData = UnityEditor.AssetDatabase.LoadAssetAtPath<PickupItemData>(slottedItemDataAssetPath);
}
#endif
if (slottedData == null)
{
var pickup = slottedObject.GetComponent<Pickup>();
if (pickup != null)
{
slottedData = pickup.itemData;
// Verify itemId matches if we have it (safety check)
if (slottedData != null && !string.IsNullOrEmpty(expectedItemDataId))
{
if (slottedData.itemId != expectedItemDataId)
{
Debug.LogWarning($"[ItemSlot] ItemId mismatch! Pickup has '{slottedData.itemId}' but expected '{expectedItemDataId}'");
}
}
if (slottedData == null)
{
Debug.LogWarning($"[ItemSlot] Pickup {pickup.gameObject.name} has null itemData! Expected itemId: {expectedItemDataId}");
if (slottedObject != null)
Destroy(slottedObject);
return;
}
}
else
{
Debug.LogWarning($"[ItemSlot] Slotted object has no Pickup component: {slottedObject.name}");
if (slottedObject != null)
Destroy(slottedObject);
return;
}
// Silently slot the item (no events, no interaction completion)
// Follower state is managed separately during save/load restoration
ApplySlottedItemState(slottedObject, slottedData, triggerEvents: false);
Debug.Log($"[ItemSlot] Successfully restored slotted item: {slottedData.itemName} (itemId: {slottedData.itemId})");
}
/// <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 - also clear the pickup's OwningSlot reference
if (currentlySlottedItemObject != null)
{
var oldPickup = currentlySlottedItemObject.GetComponent<Pickup>();
if (oldPickup != null)
{
oldPickup.OwningSlot = null;
}
}
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;
currentlySlottedItemData = itemToSlotData;
// Mark the pickup as picked up and track slot ownership for save/load
var pickup = itemToSlot.GetComponent<Pickup>();
if (pickup != null)
{
pickup.IsPickedUp = true;
pickup.OwningSlot = this;
}
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);
// 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)
{
DebugUIMessage.Show("You correctly slotted " + itemToSlotData.itemName + " into: " + itemData.itemName, Color.green);
onCorrectItemSlotted?.Invoke();
OnCorrectItemSlotted?.Invoke(itemData, _currentlySlottedItemData);
_currentState = ItemSlotState.Correct;
}
currentState = ItemSlotState.Correct;
CompleteInteraction(true);
// Fire events if requested
if (triggerEvents)
{
DebugUIMessage.Show($"You correctly slotted {itemToSlotData.itemName} into: {itemData.itemName}", Color.green);
onCorrectItemSlotted?.Invoke();
OnCorrectItemSlotted?.Invoke(itemData, currentlySlottedItemData);
}
}
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;
}
CompleteInteraction(false);
OnIncorrectItemSlotted?.Invoke(itemData, currentlySlottedItemData);
}
}
}
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);
}
/// <summary>
/// Bilateral restoration entry point: Pickup calls this to offer itself to the Slot.
/// Returns true if claim was successful, false if slot already has an item or wrong pickup.
/// </summary>
public bool TryClaimSlottedItem(Pickup pickup)
{
if (pickup == null)
return false;
// If slot already has an item, reject the claim
if (currentlySlottedItemObject != null)
{
Debug.LogWarning($"[ItemSlot] Already has a slotted item, rejecting claim from {pickup.gameObject.name}");
return false;
}
// Verify this pickup's SaveId matches what we expect (from our save data)
// Note: We don't have easy access to the expected SaveId here, so we just accept it
// The Pickup's bilateral restoration ensures it only claims the correct slot
// Claim the pickup
ApplySlottedItemState(pickup.gameObject, pickup.itemData, triggerEvents: false);
Debug.Log($"[ItemSlot] Successfully claimed slotted item: {pickup.itemData?.itemName}");
return true;
}
#endregion

View File

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

View File

@@ -1,20 +1,20 @@
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 bool wasInSlot; // NEW: Was this pickup in a slot?
public string slotSaveId; // NEW: Which slot held this pickup?
public Vector3 worldPosition;
public Quaternion worldRotation;
public bool isActive;
@@ -24,19 +24,14 @@ 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;
// Track which slot owns this pickup (for bilateral restoration)
internal ItemSlot OwningSlot { get; set; }
// 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
@@ -47,28 +42,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(() =>
protected override void OnManagedAwake()
{
ItemManager.Instance?.RegisterPickup(this);
});
}
/// <summary>
/// Unity OnDestroy callback. Unregisters from ItemManager.
/// </summary>
protected override void OnDestroy()
{
base.OnDestroy(); // Unregister from save system
base.OnDestroy();
// Unregister from ItemManager
ItemManager.Instance?.UnregisterPickup(this);
@@ -103,65 +86,58 @@ namespace Interactions
}
}
#region Interaction Logic
/// <summary>
/// Override: Called when character arrives at the interaction point.
/// Handles item pickup and combination logic.
/// Main interaction logic: Try combination, then try pickup.
/// </summary>
protected override void OnCharacterArrived()
protected override bool DoInteraction()
{
Logging.Debug("[Pickup] OnCharacterArrived");
Logging.Debug("[Pickup] DoInteraction");
var combinationResult = _followerController.TryCombineItems(this, out var combinationResultItem);
if (combinationResultItem != null)
{
CompleteInteraction(true);
// 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);
// 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();
// Mark this pickup as picked up (consumed in combination) to prevent restoration
IsPickedUp = true;
if (heldItem != null)
{
var heldPickup = heldItem.GetComponent<Pickup>();
if (heldPickup != null && heldPickup.itemData != null)
{
// Trigger the combination event
OnItemsCombined?.Invoke(itemData, heldPickup.itemData, resultItemData);
}
}
}
// Combination succeeded - original items destroyed, result picked up by TryCombineItems
FireCombinationEvent(resultItem, heldItemData);
return true;
}
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);
// Update pickup state and invoke event when the item was picked up successfully
if (wasPickedUp)
{
// 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
protected override object GetSerializableState()
@@ -169,10 +145,16 @@ namespace Interactions
// Check if this pickup is currently held by the follower
bool isHeldByFollower = IsPickedUp && !gameObject.activeSelf && transform.parent != null;
// Check if this pickup is in a slot
bool isInSlot = OwningSlot != null;
string slotId = isInSlot && OwningSlot is SaveableInteractable saveableSlot ? saveableSlot.SaveId : "";
return new PickupSaveData
{
isPickedUp = this.IsPickedUp,
wasHeldByFollower = isHeldByFollower,
wasInSlot = isInSlot,
slotSaveId = slotId,
worldPosition = transform.position,
worldRotation = transform.rotation,
isActive = gameObject.activeSelf
@@ -207,6 +189,20 @@ namespace Interactions
follower.TryClaimHeldItem(this);
}
}
// If this was in a slot, try bilateral restoration with the slot
else if (data.wasInSlot && !string.IsNullOrEmpty(data.slotSaveId))
{
// Try to give this pickup to the slot
var slot = FindSlotBySaveId(data.slotSaveId);
if (slot != null)
{
slot.TryClaimSlottedItem(this);
}
else
{
Debug.LogWarning($"[Pickup] Could not find slot with SaveId: {data.slotSaveId}");
}
}
}
else
{
@@ -220,6 +216,28 @@ namespace Interactions
// This prevents duplicate logic execution
}
/// <summary>
/// Find an ItemSlot by its SaveId (for bilateral restoration).
/// </summary>
private ItemSlot FindSlotBySaveId(string slotSaveId)
{
if (string.IsNullOrEmpty(slotSaveId)) return null;
// Get all ItemSlots from ItemManager
var allSlots = ItemManager.Instance?.GetAllItemSlots();
if (allSlots == null) return null;
foreach (var slot in allSlots)
{
if (slot is SaveableInteractable saveable && saveable.SaveId == slotSaveId)
{
return slot;
}
}
return null;
}
/// <summary>
/// Resets the pickup state when the item is dropped back into the world.
/// Called by FollowerController when swapping items.

View File

@@ -1,6 +1,4 @@
using Core.SaveLoad;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine;
namespace Interactions
{
@@ -8,21 +6,13 @@ namespace Interactions
/// Base class for interactables that participate in the save/load system.
/// Provides common save ID generation and serialization infrastructure.
/// </summary>
public abstract class SaveableInteractable : InteractableBase, ISaveParticipant
public abstract class SaveableInteractable : InteractableBase
{
[Header("Save System")]
[SerializeField]
[Tooltip("Optional custom save ID. If empty, will auto-generate from hierarchy path.")]
private string customSaveId = "";
/// <summary>
/// Sets a custom save ID for this interactable.
/// Used when spawning dynamic objects that need stable save IDs.
/// </summary>
public void SetCustomSaveId(string saveId)
{
customSaveId = saveId;
}
// Save system configuration
public override bool AutoRegisterForSave => true;
/// <summary>
/// Flag to indicate we're currently restoring from save data.
@@ -30,99 +20,10 @@ namespace Interactions
/// </summary>
protected bool IsRestoringFromSave { get; private set; }
private bool hasRegistered;
private bool hasRestoredState;
/// <summary>
/// Returns true if this participant has already had its state restored.
/// </summary>
public bool HasBeenRestored => hasRestoredState;
#region Save/Load Lifecycle Hooks
protected virtual void Awake()
{
// Register early in Awake so even disabled objects are tracked
RegisterWithSaveSystem();
}
protected virtual void Start()
{
// If we didn't register in Awake (shouldn't happen), register now
if (!hasRegistered)
{
RegisterWithSaveSystem();
}
}
protected virtual void OnDestroy()
{
UnregisterFromSaveSystem();
}
private void RegisterWithSaveSystem()
{
if (hasRegistered) return;
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.RegisterParticipant(this);
hasRegistered = true;
// Check if save data was already loaded before we registered
// If so, we need to subscribe to the next load event
if (!SaveLoadManager.Instance.IsSaveDataLoaded && !hasRestoredState)
{
SaveLoadManager.Instance.OnLoadCompleted += OnSaveDataLoadedHandler;
}
}
else
{
Debug.LogWarning($"[SaveableInteractable] SaveLoadManager not found for {gameObject.name}");
}
}
private void UnregisterFromSaveSystem()
{
if (!hasRegistered) return;
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.UnregisterParticipant(GetSaveId());
SaveLoadManager.Instance.OnLoadCompleted -= OnSaveDataLoadedHandler;
hasRegistered = false;
}
}
/// <summary>
/// Event handler for when save data finishes loading.
/// Called if the object registered before save data was loaded.
/// </summary>
private void OnSaveDataLoadedHandler(string slot)
{
// The SaveLoadManager will automatically call RestoreState on us
// We just need to unsubscribe from the event
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.OnLoadCompleted -= OnSaveDataLoadedHandler;
}
}
#region ISaveParticipant Implementation
public string GetSaveId()
{
string sceneName = GetSceneName();
if (!string.IsNullOrEmpty(customSaveId))
{
return $"{sceneName}/{customSaveId}";
}
// Auto-generate from hierarchy path
string hierarchyPath = GetHierarchyPath();
return $"{sceneName}/{hierarchyPath}";
}
public string SerializeState()
protected override string OnSceneSaveRequested()
{
object stateData = GetSerializableState();
if (stateData == null)
@@ -133,28 +34,17 @@ namespace Interactions
return JsonUtility.ToJson(stateData);
}
public void RestoreState(string serializedData)
protected override void OnSceneRestoreRequested(string serializedData)
{
if (string.IsNullOrEmpty(serializedData))
{
Debug.LogWarning($"[SaveableInteractable] Empty save data for {GetSaveId()}");
return;
}
// CRITICAL: Only restore state if we're actually in a restoration context
// This prevents state machines from teleporting objects when they enable them mid-gameplay
if (SaveLoadManager.Instance != null && !SaveLoadManager.Instance.IsRestoringState)
{
// If we're not in an active restoration cycle, this is probably a late registration
// (object was disabled during initial load and just got enabled)
// Skip restoration to avoid mid-gameplay teleportation
Debug.Log($"[SaveableInteractable] Skipping late restoration for {GetSaveId()} - object enabled after initial load");
hasRestoredState = true; // Mark as restored to prevent future attempts
Debug.LogWarning($"[SaveableInteractable] Empty save data for {SaveId}");
return;
}
// OnSceneRestoreRequested is guaranteed by the lifecycle system to only fire during actual restoration
// No need to check IsRestoringState - the lifecycle manager handles timing deterministically
IsRestoringFromSave = true;
hasRestoredState = true;
try
{
@@ -162,7 +52,7 @@ namespace Interactions
}
catch (System.Exception e)
{
Debug.LogError($"[SaveableInteractable] Failed to restore state for {GetSaveId()}: {e.Message}");
Debug.LogError($"[SaveableInteractable] Failed to restore state for {SaveId}: {e.Message}");
}
finally
{
@@ -189,61 +79,22 @@ namespace Interactions
#endregion
#region Helper Methods
private string GetSceneName()
{
Scene scene = gameObject.scene;
if (!scene.IsValid())
{
Debug.LogWarning($"[SaveableInteractable] GameObject {gameObject.name} has invalid scene");
return "UnknownScene";
}
return scene.name;
}
private string GetHierarchyPath()
{
// Build path from scene root to this object
// Format: ParentName/ChildName/ObjectName_SiblingIndex
string path = gameObject.name;
Transform current = transform.parent;
while (current != null)
{
path = $"{current.name}/{path}";
current = current.parent;
}
// Add sibling index for uniqueness among same-named objects
int siblingIndex = transform.GetSiblingIndex();
if (siblingIndex > 0)
{
path = $"{path}_{siblingIndex}";
}
return path;
}
#endregion
#region Editor Helpers
#if UNITY_EDITOR
[ContextMenu("Log Save ID")]
private void LogSaveId()
{
Debug.Log($"Save ID: {GetSaveId()}");
Debug.Log($"Save ID: {SaveId}");
}
[ContextMenu("Test Serialize/Deserialize")]
private void TestSerializeDeserialize()
{
string serialized = SerializeState();
string serialized = OnSceneSaveRequested();
Debug.Log($"Serialized state: {serialized}");
RestoreState(serialized);
OnSceneRestoreRequested(serialized);
Debug.Log("Deserialization test complete");
}
#endif

View File

@@ -6,8 +6,6 @@ using Interactions;
using System.Threading.Tasks;
using UnityEngine;
// Added for IInteractionSettings
namespace Levels
{
/// <summary>
@@ -15,23 +13,20 @@ 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 GameObject _menuObjectRef;
/// <summary>
/// Unity Awake callback. Sets up icon, interactable, and event handlers.
/// </summary>
void Awake()
protected override void Awake()
{
switchActive = true;
base.Awake();
Debug.Log($"[LevelSwitch] Awake called for {gameObject.name} in scene {gameObject.scene.name}");
if (_iconRenderer == null)
_iconRenderer = GetComponent<SpriteRenderer>();
@@ -41,6 +36,16 @@ namespace Levels
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.
@@ -68,34 +73,40 @@ namespace Levels
}
/// <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;
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)
var menuGo = Instantiate(menuPrefab);
var menu = menuGo.GetComponent<LevelSwitchMenu>();
// Spawn the menu overlay
_menuObjectRef = Instantiate(menuPrefab);
var menu = _menuObjectRef.GetComponent<LevelSwitchMenu>();
if (menu == null)
{
Debug.LogError("LevelSwitchMenu component missing on prefab!");
Destroy(menuGo);
return;
Destroy(_menuObjectRef);
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()
@@ -117,13 +128,20 @@ namespace Levels
private async void OnRestartSelected()
{
// TODO: Restart level here
await OnLevelSelected();
// Clear all save data for the target level before reloading
if (Core.SaveLoad.SaveLoadManager.Instance != null && !string.IsNullOrEmpty(switchData?.targetLevelSceneName))
{
Core.SaveLoad.SaveLoadManager.Instance.ClearLevelData(switchData.targetLevelSceneName);
Logging.Debug($"[LevelSwitch] Cleared save data for level: {switchData.targetLevelSceneName}");
}
// Now reload the level with fresh state - skipSave=true prevents re-saving cleared data
var progress = new Progress<float>(p => Logging.Debug($"Loading progress: {p * 100:F0}%"));
await SceneManagerService.Instance.SwitchSceneAsync(switchData.targetLevelSceneName, progress, autoHideLoadingScreen: true, skipSave: true);
}
private void OnMenuCancel()
{
switchActive = true; // Allow interaction again if cancelled
InputManager.Instance.SetInputMode(InputMode.GameAndUI);
}
}

View File

@@ -172,6 +172,7 @@ namespace Levels
_onRestart?.Invoke();
if (popupConfirmMenu) popupConfirmMenu.SetActive(false);
if (tintTargetImage) tintTargetImage.color = _originalTintColor;
Destroy(gameObject);
}
private void OnScrollToMinigameClicked()
@@ -255,9 +256,9 @@ namespace Levels
if (_switchData == null)
return;
var data = SaveLoadManager.Instance?.currentSaveData;
// Use the new public API to check unlock status
string minigameName = _switchData.targetMinigameSceneName;
bool unlocked = data?.unlockedMinigames != null && !string.IsNullOrEmpty(minigameName) && data.unlockedMinigames.Contains(minigameName);
bool unlocked = SaveLoadManager.Instance != null && SaveLoadManager.Instance.IsMinigameUnlocked(minigameName);
// Show/hide padlock
if (padlockImage) padlockImage.gameObject.SetActive(!unlocked);

View File

@@ -4,19 +4,16 @@ using Core;
using Input;
using Interactions;
using System.Threading.Tasks;
using Bootstrap;
using PuzzleS;
using UnityEngine;
using Core.SaveLoad;
// Added for IInteractionSettings
namespace Levels
{
/// <summary>
/// Saveable data for MinigameSwitch state
/// </summary>
[System.Serializable]
[Serializable]
public class MinigameSwitchSaveData
{
public bool isUnlocked;
@@ -50,9 +47,7 @@ namespace Levels
/// </summary>
protected override void Awake()
{
base.Awake(); // Register with save system
BootCompletionService.RegisterInitAction(InitializePostBoot);
base.Awake();
switchActive = true;
if (iconRenderer == null)
@@ -69,9 +64,9 @@ namespace Levels
ApplySwitchData();
}
protected override void Start()
protected override void OnManagedAwake()
{
base.Start(); // Register with save system
base.OnManagedAwake();
// If startUnlocked is true, always start active
if (startUnlocked)
@@ -80,7 +75,11 @@ namespace Levels
return;
}
// Otherwise, if not restoring from save, start inactive
if (PuzzleManager.Instance != null)
{
PuzzleManager.Instance.OnAllPuzzlesComplete += HandleAllPuzzlesComplete;
}
if (!IsRestoringFromSave && !isUnlocked)
{
gameObject.SetActive(false);
@@ -89,7 +88,12 @@ namespace Levels
protected override void OnDestroy()
{
base.OnDestroy(); // Unregister from save system
base.OnDestroy();
if (PuzzleManager.Instance != null)
{
PuzzleManager.Instance.OnAllPuzzlesComplete -= HandleAllPuzzlesComplete;
}
}
private void HandleAllPuzzlesComplete(PuzzleS.PuzzleLevelDataSO _)
@@ -98,7 +102,13 @@ namespace Levels
isUnlocked = true;
gameObject.SetActive(true);
// Save will happen automatically on next save cycle via ISaveParticipant
// Add to global unlocked minigames list
if (switchData != null && !string.IsNullOrEmpty(switchData.targetLevelSceneName))
{
Core.SaveLoad.SaveLoadManager.Instance?.UnlockMinigame(switchData.targetLevelSceneName);
}
// Save will happen automatically on next save cycle via SaveableInteractable
}
#if UNITY_EDITOR
@@ -128,34 +138,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()
@@ -175,10 +207,6 @@ namespace Levels
InputManager.Instance.SetInputMode(InputMode.GameAndUI);
}
private void InitializePostBoot()
{
PuzzleManager.Instance.OnAllPuzzlesComplete += HandleAllPuzzlesComplete;
}
#region Save/Load Implementation
@@ -209,6 +237,12 @@ namespace Levels
isUnlocked = data.isUnlocked;
// Sync with global unlocked minigames list
if (isUnlocked && switchData != null && !string.IsNullOrEmpty(switchData.targetLevelSceneName))
{
Core.SaveLoad.SaveLoadManager.Instance?.UnlockMinigame(switchData.targetLevelSceneName);
}
// Show/hide based on unlock state
gameObject.SetActive(isUnlocked);
}

View File

@@ -2,14 +2,13 @@
using AppleHills.Core.Settings;
using Cinematics;
using Core;
using Core.Lifecycle;
using Input;
using Minigames.DivingForPictures.PictureCamera;
using System;
using System.Collections;
using System.Collections.Generic;
using Bootstrap;
using Minigames.DivingForPictures.Bubbles;
using UI;
using UI.Core;
using UnityEngine;
using UnityEngine.Events;
@@ -17,7 +16,7 @@ using UnityEngine.Playables;
namespace Minigames.DivingForPictures
{
public class DivingGameManager : MonoBehaviour, IPausable
public class DivingGameManager : ManagedBehaviour, IPausable
{
[Header("Monster Prefabs")]
[Tooltip("Array of monster prefabs to spawn randomly")]
@@ -104,10 +103,12 @@ 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 Awake()
{
_settings = GameManager.GetSettingsObject<IDivingMinigameSettings>();
_currentSpawnProbability = _settings?.BaseSpawnProbability ?? 0.2f;
base.Awake();
if (_instance == null)
{
@@ -117,20 +118,29 @@ namespace Minigames.DivingForPictures
{
Destroy(gameObject);
}
}
protected override void OnManagedAwake()
{
_settings = GameManager.GetSettingsObject<IDivingMinigameSettings>();
_currentSpawnProbability = _settings?.BaseSpawnProbability ?? 0.2f;
// Ensure any previous run state is reset when this manager awakes
_isGameOver = false;
Logging.Debug("[DivingGameManager] Initialized");
}
private void Start()
protected override void OnSceneReady()
{
// Register for post-boot initialization
BootCompletionService.RegisterInitAction(InitializePostBoot);
InitializeGame();
// Subscribe to player damage events (this doesn't depend on initialization)
// Subscribe to scene-specific events
CinematicsManager.Instance.OnCinematicStopped += EndGame;
PlayerCollisionBehavior.OnDamageTaken += OnPlayerDamageTaken;
OnMonsterSpawned += DoMonsterSpawned;
// Validate rope references (this doesn't depend on initialization)
// Validate rope references
ValidateRopeReferences();
viewfinderManager = CameraViewfinderManager.Instance;
@@ -151,31 +161,19 @@ namespace Minigames.DivingForPictures
RegisterExemptFromPhotoSequencePausing(viewfinderPausable);
}
}
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;
OnMonsterSpawned -= DoMonsterSpawned;
// Unregister from GameManager
if (GameManager.Instance != null)
if (CinematicsManager.Instance != null)
{
GameManager.Instance.UnregisterPausableComponent(this);
CinematicsManager.Instance.OnCinematicStopped -= EndGame;
}
// Unregister all pausable components
@@ -194,6 +192,8 @@ namespace Minigames.DivingForPictures
private void Update()
{
if(_settings == null) return;
_timeSinceLastSpawn += Time.deltaTime;
// Gradually increase spawn probability over time
@@ -662,18 +662,14 @@ namespace Minigames.DivingForPictures
// 1) Call the booster pack giver if available
bool completed = false;
var giver = UI.CardSystem.BoosterPackGiver.Instance;
var giver = UI.CardSystem.MinigameBoosterGiver.Instance;
if (giver != null)
{
// Temporarily subscribe to completion
UnityAction onDone = null;
onDone = () => { completed = true; giver.OnCompleted.RemoveListener(onDone); };
giver.OnCompleted.AddListener(onDone);
UIPageController.Instance.ShowAllUI();
giver.GiveBoosterPack();
giver.GiveBooster(() => { completed = true; });
// 2) Wait for it to finish (with a safety timeout in case it's not wired)
float timeout = 5f; // fallback to avoid blocking forever
float timeout = 10f; // fallback to avoid blocking forever
float elapsed = 0f;
while (!completed && elapsed < timeout)
{
@@ -684,7 +680,7 @@ namespace Minigames.DivingForPictures
else
{
// If no giver is present, proceed immediately
Logging.Debug("[DivingGameManager] BoosterPackGiver not found; skipping booster animation.");
Logging.Debug("[DivingGameManager] MinigameBoosterGiver not found; skipping booster animation.");
}
// 3) Only then show the game over screen

View File

@@ -1,12 +1,10 @@
using Interactions;
using UnityEngine;
using Pathfinding;
using UnityEngine.SceneManagement;
using Utils;
using AppleHills.Core.Settings;
using Core;
using Core.SaveLoad;
using Bootstrap;
using Core.Lifecycle;
using UnityEngine.Events;
/// <summary>
@@ -18,13 +16,13 @@ public class FollowerSaveData
public Vector3 worldPosition;
public Quaternion worldRotation;
public string heldItemSaveId; // Save ID of held pickup (if any)
public string heldItemDataAssetPath; // Asset path to PickupItemData
public string heldItemDataAssetPath; // ItemId of the PickupItemData (for fallback restoration)
}
/// <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
{
private static readonly int CombineTrigger = Animator.StringToHash("Combine");
@@ -54,6 +52,12 @@ public class FollowerController : MonoBehaviour, ISaveParticipant
// Direction variables for 2D blend tree animation
private float _lastDirX = 0f; // -1 (left) to 1 (right)
private float _lastDirY = -1f; // -1 (down) to 1 (up)
// Save system configuration
public override bool AutoRegisterForSave => true;
// Scene-specific SaveId - each level has its own follower state
public override string SaveId => $"{gameObject.scene.name}/FollowerController";
private float _currentSpeed = 0f;
private Animator _animator;
private Transform _artTransform;
@@ -98,12 +102,13 @@ public class FollowerController : MonoBehaviour, ISaveParticipant
private Input.PlayerTouchController _playerTouchController;
// Save system tracking
private bool hasBeenRestored;
// Save system tracking for bilateral restoration
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
@@ -122,44 +127,11 @@ public class FollowerController : MonoBehaviour, ISaveParticipant
// Initialize settings references
_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
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.RegisterParticipant(this);
Logging.Debug("[FollowerController] Registered with SaveLoadManager");
}
else
{
Logging.Warning("[FollowerController] SaveLoadManager not available for registration");
}
}
void OnEnable()
{
SceneManager.sceneLoaded += OnSceneLoaded;
FindPlayerReference();
}
void OnDisable()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
// Unregister from save system
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.UnregisterParticipant(GetSaveId());
}
}
void OnSceneLoaded(Scene scene, LoadSceneMode mode)
protected override void OnSceneReady()
{
// Find player reference when scene is ready (called for every scene load)
FindPlayerReference();
}
@@ -167,8 +139,6 @@ public class FollowerController : MonoBehaviour, ISaveParticipant
{
if (_playerTransform == null)
{
FindPlayerReference();
if (_playerTransform == null)
return;
}
@@ -583,19 +553,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 +591,43 @@ 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);
if (matchingRule == null || matchingRule.resultPrefab == null)
return CombinationResult.Unsuccessful;
// Execute combination
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)
// Mark items as picked up before disabling (for save system)
pickupA.IsPickedUp = true;
pickupB.IsPickedUp = true;
Destroy(pickupA.gameObject);
Destroy(pickupB.gameObject);
TryPickupItem(newItem, itemData);
// Disable instead of destroying immediately so they can save their state
// The save system will mark them as picked up and won't restore them
pickupA.gameObject.SetActive(false);
pickupB.gameObject.SetActive(false);
// Pickup the result (don't drop it!)
TryPickupItem(newItem, resultPickup.itemData, dropItem: false);
// Visual feedback
PlayAnimationStationary("Combine", 10.0f);
PulverIsCombining.Invoke();
return CombinationResult.Successful;
}
// If no combination found, return Unsuccessful
return CombinationResult.Unsuccessful;
return CombinationResult.Successful;
}
/// <summary>
@@ -673,6 +657,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 +668,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 +682,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,44 +698,36 @@ 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();
}
#endregion ItemInteractions
#region ISaveParticipant Implementation
#region Save/Load Lifecycle Hooks
public bool HasBeenRestored => hasBeenRestored;
public string GetSaveId()
{
return "FollowerController";
}
public string SerializeState()
protected override string OnSceneSaveRequested()
{
var saveData = new FollowerSaveData
{
@@ -755,26 +741,24 @@ public class FollowerController : MonoBehaviour, ISaveParticipant
var pickup = _cachedPickupObject.GetComponent<Pickup>();
if (pickup is SaveableInteractable saveable)
{
saveData.heldItemSaveId = saveable.GetSaveId();
saveData.heldItemSaveId = saveable.SaveId;
}
// Save the itemId for build-compatible restoration
if (_currentlyHeldItemData != null)
{
#if UNITY_EDITOR
saveData.heldItemDataAssetPath = UnityEditor.AssetDatabase.GetAssetPath(_currentlyHeldItemData);
#endif
saveData.heldItemDataAssetPath = _currentlyHeldItemData.itemId;
}
}
return JsonUtility.ToJson(saveData);
}
public void RestoreState(string serializedData)
protected override void OnSceneRestoreRequested(string serializedData)
{
if (string.IsNullOrEmpty(serializedData))
{
Logging.Debug("[FollowerController] No saved state to restore");
hasBeenRestored = true;
return;
}
@@ -794,7 +778,6 @@ public class FollowerController : MonoBehaviour, ISaveParticipant
TryRestoreHeldItem(saveData.heldItemSaveId, saveData.heldItemDataAssetPath);
}
hasBeenRestored = true;
Logging.Debug($"[FollowerController] Restored position: {saveData.worldPosition}");
}
}
@@ -806,9 +789,10 @@ public class FollowerController : MonoBehaviour, ISaveParticipant
/// <summary>
/// Bilateral restoration: Follower tries to find and claim the held item.
/// If pickup doesn't exist yet, it will try to claim us when it restores.
/// If pickup doesn't exist in the scene (e.g., dynamically spawned combined item),
/// spawns it from the itemData.
/// </summary>
private void TryRestoreHeldItem(string heldItemSaveId, string heldItemDataAssetPath)
private void TryRestoreHeldItem(string heldItemSaveId, string itemDataId)
{
if (_hasRestoredHeldItem)
{
@@ -816,10 +800,30 @@ public class FollowerController : MonoBehaviour, ISaveParticipant
return;
}
// Try to find the pickup immediately
// Try to find the pickup in the scene by SaveId
GameObject heldObject = ItemManager.Instance?.FindPickupBySaveId(heldItemSaveId);
if (heldObject == null)
if (heldObject == null && !string.IsNullOrEmpty(itemDataId))
{
// Item not found in scene - it might be a dynamically spawned combined item
// Try to spawn it from the itemDataId
Logging.Debug($"[FollowerController] Held item not found in scene: {heldItemSaveId}, attempting to spawn from itemId: {itemDataId}");
GameObject prefab = _interactionSettings?.FindPickupPrefabByItemId(itemDataId);
if (prefab != null)
{
// Spawn the item (inactive, since it's being held)
heldObject = Instantiate(prefab, transform.position, Quaternion.identity);
heldObject.SetActive(false);
Logging.Debug($"[FollowerController] Successfully spawned combined item: {itemDataId}");
}
else
{
Logging.Warning($"[FollowerController] Could not find prefab for itemId: {itemDataId}");
return;
}
}
else if (heldObject == null)
{
Logging.Debug($"[FollowerController] Held item not found yet: {heldItemSaveId}, waiting for pickup to restore");
return; // Pickup will find us when it restores
@@ -828,12 +832,14 @@ public class FollowerController : MonoBehaviour, ISaveParticipant
var pickup = heldObject.GetComponent<Pickup>();
if (pickup == null)
{
Logging.Warning($"[FollowerController] Found object but no Pickup component: {heldItemSaveId}");
Logging.Warning($"[FollowerController] Found/spawned object but no Pickup component: {heldItemSaveId}");
if (heldObject != null)
Destroy(heldObject);
return;
}
// Claim the pickup
TakeOwnership(pickup, heldItemDataAssetPath);
TakeOwnership(pickup, itemDataId);
}
/// <summary>
@@ -854,9 +860,9 @@ public class FollowerController : MonoBehaviour, ISaveParticipant
// Verify this is the expected pickup
if (pickup is SaveableInteractable saveable)
{
if (saveable.GetSaveId() != _expectedHeldItemSaveId)
if (saveable.SaveId != _expectedHeldItemSaveId)
{
Logging.Warning($"[FollowerController] Pickup tried to claim but saveId mismatch: {saveable.GetSaveId()} != {_expectedHeldItemSaveId}");
Logging.Warning($"[FollowerController] Pickup tried to claim but saveId mismatch: {saveable.SaveId} != {_expectedHeldItemSaveId}");
return false;
}
}
@@ -869,28 +875,29 @@ public class FollowerController : MonoBehaviour, ISaveParticipant
/// <summary>
/// Takes ownership of a pickup during restoration. Called by both restoration paths.
/// </summary>
private void TakeOwnership(Pickup pickup, string itemDataAssetPath)
private void TakeOwnership(Pickup pickup, string itemDataIdOrPath)
{
if (_hasRestoredHeldItem)
return; // Already claimed
// Get the item data
// Get the item data from the pickup
PickupItemData heldData = pickup.itemData;
#if UNITY_EDITOR
// Try loading from asset path if available and pickup doesn't have data
if (heldData == null && !string.IsNullOrEmpty(itemDataAssetPath))
{
heldData = UnityEditor.AssetDatabase.LoadAssetAtPath<PickupItemData>(itemDataAssetPath);
}
#endif
// Fallback: If pickup doesn't have itemData, log detailed error
if (heldData == null)
{
Logging.Warning($"[FollowerController] Could not get item data for pickup: {pickup.gameObject.name}");
Logging.Warning($"[FollowerController] Pickup {pickup.gameObject.name} has null itemData!");
Logging.Warning($"[FollowerController] Expected itemId: {itemDataIdOrPath}");
Logging.Warning($"[FollowerController] This pickup prefab may be missing its PickupItemData reference.");
return;
}
// Verify itemId matches if we have it (additional safety check)
if (!string.IsNullOrEmpty(itemDataIdOrPath) && heldData.itemId != itemDataIdOrPath)
{
Logging.Warning($"[FollowerController] ItemId mismatch! Pickup has '{heldData.itemId}' but expected '{itemDataIdOrPath}'");
}
// Setup the held item
_cachedPickupObject = pickup.gameObject;
_cachedPickupObject.SetActive(false); // Held items should be hidden
@@ -898,7 +905,7 @@ public class FollowerController : MonoBehaviour, ISaveParticipant
_animator.SetBool("IsCarrying", true);
_hasRestoredHeldItem = true;
Logging.Debug($"[FollowerController] Successfully restored held item: {heldData.itemName}");
Logging.Debug($"[FollowerController] Successfully restored held item: {heldData.itemName} (itemId: {heldData.itemId})");
}
/// <summary>
@@ -910,7 +917,7 @@ public class FollowerController : MonoBehaviour, ISaveParticipant
return FindObjectOfType<FollowerController>();
}
#endregion ISaveParticipant Implementation
#endregion Save/Load Lifecycle Hooks
#if UNITY_EDITOR
void OnDrawGizmos()

View File

@@ -2,6 +2,7 @@
using Interactions;
using UnityEngine;
using Core;
using Core.Lifecycle;
namespace PuzzleS
{
@@ -9,7 +10,7 @@ namespace PuzzleS
/// Manages the state and interactions for a single puzzle step, including unlock/lock logic and event handling.
/// </summary>
[RequireComponent(typeof(InteractableBase))]
public class ObjectiveStepBehaviour : MonoBehaviour, IPuzzlePrompt
public class ObjectiveStepBehaviour : ManagedBehaviour, IPuzzlePrompt
{
/// <summary>
/// The data object representing this puzzle step.
@@ -31,7 +32,7 @@ namespace PuzzleS
// Enum for tracking proximity state (simplified to just Close and Far)
public enum ProximityState { Close, Far }
void Awake()
protected override void Awake()
{
_interactable = GetComponent<InteractableBase>();
@@ -55,6 +56,23 @@ namespace PuzzleS
Logging.Warning($"[Puzzles] Indicator prefab for {stepData?.stepId} does not implement IPuzzlePrompt");
}
}
base.Awake();
}
protected override void OnManagedAwake()
{
base.OnManagedAwake();
// Register with PuzzleManager - safe to access .Instance here
if (stepData != null && PuzzleManager.Instance != null)
{
PuzzleManager.Instance.RegisterStepBehaviour(this);
}
else if (stepData == null)
{
Logging.Warning($"[Puzzles] Cannot register step on {gameObject.name}: stepData is null");
}
}
void OnEnable()
@@ -69,32 +87,20 @@ namespace PuzzleS
}
}
void Start()
protected override void OnDestroy()
{
// Simply register with the PuzzleManager
// The manager will handle state updates appropriately based on whether data is loaded
if (stepData != null && PuzzleManager.Instance != null)
{
PuzzleManager.Instance.RegisterStepBehaviour(this);
}
else if (stepData == null)
{
Logging.Warning($"[Puzzles] Cannot register step on {gameObject.name}: stepData is null");
}
}
void OnDestroy()
{
if (_interactable != null)
{
_interactable.interactionStarted.RemoveListener(OnInteractionStarted);
_interactable.interactionComplete.RemoveListener(OnInteractionComplete);
}
base.OnDestroy();
if (PuzzleManager.Instance != null && stepData != null)
{
PuzzleManager.Instance.UnregisterStepBehaviour(this);
}
if (_interactable != null)
{
_interactable.interactionStarted.RemoveListener(OnInteractionStarted);
_interactable.interactionComplete.RemoveListener(OnInteractionComplete);
}
}
/// <summary>

View File

@@ -5,9 +5,8 @@ using System.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
using AppleHills.Core.Settings;
using Bootstrap;
using Core;
using Core.SaveLoad;
using Core.Lifecycle;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using Utils;
@@ -28,7 +27,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
{
private static PuzzleManager _instance;
@@ -49,6 +48,27 @@ namespace PuzzleS
// Store registered behaviors that are waiting for data to be loaded
private List<ObjectiveStepBehaviour> _registeredBehaviours = new List<ObjectiveStepBehaviour>();
// Save system configuration
public override bool AutoRegisterForSave => true;
/// <summary>
/// SaveId uses CurrentGameplayScene instead of GetActiveScene() because PuzzleManager
/// lives in DontDestroyOnLoad and needs to save/load data per-scene.
/// </summary>
public override string SaveId
{
get
{
string sceneName = SceneManagerService.Instance?.CurrentGameplayScene;
if (string.IsNullOrEmpty(sceneName))
{
// Fallback during early initialization
sceneName = SceneManager.GetActiveScene().name;
}
return $"{sceneName}/PuzzleManager";
}
}
/// <summary>
/// Singleton instance of the PuzzleManager.
/// </summary>
@@ -66,7 +86,6 @@ namespace PuzzleS
// Save/Load restoration tracking
private bool _isDataRestored = false;
private bool _hasBeenRestored = false;
private List<ObjectiveStepBehaviour> _pendingRegistrations = new List<ObjectiveStepBehaviour>();
// Registration for ObjectiveStepBehaviour
@@ -75,35 +94,20 @@ namespace PuzzleS
// Track pending unlocks for steps that were unlocked before their behavior registered
private HashSet<string> _pendingUnlocks = new HashSet<string>();
/// <summary>
/// Returns true if this participant has already had its state restored.
/// Used by SaveLoadManager to prevent double-restoration.
/// </summary>
public bool HasBeenRestored => _hasBeenRestored;
public override int ManagedAwakePriority => 80; // Puzzle systems
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;
// 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 +121,40 @@ namespace PuzzleS
LoadPuzzleDataForCurrentScene();
}
Logging.Debug("[PuzzleManager] Subscribed to SceneManagerService events");
// 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;
}
void OnDestroy()
{
StopProximityChecks();
Logging.Debug("[PuzzleManager] Initialized");
}
// Unsubscribe from scene manager events
/// <summary>
/// Called when any scene finishes loading. Loads puzzles for the new scene.
/// </summary>
private void OnSceneLoadCompleted(string sceneName)
{
Logging.Debug($"[Puzzles] Scene loaded: {sceneName}, loading puzzle data");
LoadPuzzlesForScene(sceneName);
}
protected override void OnDestroy()
{
base.OnDestroy();
// 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))
@@ -186,6 +184,8 @@ namespace PuzzleS
return;
}
// Reset restoration flag when loading new scene data
_isDataRestored = false;
_isDataLoaded = false;
string addressablePath = $"Puzzles/{currentScene}";
@@ -215,11 +215,10 @@ namespace PuzzleS
_currentLevelData = handle.Result;
Logging.Debug($"[Puzzles] Loaded level data: {_currentLevelData.levelId} with {_currentLevelData.allSteps.Count} steps");
// Reset state
_completedSteps.Clear();
_unlockedSteps.Clear();
// Don't clear steps here - SceneManagerService calls ClearPuzzleState() before scene transitions
// This allows save restoration to work properly without race conditions
// Unlock initial steps
// Unlock initial steps (adds to existing unlocked steps from save restoration)
UnlockInitialSteps();
// Update all registered behaviors now that data is loaded
@@ -569,21 +568,30 @@ namespace PuzzleS
return _isDataLoaded;
}
#region ISaveParticipant Implementation
/// <summary>
/// Get unique save ID for this puzzle manager instance
/// Clears all puzzle state (completed steps, unlocked steps, registrations).
/// Called by SceneManagerService before scene transitions to ensure clean state.
/// </summary>
public string GetSaveId()
public void ClearPuzzleState()
{
string sceneName = SceneManager.GetActiveScene().name;
return $"{sceneName}/PuzzleManager";
Logging.Debug("[PuzzleManager] Clearing puzzle state");
_completedSteps.Clear();
_unlockedSteps.Clear();
_isDataRestored = false;
// Clear any pending registrations from the old scene
_pendingRegistrations.Clear();
_pendingUnlocks.Clear();
// Unregister all step behaviours from the old scene
_stepBehaviours.Clear();
_registeredBehaviours.Clear();
}
/// <summary>
/// Serialize current puzzle state to JSON
/// </summary>
public string SerializeState()
#region Save/Load Lifecycle Hooks
protected override string OnSceneSaveRequested()
{
if (_currentLevelData == null)
{
@@ -603,16 +611,14 @@ namespace PuzzleS
return json;
}
/// <summary>
/// Restore puzzle state from serialized JSON data
/// </summary>
public void RestoreState(string data)
protected override void OnSceneRestoreRequested(string data)
{
Debug.Log("[XAXA] PuzzleManager loading with data: " + data);
if (string.IsNullOrEmpty(data) || data == "{}")
{
Logging.Debug("[PuzzleManager] No puzzle save data to restore");
_isDataRestored = true;
_hasBeenRestored = true;
return;
}
@@ -623,7 +629,6 @@ namespace PuzzleS
{
Logging.Warning("[PuzzleManager] Failed to deserialize puzzle save data");
_isDataRestored = true;
_hasBeenRestored = true;
return;
}
@@ -632,13 +637,13 @@ namespace PuzzleS
_unlockedSteps = new HashSet<string>(saveData.unlockedStepIds ?? new List<string>());
_isDataRestored = true;
_hasBeenRestored = true;
Logging.Debug($"[PuzzleManager] Restored puzzle state: {_completedSteps.Count} completed, {_unlockedSteps.Count} unlocked steps");
// Update any behaviors that registered before RestoreState was called
foreach (var behaviour in _pendingRegistrations)
{
if(behaviour != null)
UpdateStepState(behaviour);
}
_pendingRegistrations.Clear();
@@ -647,7 +652,6 @@ namespace PuzzleS
{
Debug.LogError($"[PuzzleManager] Error restoring puzzle state: {e.Message}");
_isDataRestored = true;
_hasBeenRestored = true;
}
}

View File

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

View File

@@ -1,18 +1,22 @@
using UnityEngine;
using Core.SaveLoad;
using Pixelplacement;
using UnityEngine;
namespace StateMachines.Quarry.AnneLise
{
public class AnneLiseBushBehaviour : MonoBehaviour
{
private StateMachine anneLiseBushStateMachine;
private AppleMachine _anneLiseBushStateMachine;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
anneLiseBushStateMachine = GetComponent<StateMachine>();
_anneLiseBushStateMachine = GetComponent<AppleMachine>();
}
public void TakePhoto()
{
anneLiseBushStateMachine.ChangeState("TakePhoto");
_anneLiseBushStateMachine.ChangeState("TakePhoto");
}
}
}

View File

@@ -1,35 +1,71 @@
using Core.SaveLoad;
using Input;
using Pixelplacement;
using System.Collections;
using UnityEngine;
using UnityEngine.Audio;
using UnityEngine.Events;
using static Input.PlayerTouchController;
using System;
public class TakePhotoState : State
namespace StateMachines.Quarry.AnneLise
{
public class TakePhotoState : AppleState
{
public Transform playerTargetObject;
private GameObject playerCharacter;
private PlayerTouchController playerTouchController;
private Vector3 newPlayerPosition;
private GameObject _playerCharacter;
private PlayerTouchController _playerTouchController;
private Vector3 _newPlayerPosition;
public UnityEvent animFlash;
public UnityEvent animStart;
void OnEnable()
/// <summary>
/// Called when entering this state during normal gameplay.
/// Initiates player movement and triggers photo-taking sequence.
/// </summary>
public override void OnEnterState()
{
playerCharacter = GameObject.FindWithTag("Player");
playerTouchController = playerCharacter.GetComponent<PlayerTouchController>();
playerTouchController.OnArrivedAtTarget += PlayerHasArrived;
// Find references that are needed regardless of enter/restore
_playerCharacter = GameObject.FindWithTag("Player");
_playerTouchController = _playerCharacter.GetComponent<PlayerTouchController>();
newPlayerPosition = new Vector3(playerTargetObject.transform.position.x, playerTargetObject.transform.position.y, playerTargetObject.transform.position.z);
playerTouchController.InterruptMoveTo();
playerTouchController.MoveToAndNotify(newPlayerPosition);
// Subscribe to player arrival event
_playerTouchController.OnArrivedAtTarget += PlayerHasArrived;
// Move player to photo position
_newPlayerPosition = new Vector3(
playerTargetObject.transform.position.x,
playerTargetObject.transform.position.y,
playerTargetObject.transform.position.z);
_playerTouchController.InterruptMoveTo();
_playerTouchController.MoveToAndNotify(_newPlayerPosition);
// Disable input during photo sequence
InputManager.Instance.SetInputMode(InputMode.InputDisabled);
}
/// <summary>
/// Called when restoring this state from save data.
/// Skips player movement and animations - just sets up the restored state.
/// </summary>
/// <param name="data">Serialized state data (currently unused for this state)</param>
public override void OnRestoreState(string data)
{
// When restoring, we don't want to move the player or play animations
// The state is restored silently - player stays where they are
// Input mode will be restored by the input system's own save/load
// If we needed to restore any internal state data, we'd deserialize it here
// For now, this state has no persistent data beyond being active
}
/// <summary>
/// Serialize this state's data for saving.
/// Currently this state has no additional data to save beyond being active.
/// </summary>
/// <returns>Serialized state data as JSON string</returns>
public override string SerializeState()
{
// This state doesn't have internal data to save
// The fact that it's the active state is saved by AppleMachine
return "";
}
// When the player has arrived at the bush do Animator.SetTrigger(Takephoto) and whatevs
@@ -43,13 +79,16 @@ public class TakePhotoState : State
void PlayerHasArrived()
{
GetComponent<Animator>().SetTrigger("TakePhoto");
playerTouchController.OnArrivedAtTarget -= PlayerHasArrived;
_playerTouchController.OnArrivedAtTarget -= PlayerHasArrived;
}
private void OnDisable()
{
playerTouchController.OnArrivedAtTarget -= PlayerHasArrived;
// Cleanup: Unsubscribe from events
if (_playerTouchController != null)
{
_playerTouchController.OnArrivedAtTarget -= PlayerHasArrived;
}
}
public void AnimStarted()
@@ -62,5 +101,5 @@ public class TakePhotoState : State
animFlash.Invoke();
}
}
}

View File

@@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using AppleHills.Data.CardSystem;
using Bootstrap;
using Data.CardSystem;
using Pixelplacement;
using UI.Core;
@@ -41,8 +40,10 @@ namespace UI.CardSystem
private List<AlbumCardPlacementDraggable> _activeCards = new List<AlbumCardPlacementDraggable>();
private const int MAX_VISIBLE_CARDS = 3;
private void Awake()
protected override void OnManagedAwake()
{
base.OnManagedAwake();
// Make sure we have a CanvasGroup for transitions
if (canvasGroup == null)
canvasGroup = GetComponent<CanvasGroup>();
@@ -64,16 +65,7 @@ namespace UI.CardSystem
// Set up booster pack button listeners
SetupBoosterButtonListeners();
// Register for post-boot initialization
BootCompletionService.RegisterInitAction(InitializePostBoot);
// UI pages should start disabled
gameObject.SetActive(false);
}
private void InitializePostBoot()
{
// Subscribe to CardSystemManager events
// Subscribe to CardSystemManager events (managers are guaranteed to be initialized)
if (CardSystemManager.Instance != null)
{
CardSystemManager.Instance.OnBoosterCountChanged += OnBoosterCountChanged;
@@ -84,6 +76,9 @@ namespace UI.CardSystem
int initialCount = CardSystemManager.Instance.GetBoosterPackCount();
UpdateBoosterButtons(initialCount);
}
// UI pages should start disabled
gameObject.SetActive(false);
}
private void SetupBoosterButtonListeners()
@@ -102,7 +97,7 @@ namespace UI.CardSystem
}
}
private void OnDestroy()
protected override void OnDestroy()
{
// Unsubscribe from CardSystemManager
if (CardSystemManager.Instance != null)
@@ -134,6 +129,9 @@ namespace UI.CardSystem
// Clean up active cards
CleanupActiveCards();
// Call base implementation
base.OnDestroy();
}
private void OnExitButtonClicked()

View File

@@ -1,4 +1,4 @@
using Bootstrap;
using Core.Lifecycle;
using Data.CardSystem;
using Pixelplacement;
using Pixelplacement.TweenSystem;
@@ -12,7 +12,7 @@ namespace UI.CardSystem
/// Can be reused across different UI elements that need to show numeric notifications
/// Automatically syncs with CardSystemManager to display booster pack count
/// </summary>
public class BoosterNotificationDot : MonoBehaviour
public class BoosterNotificationDot : ManagedBehaviour
{
[Header("UI References")]
[SerializeField] private GameObject dotBackground;
@@ -40,8 +40,10 @@ namespace UI.CardSystem
private TweenBase _activeTween;
private void Awake()
protected override void OnManagedAwake()
{
base.OnManagedAwake();
// Store original scale for pulse animation
if (dotBackground != null)
{
@@ -54,13 +56,7 @@ namespace UI.CardSystem
countText.color = textColor;
}
// Register for post-boot initialization
BootCompletionService.RegisterInitAction(InitializePostBoot);
}
private void InitializePostBoot()
{
// Subscribe to CardSystemManager events
// Subscribe to CardSystemManager events (managers are guaranteed to be initialized)
if (CardSystemManager.Instance != null)
{
CardSystemManager.Instance.OnBoosterCountChanged += OnBoosterCountChanged;
@@ -76,13 +72,16 @@ namespace UI.CardSystem
}
}
private void OnDestroy()
protected override void OnDestroy()
{
// Unsubscribe from CardSystemManager events to prevent memory leaks
if (CardSystemManager.Instance != null)
{
CardSystemManager.Instance.OnBoosterCountChanged -= OnBoosterCountChanged;
}
// Call base implementation
base.OnDestroy();
}
/// <summary>

View File

@@ -192,6 +192,20 @@ namespace UI.CardSystem
_placedCard = albumCard;
_isOccupiedPermanently = true;
// Resize the card to match the slot size (same as placed cards)
RectTransform cardRect = albumCard.transform as RectTransform;
RectTransform slotRect = transform as RectTransform;
if (cardRect != null && slotRect != null)
{
// Set height to match slot height (AspectRatioFitter will handle width)
float targetHeight = slotRect.rect.height;
cardRect.sizeDelta = new Vector2(cardRect.sizeDelta.x, targetHeight);
// Ensure position and rotation are centered
cardRect.localPosition = Vector3.zero;
cardRect.localRotation = Quaternion.identity;
}
// Register with AlbumViewPage for enlarge/shrink handling
AlbumViewPage albumPage = FindObjectOfType<AlbumViewPage>();
if (albumPage != null)

View File

@@ -0,0 +1,242 @@
using System;
using System.Collections;
using Data.CardSystem;
using Pixelplacement;
using UnityEngine;
using UnityEngine.UI;
namespace UI.CardSystem
{
/// <summary>
/// Singleton UI component for granting booster packs from minigames.
/// Displays a booster pack with glow effect, waits for user to click continue,
/// then animates the pack flying to bottom-left corner before granting the reward.
/// </summary>
public class MinigameBoosterGiver : MonoBehaviour
{
public static MinigameBoosterGiver Instance { get; private set; }
[Header("Visual References")]
[SerializeField] private GameObject visualContainer;
[SerializeField] private RectTransform boosterImage;
[SerializeField] private RectTransform glowImage;
[SerializeField] private Button continueButton;
[Header("Animation Settings")]
[SerializeField] private float hoverAmount = 20f;
[SerializeField] private float hoverDuration = 1.5f;
[SerializeField] private float glowPulseMin = 0.9f;
[SerializeField] private float glowPulseMax = 1.1f;
[SerializeField] private float glowPulseDuration = 1.2f;
[Header("Disappear Animation")]
[SerializeField] private Vector2 targetBottomLeftOffset = new Vector2(100f, 100f);
[SerializeField] private float disappearDuration = 0.8f;
[SerializeField] private float disappearScale = 0.2f;
private Vector3 _boosterInitialPosition;
private Vector3 _boosterInitialScale;
private Vector3 _glowInitialScale;
private Coroutine _currentSequence;
private Action _onCompleteCallback;
private void Awake()
{
// Singleton pattern
if (Instance != null && Instance != this)
{
Debug.LogWarning("[MinigameBoosterGiver] Duplicate instance found. Destroying.");
Destroy(gameObject);
return;
}
Instance = this;
// Cache initial values
if (boosterImage != null)
{
_boosterInitialPosition = boosterImage.localPosition;
_boosterInitialScale = boosterImage.localScale;
}
if (glowImage != null)
{
_glowInitialScale = glowImage.localScale;
}
// Setup button listener
if (continueButton != null)
{
continueButton.onClick.AddListener(OnContinueClicked);
}
// Start hidden
if (visualContainer != null)
{
visualContainer.SetActive(false);
}
}
private void OnDestroy()
{
if (Instance == this)
{
Instance = null;
}
if (continueButton != null)
{
continueButton.onClick.RemoveListener(OnContinueClicked);
}
}
/// <summary>
/// Public API to give a booster pack. Displays UI, starts animations, and waits for user interaction.
/// </summary>
/// <param name="onComplete">Optional callback when the sequence completes and pack is granted</param>
public void GiveBooster(Action onComplete = null)
{
if (_currentSequence != null)
{
Debug.LogWarning("[MinigameBoosterGiver] Already running a sequence. Ignoring new request.");
return;
}
_onCompleteCallback = onComplete;
_currentSequence = StartCoroutine(GiveBoosterSequence());
}
private IEnumerator GiveBoosterSequence()
{
// Show the visual
if (visualContainer != null)
{
visualContainer.SetActive(true);
}
// Reset positions and scales
if (boosterImage != null)
{
boosterImage.localPosition = _boosterInitialPosition;
boosterImage.localScale = _boosterInitialScale;
}
if (glowImage != null)
{
glowImage.localScale = _glowInitialScale;
}
// Enable the continue button
if (continueButton != null)
{
continueButton.interactable = true;
}
// Start idle hovering animation on booster (ping-pong)
if (boosterImage != null)
{
Vector3 hoverTarget = _boosterInitialPosition + Vector3.up * hoverAmount;
Tween.LocalPosition(boosterImage, hoverTarget, hoverDuration, 0f, Tween.EaseLinear, Tween.LoopType.PingPong);
}
// Start pulsing animation on glow (ping-pong scale)
if (glowImage != null)
{
Vector3 glowPulseScale = _glowInitialScale * glowPulseMax;
Tween.LocalScale(glowImage, glowPulseScale, glowPulseDuration, 0f, Tween.EaseOut, Tween.LoopType.PingPong);
}
// Wait for button click (handled by OnContinueClicked)
yield return null;
}
private void OnContinueClicked()
{
if (_currentSequence == null)
{
return; // Not in a sequence
}
// Disable button to prevent double-clicks
if (continueButton != null)
{
continueButton.interactable = false;
}
// Stop the ongoing animations by stopping all tweens on these objects
if (boosterImage != null)
{
Tween.Stop(boosterImage.GetInstanceID());
}
if (glowImage != null)
{
Tween.Stop(glowImage.GetInstanceID());
// Fade out the glow
Tween.LocalScale(glowImage, Vector3.zero, disappearDuration * 0.5f, 0f, Tween.EaseInBack);
}
// Start disappear animation
StartCoroutine(DisappearSequence());
}
private IEnumerator DisappearSequence()
{
if (boosterImage == null)
{
yield break;
}
// Calculate bottom-left corner position in local space
RectTransform canvasRect = GetComponentInParent<Canvas>()?.GetComponent<RectTransform>();
Vector3 targetPosition;
if (canvasRect != null)
{
// Convert bottom-left corner with offset to local position
Vector2 bottomLeft = new Vector2(-canvasRect.rect.width / 2f, -canvasRect.rect.height / 2f);
targetPosition = bottomLeft + targetBottomLeftOffset;
}
else
{
// Fallback if no canvas found
targetPosition = _boosterInitialPosition + new Vector3(-500f, -500f, 0f);
}
// Tween to bottom-left corner
Tween.LocalPosition(boosterImage, targetPosition, disappearDuration, 0f, Tween.EaseInBack);
// Scale down
Vector3 targetScale = _boosterInitialScale * disappearScale;
Tween.LocalScale(boosterImage, targetScale, disappearDuration, 0f, Tween.EaseInBack);
// Wait for animation to complete
yield return new WaitForSeconds(disappearDuration);
// Grant the booster pack
if (CardSystemManager.Instance != null)
{
CardSystemManager.Instance.AddBoosterPack(1);
Debug.Log("[MinigameBoosterGiver] Booster pack granted!");
}
else
{
Debug.LogWarning("[MinigameBoosterGiver] CardSystemManager not found, cannot grant booster pack.");
}
// Hide the visual
if (visualContainer != null)
{
visualContainer.SetActive(false);
}
// Invoke completion callback
_onCompleteCallback?.Invoke();
_onCompleteCallback = null;
// Clear sequence reference
_currentSequence = null;
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 7d8f3e9a2b4c5f6d1a8e9c0b3d4f5a6b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
using System;
using Core;
using Core.SaveLoad;
using UnityEngine;
using UnityEngine.SceneManagement;
using Bootstrap;
using UI.Core;
using Pixelplacement;
@@ -22,9 +22,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,19 +37,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);
}
private void InitializePostBoot()
protected override void OnManagedAwake()
{
// Subscribe to scene-dependent events - must be in OnManagedAwake, not OnSceneReady
// because PauseMenu is in DontDestroyOnLoad and OnSceneReady only fires once
if (SceneManagerService.Instance != null)
{
// Subscribe to scene loaded events
SceneManagerService.Instance.SceneLoadCompleted += SetPauseMenuByLevel;
}
// Also react to global UI hide/show events from the page controller
if (UIPageController.Instance != null)
@@ -53,16 +61,21 @@ 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 OnSceneReady()
{
// This only fires once for DontDestroyOnLoad objects, so we handle scene loads in OnManagedAwake
}
protected override void OnDestroy()
{
base.OnDestroy();
// Unsubscribe when destroyed
if (SceneManagerService.Instance != null)
{
@@ -81,17 +94,30 @@ namespace UI
/// <param name="levelName">The name of the level/scene</param>
public void SetPauseMenuByLevel(string levelName)
{
HidePauseMenu();
// TODO: Implement level-based pause menu visibility logic if needed
/*if (string.IsNullOrEmpty(levelName))
return;
// When a new scene loads, ensure pause menu is removed from UIPageController stack
// and properly hidden, regardless of pause state
if (UIPageController.Instance != null && UIPageController.Instance.CurrentPage == this)
{
UIPageController.Instance.PopPage();
}
bool isStartingLevel = levelName.ToLower().Contains("startingscene");
// Ensure pause state is cleared
if (GameManager.Instance != null && GameManager.Instance.IsPaused)
{
EndPauseSideEffects();
}
if(isStartingLevel)
HidePauseMenu(false); // Ensure menu is hidden when switching to a game level
// Hide the menu UI
if (pauseMenuPanel != null) pauseMenuPanel.SetActive(false);
if (canvasGroup != null)
{
canvasGroup.alpha = 0f;
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
gameObject.SetActive(false);
Logging.Debug($"[PauseMenu] Setting pause menu active: {!isStartingLevel} for scene: {levelName}");*/
Logging.Debug($"[PauseMenu] Cleaned up pause menu state for scene: {levelName}");
}
/// <summary>
@@ -238,6 +264,18 @@ namespace UI
/// </summary>
public async void ExitToAppleHills()
{
// Pop from UIPageController stack before switching scenes
if (UIPageController.Instance != null && UIPageController.Instance.CurrentPage == this)
{
UIPageController.Instance.PopPage();
}
// Ensure pause state is cleared
if (GameManager.Instance != null && GameManager.Instance.IsPaused)
{
EndPauseSideEffects();
}
// Replace with the actual scene name as set in Build Settings
var progress = new Progress<float>(p => Logging.Debug($"Loading progress: {p * 100:F0}%"));
await SceneManagerService.Instance.SwitchSceneAsync("AppleHillsOverworld", progress);
@@ -257,8 +295,20 @@ namespace UI
public async void ReloadLevel()
{
// Clear all save data for the current gameplay level before reloading
if (SaveLoadManager.Instance != null && SceneManagerService.Instance != null)
{
string currentLevel = SceneManagerService.Instance.CurrentGameplayScene;
if (!string.IsNullOrEmpty(currentLevel))
{
SaveLoadManager.Instance.ClearLevelData(currentLevel);
Logging.Debug($"[PauseMenu] Cleared save data for current level: {currentLevel}");
}
}
// Now reload the current scene with fresh state - skipSave=true prevents re-saving cleared data
var progress = new Progress<float>(p => Logging.Debug($"Loading progress: {p * 100:F0}%"));
await SceneManagerService.Instance.ReloadCurrentScene(progress);
await SceneManagerService.Instance.ReloadCurrentScene(progress, autoHideLoadingScreen: true, skipSave: true);
}
/// <summary>

View File

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

View File

@@ -14,10 +14,10 @@ MonoBehaviour:
m_EditorClassIdentifier: AppleHillsScripts::AppleHills.Core.Settings.DebugSettings
showDebugUiMessages: 1
pauseTimeOnPauseGame: 0
useSaveLoadSystem: 0
bootstrapLogVerbosity: 1
settingsLogVerbosity: 1
gameManagerLogVerbosity: 1
sceneLogVerbosity: 1
saveLoadLogVerbosity: 1
inputLogVerbosity: 1
useSaveLoadSystem: 1
bootstrapLogVerbosity: 0
settingsLogVerbosity: 0
gameManagerLogVerbosity: 0
sceneLogVerbosity: 0
saveLoadLogVerbosity: 0
inputLogVerbosity: 0

8
Assets/_Recovery.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 21ea3de9e8c22e449bf12522c31b27ed
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: