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:
5
.github/copilot-instructions.md
vendored
5
.github/copilot-instructions.md
vendored
@@ -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.
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
3
Assets/Editor/Lifecycle.meta
Normal file
3
Assets/Editor/Lifecycle.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c5d626da49844592981ef14524e3a308
|
||||
timeCreated: 1762332131
|
||||
144
Assets/Editor/Lifecycle/EditorLifecycleBootstrap.cs
Normal file
144
Assets/Editor/Lifecycle/EditorLifecycleBootstrap.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
9
Assets/Editor/Lifecycle/EditorLifecycleBootstrap.cs.meta
Normal file
9
Assets/Editor/Lifecycle/EditorLifecycleBootstrap.cs.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7f3e8a9c4d5b6e7f8a9b0c1d2e3f4a5b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
31
Assets/Editor/Tools/DebugSaveIds.cs
Normal file
31
Assets/Editor/Tools/DebugSaveIds.cs
Normal 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 ===");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Editor/Tools/DebugSaveIds.cs.meta
Normal file
3
Assets/Editor/Tools/DebugSaveIds.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a34fbba4efbb4acd85d79a99abf00a08
|
||||
timeCreated: 1762358959
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -166,7 +166,6 @@ MonoBehaviour:
|
||||
interactionComplete:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
customSaveId:
|
||||
itemData: {fileID: 11400000, guid: 0c6986639ca176a419c92f5a327d95ce, type: 2}
|
||||
iconRenderer: {fileID: 7494677664706785084}
|
||||
--- !u!1001 &8589202998731622905
|
||||
|
||||
@@ -140,6 +140,5 @@ MonoBehaviour:
|
||||
interactionComplete:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
customSaveId:
|
||||
itemData: {fileID: 11400000, guid: 43f22dbbb4c0eec4f8108d0f0eea43c2, type: 2}
|
||||
iconRenderer: {fileID: 4055726361761331703}
|
||||
|
||||
@@ -140,6 +140,5 @@ MonoBehaviour:
|
||||
interactionComplete:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
customSaveId:
|
||||
itemData: {fileID: 11400000, guid: a8baa800efa25a344a95b190cf349e2d, type: 2}
|
||||
iconRenderer: {fileID: 4774534086162962138}
|
||||
|
||||
@@ -140,6 +140,5 @@ MonoBehaviour:
|
||||
interactionComplete:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
customSaveId:
|
||||
itemData: {fileID: 11400000, guid: 560ba2059ce14dc4da580e2f43b2e65f, type: 2}
|
||||
iconRenderer: {fileID: 4986096986936361008}
|
||||
|
||||
@@ -140,6 +140,5 @@ MonoBehaviour:
|
||||
interactionComplete:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
customSaveId:
|
||||
itemData: {fileID: 11400000, guid: 3b1f3472171abc943bb099ce31d6fc7c, type: 2}
|
||||
iconRenderer: {fileID: 4266110216568578813}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
1520
Assets/Prefabs/Puzzles/Picnic.prefab
Normal file
1520
Assets/Prefabs/Puzzles/Picnic.prefab
Normal file
File diff suppressed because it is too large
Load Diff
7
Assets/Prefabs/Puzzles/Picnic.prefab.meta
Normal file
7
Assets/Prefabs/Puzzles/Picnic.prefab.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 97f767ded753d524086106f3c39a645f
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AppleHills.Core.Settings;
|
||||
using Core;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Bootstrap
|
||||
{
|
||||
/// <summary>
|
||||
/// Service that provides notification and management of boot completion status.
|
||||
/// Allows systems to subscribe to boot completion events, register initialization actions with priorities,
|
||||
/// or await boot completion asynchronously.
|
||||
/// </summary>
|
||||
public static class BootCompletionService
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates if the boot process has completed
|
||||
/// </summary>
|
||||
public static bool IsBootComplete { get; private set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when boot completes
|
||||
/// </summary>
|
||||
public static event Action OnBootComplete;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an initialization action with priority
|
||||
/// </summary>
|
||||
private class InitializationAction
|
||||
{
|
||||
public Action Action { get; }
|
||||
public int Priority { get; }
|
||||
public string Name { get; }
|
||||
|
||||
public InitializationAction(Action action, int priority, string name)
|
||||
{
|
||||
Action = action;
|
||||
Priority = priority;
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
// List of initialization actions to be executed once boot completes
|
||||
private static List<InitializationAction> _initializationActions = new List<InitializationAction>();
|
||||
|
||||
// TaskCompletionSource for async await pattern
|
||||
private static TaskCompletionSource<bool> _bootCompletionTask = new TaskCompletionSource<bool>();
|
||||
|
||||
/// <summary>
|
||||
/// Called by CustomBoot when the boot process is complete
|
||||
/// </summary>
|
||||
internal static void HandleBootCompleted()
|
||||
{
|
||||
if (IsBootComplete)
|
||||
return;
|
||||
|
||||
IsBootComplete = true;
|
||||
|
||||
LogDebugMessage("Boot process completed, executing initialization actions");
|
||||
|
||||
// Execute initialization actions in priority order (lower number = higher priority)
|
||||
ExecuteInitializationActions();
|
||||
|
||||
// Trigger the event
|
||||
OnBootComplete?.Invoke();
|
||||
|
||||
// Complete the task for async waiters
|
||||
_bootCompletionTask.TrySetResult(true);
|
||||
|
||||
LogDebugMessage("All boot completion handlers executed");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register an action to be executed when boot completes.
|
||||
/// Lower priority numbers run first.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to execute</param>
|
||||
/// <param name="priority">Priority (lower numbers run first)</param>
|
||||
/// <param name="name">Name for debugging</param>
|
||||
public static void RegisterInitAction(Action action, int priority = 100, string name = null)
|
||||
{
|
||||
if (action == null)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrEmpty(name))
|
||||
name = $"Action_{_initializationActions.Count}";
|
||||
|
||||
var initAction = new InitializationAction(action, priority, name);
|
||||
|
||||
if (IsBootComplete)
|
||||
{
|
||||
// If boot is already complete, execute immediately
|
||||
LogDebugMessage($"Executing late registration: {name} (Priority: {priority})");
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogDebugMessage($"Error executing init action '{name}': {ex}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise add to the queue
|
||||
_initializationActions.Add(initAction);
|
||||
LogDebugMessage($"Registered init action: {name} (Priority: {priority})");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait asynchronously for boot completion
|
||||
/// </summary>
|
||||
/// <returns>Task that completes when boot is complete</returns>
|
||||
public static Task WaitForBootCompletionAsync()
|
||||
{
|
||||
if (IsBootComplete)
|
||||
return Task.CompletedTask;
|
||||
|
||||
return _bootCompletionTask.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute all registered initialization actions in priority order
|
||||
/// </summary>
|
||||
private static void ExecuteInitializationActions()
|
||||
{
|
||||
// Sort by priority (lowest first)
|
||||
var sortedActions = _initializationActions
|
||||
.OrderBy(a => a.Priority)
|
||||
.ToList();
|
||||
|
||||
foreach (var action in sortedActions)
|
||||
{
|
||||
try
|
||||
{
|
||||
LogDebugMessage($"Executing: {action.Name} (Priority: {action.Priority})");
|
||||
action.Action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogDebugMessage($"Error executing init action '{action.Name}': {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the list after execution
|
||||
_initializationActions.Clear();
|
||||
}
|
||||
|
||||
private static void LogDebugMessage(string message)
|
||||
{
|
||||
if (DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().bootstrapLogVerbosity <=
|
||||
LogVerbosity.Debug)
|
||||
{
|
||||
Logging.Debug($"[BootCompletionService] {message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aa0228cf33a64515bc166b7a9bc8c0b9
|
||||
timeCreated: 1760606319
|
||||
@@ -1,18 +1,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();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Threading.Tasks;
|
||||
using AppleHills.Core.Settings;
|
||||
using Core;
|
||||
using Core.Lifecycle;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
@@ -39,6 +40,10 @@ namespace Bootstrap
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)]
|
||||
private static void Initialise()
|
||||
{
|
||||
// Create LifecycleManager FIRST - before any bootstrap logic
|
||||
// This ensures it exists when boot completes
|
||||
LifecycleManager.CreateInstance();
|
||||
|
||||
//We should always clean up after Addressables, so let's take care of that immediately
|
||||
Application.quitting += ApplicationOnUnloading;
|
||||
|
||||
@@ -97,12 +102,14 @@ namespace Bootstrap
|
||||
OnBootProgressChanged?.Invoke(1f);
|
||||
OnBootCompleted?.Invoke();
|
||||
|
||||
// Notify the BootCompletionService that boot is complete
|
||||
// Notify the LifecycleManager that boot is complete
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
// Direct call to boot completion service
|
||||
LogDebugMessage("Calling BootCompletionService.HandleBootCompleted()");
|
||||
BootCompletionService.HandleBootCompleted();
|
||||
LogDebugMessage("Calling LifecycleManager.OnBootCompletionTriggered()");
|
||||
if (LifecycleManager.Instance != null)
|
||||
{
|
||||
LifecycleManager.Instance.OnBootCompletionTriggered();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,12 +124,14 @@ namespace Bootstrap
|
||||
OnBootProgressChanged?.Invoke(1f);
|
||||
OnBootCompleted?.Invoke();
|
||||
|
||||
// Notify the BootCompletionService that boot is complete
|
||||
// Notify the LifecycleManager that boot is complete
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
// Direct call to boot completion service
|
||||
LogDebugMessage("Calling BootCompletionService.HandleBootCompleted()");
|
||||
BootCompletionService.HandleBootCompleted();
|
||||
LogDebugMessage("Calling LifecycleManager.OnBootCompletionTriggered()");
|
||||
if (LifecycleManager.Instance != null)
|
||||
{
|
||||
LifecycleManager.Instance.OnBootCompletionTriggered();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Bootstrap;
|
||||
using Core;
|
||||
using Core.Lifecycle;
|
||||
using UI;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
@@ -14,7 +14,7 @@ namespace Cinematics
|
||||
/// <summary>
|
||||
/// Handles loading, playing and unloading cinematics
|
||||
/// </summary>
|
||||
public class CinematicsManager : MonoBehaviour
|
||||
public class CinematicsManager : ManagedBehaviour
|
||||
{
|
||||
public event System.Action OnCinematicStarted;
|
||||
public event System.Action OnCinematicStopped;
|
||||
@@ -37,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
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using Bootstrap;
|
||||
using Core;
|
||||
using Core.Lifecycle;
|
||||
using Input;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Cinematics
|
||||
{
|
||||
public class SkipCinematic : MonoBehaviour, ITouchInputConsumer
|
||||
public class SkipCinematic : ManagedBehaviour, ITouchInputConsumer
|
||||
{
|
||||
[Header("Configuration")]
|
||||
[SerializeField] private float holdDuration = 2.0f;
|
||||
@@ -17,39 +17,28 @@ namespace Cinematics
|
||||
private bool _skipPerformed;
|
||||
private bool _initialized = false;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
// Register for post-boot initialization
|
||||
BootCompletionService.RegisterInitAction(InitializePostBoot);
|
||||
}
|
||||
public override int ManagedAwakePriority => 180; // Cinematic UI
|
||||
|
||||
void Start()
|
||||
protected override void OnManagedAwake()
|
||||
{
|
||||
// Reset the progress bar
|
||||
if (radialProgressBar != null)
|
||||
{
|
||||
radialProgressBar.fillAmount = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
// Clean up subscriptions regardless of initialization state
|
||||
UnsubscribeFromCinematicsEvents();
|
||||
}
|
||||
|
||||
private void InitializePostBoot()
|
||||
{
|
||||
// Safe initialization of manager dependencies after boot is complete
|
||||
if (_initialized)
|
||||
return;
|
||||
|
||||
_initialized = true;
|
||||
|
||||
// Subscribe to CinematicsManager events now that boot is complete
|
||||
SubscribeToCinematicsEvents();
|
||||
|
||||
Logging.Debug("[SkipCinematic] Post-boot initialization complete");
|
||||
Logging.Debug("[SkipCinematic] Initialized");
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
base.OnDestroy();
|
||||
|
||||
// Clean up subscriptions
|
||||
UnsubscribeFromCinematicsEvents();
|
||||
}
|
||||
|
||||
private void SubscribeToCinematicsEvents()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using AppleHills.Core.Interfaces;
|
||||
using AppleHills.Core.Settings;
|
||||
using Bootstrap;
|
||||
using Core.Lifecycle;
|
||||
using Core.Settings;
|
||||
using Input;
|
||||
using UnityEngine;
|
||||
@@ -12,7 +12,7 @@ namespace Core
|
||||
/// <summary>
|
||||
/// Singleton manager for global game state and settings. Provides accessors for various gameplay parameters.
|
||||
/// </summary>
|
||||
public class GameManager : MonoBehaviour
|
||||
public class GameManager : ManagedBehaviour
|
||||
{
|
||||
// Singleton implementation
|
||||
private static GameManager _instance;
|
||||
@@ -34,33 +34,33 @@ namespace Core
|
||||
public event Action OnGamePaused;
|
||||
public event Action OnGameResumed;
|
||||
|
||||
void Awake()
|
||||
// ManagedBehaviour configuration
|
||||
public override int ManagedAwakePriority => 10; // Core infrastructure - runs early
|
||||
|
||||
private new void Awake()
|
||||
{
|
||||
base.Awake(); // CRITICAL: Register with LifecycleManager!
|
||||
|
||||
// Set instance immediately so it's available before OnManagedAwake() is called
|
||||
_instance = this;
|
||||
|
||||
// Create settings providers if it doesn't exist
|
||||
// Create settings providers - must happen in Awake so other managers can access settings in their ManagedAwake
|
||||
SettingsProvider.Instance.gameObject.name = "Settings Provider";
|
||||
DeveloperSettingsProvider.Instance.gameObject.name = "Developer Settings Provider";
|
||||
|
||||
// Load all settings synchronously during Awake
|
||||
// Load all settings synchronously - critical infrastructure for other managers
|
||||
InitializeSettings();
|
||||
InitializeDeveloperSettings();
|
||||
|
||||
// Register for post-boot initialization
|
||||
BootCompletionService.RegisterInitAction(InitializePostBoot);
|
||||
|
||||
// DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Load verbosity settings early
|
||||
_settingsLogVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().settingsLogVerbosity;
|
||||
_managerLogVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().gameManagerLogVerbosity;
|
||||
}
|
||||
|
||||
private void InitializePostBoot()
|
||||
protected override void OnManagedAwake()
|
||||
{
|
||||
// For post-boot correct initialization order
|
||||
// Settings are already initialized in Awake()
|
||||
// This is available for future initialization that depends on other managers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Interactions;
|
||||
using Bootstrap;
|
||||
using Core.Lifecycle;
|
||||
using Core.SaveLoad;
|
||||
|
||||
namespace Core
|
||||
@@ -11,7 +11,7 @@ namespace Core
|
||||
/// Central registry for pickups and item slots.
|
||||
/// Mirrors the singleton pattern used by PuzzleManager.
|
||||
/// </summary>
|
||||
public class ItemManager : MonoBehaviour
|
||||
public class ItemManager : ManagedBehaviour
|
||||
{
|
||||
private static ItemManager _instance;
|
||||
|
||||
@@ -48,35 +48,32 @@ namespace Core
|
||||
// Args: first item data, second item data, result item data
|
||||
public event Action<PickupItemData, PickupItemData, PickupItemData> OnItemsCombined;
|
||||
|
||||
void Awake()
|
||||
public override int ManagedAwakePriority => 75; // Item registry
|
||||
|
||||
private new void Awake()
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
3
Assets/Scripts/Core/Lifecycle.meta
Normal file
3
Assets/Scripts/Core/Lifecycle.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 06a2c07342e5422eae1eb613f614ed61
|
||||
timeCreated: 1762206473
|
||||
48
Assets/Scripts/Core/Lifecycle/LifecycleEnums.cs
Normal file
48
Assets/Scripts/Core/Lifecycle/LifecycleEnums.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
namespace Core.Lifecycle
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the different lifecycle phases that can be broadcast by the LifecycleManager.
|
||||
/// All ManagedBehaviours participate in all lifecycle phases by default.
|
||||
/// </summary>
|
||||
public enum LifecyclePhase
|
||||
{
|
||||
/// <summary>
|
||||
/// Called once per component after bootstrap completes.
|
||||
/// Guaranteed to be called after all bootstrap resources are loaded.
|
||||
/// For late-registered components, called immediately upon registration.
|
||||
/// </summary>
|
||||
ManagedAwake,
|
||||
|
||||
/// <summary>
|
||||
/// Called before a scene is unloaded.
|
||||
/// Only called for components in the scene being unloaded.
|
||||
/// </summary>
|
||||
SceneUnloading,
|
||||
|
||||
/// <summary>
|
||||
/// Called after a scene has finished loading.
|
||||
/// Only called for components in the scene being loaded.
|
||||
/// </summary>
|
||||
SceneReady,
|
||||
|
||||
/// <summary>
|
||||
/// Called before scene unloads to save data via SaveLoadManager.
|
||||
/// Integrates with existing SaveLoadManager save system.
|
||||
/// </summary>
|
||||
SaveRequested,
|
||||
|
||||
/// <summary>
|
||||
/// Called after scene loads to restore data via SaveLoadManager.
|
||||
/// Integrates with existing SaveLoadManager restore system.
|
||||
/// </summary>
|
||||
RestoreRequested,
|
||||
|
||||
/// <summary>
|
||||
/// Called during OnDestroy before component is destroyed.
|
||||
/// Use for custom cleanup logic.
|
||||
/// Most cleanup is automatic (managed events, auto-registrations).
|
||||
/// </summary>
|
||||
ManagedDestroy
|
||||
}
|
||||
}
|
||||
|
||||
2
Assets/Scripts/Core/Lifecycle/LifecycleEnums.cs.meta
Normal file
2
Assets/Scripts/Core/Lifecycle/LifecycleEnums.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5f5f0f19f08240d4d9863b6be6a3cf03
|
||||
637
Assets/Scripts/Core/Lifecycle/LifecycleManager.cs
Normal file
637
Assets/Scripts/Core/Lifecycle/LifecycleManager.cs
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
2
Assets/Scripts/Core/Lifecycle/LifecycleManager.cs.meta
Normal file
2
Assets/Scripts/Core/Lifecycle/LifecycleManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db6d4743867a3a44381d511cea39218d
|
||||
288
Assets/Scripts/Core/Lifecycle/ManagedBehaviour.cs
Normal file
288
Assets/Scripts/Core/Lifecycle/ManagedBehaviour.cs
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
2
Assets/Scripts/Core/Lifecycle/ManagedBehaviour.cs.meta
Normal file
2
Assets/Scripts/Core/Lifecycle/ManagedBehaviour.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af776ef1493d6e543aa3cbe2601f4ef2
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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}'");
|
||||
}
|
||||
|
||||
148
Assets/Scripts/Core/SaveablePlayableDirector.cs
Normal file
148
Assets/Scripts/Core/SaveablePlayableDirector.cs
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Scripts/Core/SaveablePlayableDirector.cs.meta
Normal file
3
Assets/Scripts/Core/SaveablePlayableDirector.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a5c5614fc04140cb81e5bda7451f7b14
|
||||
timeCreated: 1762360145
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using UnityEngine;
|
||||
|
||||
// TODO: Remove this
|
||||
public class LureSpot : MonoBehaviour
|
||||
{
|
||||
[SerializeField] public GameObject luredBird;
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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--;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Bootstrap;
|
||||
using Core;
|
||||
using Core.Lifecycle;
|
||||
using Interactions;
|
||||
using UnityEngine;
|
||||
using PuzzleS;
|
||||
@@ -12,7 +12,7 @@ namespace Dialogue
|
||||
{
|
||||
[AddComponentMenu("AppleHills/Dialogue/Dialogue Component")]
|
||||
[RequireComponent(typeof(AppleAudioSource))]
|
||||
public class DialogueComponent : MonoBehaviour
|
||||
public class DialogueComponent : ManagedBehaviour
|
||||
{
|
||||
[SerializeField] private RuntimeDialogueGraph dialogueGraph;
|
||||
|
||||
@@ -35,7 +35,9 @@ namespace Dialogue
|
||||
public string CurrentSpeakerName => dialogueGraph?.speakerName;
|
||||
|
||||
|
||||
private void Start()
|
||||
public override int ManagedAwakePriority => 150; // Dialogue systems
|
||||
|
||||
protected override void OnManagedAwake()
|
||||
{
|
||||
// Get required components
|
||||
appleAudioSource = GetComponent<AppleAudioSource>();
|
||||
@@ -58,11 +60,6 @@ namespace Dialogue
|
||||
speechBubble.UpdatePromptVisibility(HasAnyLines());
|
||||
}
|
||||
|
||||
BootCompletionService.RegisterInitAction(InitializePostBoot);
|
||||
}
|
||||
|
||||
private void InitializePostBoot()
|
||||
{
|
||||
// Register for global events
|
||||
PuzzleManager.Instance.OnStepCompleted += OnAnyPuzzleStepCompleted;
|
||||
ItemManager.Instance.OnItemPickedUp += OnAnyItemPickedUp;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic; // Added for List<ITouchInputConsumer>
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.SceneManagement;
|
||||
using AppleHills.Core.Settings;
|
||||
using Bootstrap;
|
||||
using Core; // Added for IInteractionSettings
|
||||
using Core;
|
||||
using Core.Lifecycle;
|
||||
|
||||
namespace Input
|
||||
{
|
||||
@@ -22,7 +21,7 @@ namespace Input
|
||||
/// Handles input events and dispatches them to the appropriate ITouchInputConsumer.
|
||||
/// Supports tap and hold/drag logic, with interactable delegation and debug logging.
|
||||
/// </summary>
|
||||
public class InputManager : MonoBehaviour
|
||||
public class InputManager : ManagedBehaviour
|
||||
{
|
||||
private const string UiActions = "UI";
|
||||
private const string GameActions = "PlayerTouch";
|
||||
@@ -51,33 +50,29 @@ namespace Input
|
||||
private bool isHoldActive;
|
||||
private LogVerbosity _logVerbosity = LogVerbosity.Warning;
|
||||
|
||||
void Awake()
|
||||
public override int ManagedAwakePriority => 25; // Input infrastructure
|
||||
|
||||
private new void Awake()
|
||||
{
|
||||
base.Awake(); // CRITICAL: Register with LifecycleManager!
|
||||
|
||||
// Set instance immediately so it's available before OnManagedAwake() is called
|
||||
_instance = this;
|
||||
|
||||
// Register for post-boot initialization
|
||||
BootCompletionService.RegisterInitAction(InitializePostBoot);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Load verbosity settings early
|
||||
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().inputLogVerbosity;
|
||||
}
|
||||
|
||||
private void InitializePostBoot()
|
||||
{
|
||||
// Subscribe to scene load completed events now that boot is complete
|
||||
SceneManagerService.Instance.SceneLoadCompleted += SwitchInputOnSceneLoaded;
|
||||
|
||||
// Initialize settings reference
|
||||
// Initialize settings reference early (GameManager sets these up in its Awake)
|
||||
_interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
|
||||
|
||||
// Set up PlayerInput component and actions - critical for input to work
|
||||
playerInput = GetComponent<PlayerInput>();
|
||||
if (playerInput == null)
|
||||
{
|
||||
Debug.LogError("[InputManager] InputManager requires a PlayerInput component attached to the same GameObject.");
|
||||
return;
|
||||
}
|
||||
|
||||
tapMoveAction = playerInput.actions.FindAction("TapMove", false);
|
||||
holdMoveAction = playerInput.actions.FindAction("HoldMove", false);
|
||||
positionAction = playerInput.actions.FindAction("TouchPosition", false);
|
||||
@@ -90,14 +85,39 @@ namespace Input
|
||||
holdMoveAction.canceled += OnHoldMoveCanceled;
|
||||
}
|
||||
|
||||
// Initialize input mode for current scene
|
||||
SwitchInputOnSceneLoaded(SceneManager.GetActiveScene().name);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
protected override void OnManagedAwake()
|
||||
{
|
||||
// Unsubscribe from SceneManagerService
|
||||
// Subscribe to scene load events from SceneManagerService
|
||||
// This must happen in ManagedAwake because SceneManagerService instance needs to be set first
|
||||
if (SceneManagerService.Instance != null)
|
||||
SceneManagerService.Instance.SceneLoadCompleted -= SwitchInputOnSceneLoaded;
|
||||
{
|
||||
SceneManagerService.Instance.SceneLoadCompleted += OnSceneLoadCompleted;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when any scene finishes loading. Restores input to GameAndUI mode.
|
||||
/// </summary>
|
||||
private void OnSceneLoadCompleted(string sceneName)
|
||||
{
|
||||
LogDebugMessage($"Scene loaded: {sceneName}, restoring input mode");
|
||||
SwitchInputOnSceneLoaded(sceneName);
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
// Unsubscribe from SceneManagerService events
|
||||
if (SceneManagerService.Instance != null)
|
||||
{
|
||||
SceneManagerService.Instance.SceneLoadCompleted -= OnSceneLoadCompleted;
|
||||
}
|
||||
|
||||
base.OnDestroy();
|
||||
// Input action cleanup happens automatically
|
||||
}
|
||||
|
||||
private void SwitchInputOnSceneLoaded(string sceneName)
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
using Pathfinding;
|
||||
using AppleHills.Core.Settings;
|
||||
using Core;
|
||||
using Core.Lifecycle;
|
||||
using Core.SaveLoad;
|
||||
using Bootstrap;
|
||||
|
||||
namespace Input
|
||||
{
|
||||
@@ -21,7 +21,7 @@ namespace Input
|
||||
/// Handles player movement in response to tap and hold input events.
|
||||
/// Supports both direct and pathfinding movement modes, and provides event/callbacks for arrival/cancellation.
|
||||
/// </summary>
|
||||
public class PlayerTouchController : MonoBehaviour, ITouchInputConsumer, ISaveParticipant
|
||||
public class PlayerTouchController : ManagedBehaviour, ITouchInputConsumer
|
||||
{
|
||||
// --- 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}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
using UnityEngine;
|
||||
using Input;
|
||||
using Interactions;
|
||||
|
||||
namespace Interactions
|
||||
namespace Interactions
|
||||
{
|
||||
/// <summary>
|
||||
/// Interactable that immediately completes when the character arrives at the interaction point.
|
||||
@@ -11,11 +7,11 @@ namespace Interactions
|
||||
public class OneClickInteraction : InteractableBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Override: Immediately completes the interaction with success when character arrives.
|
||||
/// Main interaction logic: Simply return success.
|
||||
/// </summary>
|
||||
protected override void OnCharacterArrived()
|
||||
protected override bool DoInteraction()
|
||||
{
|
||||
CompleteInteraction(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
using AppleHills.Core.Settings;
|
||||
using Bootstrap;
|
||||
using Core;
|
||||
using PuzzleS;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Audio;
|
||||
using AppleHills.Core;
|
||||
@@ -9,8 +5,9 @@ using AppleHills.Core.Interfaces;
|
||||
using System.Collections.Generic;
|
||||
using AudioSourceEvents;
|
||||
using System;
|
||||
using Core.Lifecycle;
|
||||
|
||||
public class AudioManager : MonoBehaviour, IPausable
|
||||
public class AudioManager : ManagedBehaviour, IPausable
|
||||
{
|
||||
/// <summary>
|
||||
/// Play all audio, just music or no audio at all when the game is paused.
|
||||
@@ -42,18 +39,21 @@ public class AudioManager : MonoBehaviour, IPausable
|
||||
/// </summary>
|
||||
public static AudioManager Instance => _instance;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
_instance = this;
|
||||
// ManagedBehaviour configuration
|
||||
public override int ManagedAwakePriority => 30; // Audio infrastructure
|
||||
public override bool AutoRegisterPausable => true; // Auto-register as IPausable
|
||||
|
||||
// Register for post-boot initialization
|
||||
BootCompletionService.RegisterInitAction(InitializePostBoot);
|
||||
GameManager.Instance.RegisterPausableComponent(this);
|
||||
private new void Awake()
|
||||
{
|
||||
base.Awake(); // CRITICAL: Register with LifecycleManager!
|
||||
|
||||
// Set instance immediately so it's available before OnManagedAwake() is called
|
||||
_instance = this;
|
||||
}
|
||||
|
||||
private void InitializePostBoot()
|
||||
protected override void OnManagedAwake()
|
||||
{
|
||||
|
||||
// Auto-registration with GameManager handled by ManagedBehaviour
|
||||
}
|
||||
|
||||
// Start is called once before the first execution of Update after the MonoBehaviour is created
|
||||
|
||||
@@ -1,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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
242
Assets/Scripts/UI/CardSystem/MinigameBoosterGiver.cs
Normal file
242
Assets/Scripts/UI/CardSystem/MinigameBoosterGiver.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
Assets/Scripts/UI/CardSystem/MinigameBoosterGiver.cs.meta
Normal file
12
Assets/Scripts/UI/CardSystem/MinigameBoosterGiver.cs.meta
Normal file
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7d8f3e9a2b4c5f6d1a8e9c0b3d4f5a6b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Core.Lifecycle;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UI.Core
|
||||
@@ -6,12 +7,17 @@ namespace UI.Core
|
||||
/// <summary>
|
||||
/// Base class for UI pages that can transition in and out.
|
||||
/// Extended by specific UI page implementations for the card system.
|
||||
/// Now inherits from ManagedBehaviour for lifecycle support.
|
||||
/// Children can override lifecycle hooks if they need boot-dependent initialization.
|
||||
/// </summary>
|
||||
public abstract class UIPage : MonoBehaviour
|
||||
public abstract class UIPage : ManagedBehaviour
|
||||
{
|
||||
[Header("Page Settings")]
|
||||
public string PageName;
|
||||
|
||||
// UI pages load after UI infrastructure (UIPageController is priority 50)
|
||||
public override int ManagedAwakePriority => 200;
|
||||
|
||||
// Events using System.Action instead of UnityEvents
|
||||
public event Action OnTransitionInStarted;
|
||||
public event Action OnTransitionInCompleted;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Bootstrap;
|
||||
using Core;
|
||||
using UnityEngine;
|
||||
using Core.Lifecycle;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace UI.Core
|
||||
@@ -11,7 +10,7 @@ namespace UI.Core
|
||||
/// Manages UI page transitions and maintains a stack of active pages.
|
||||
/// Pages are pushed onto a stack for navigation and popped when going back.
|
||||
/// </summary>
|
||||
public class UIPageController : MonoBehaviour
|
||||
public class UIPageController : ManagedBehaviour
|
||||
{
|
||||
private static UIPageController _instance;
|
||||
public static UIPageController Instance => _instance;
|
||||
@@ -30,36 +29,25 @@ namespace UI.Core
|
||||
private PlayerInput _playerInput;
|
||||
private InputAction _cancelAction;
|
||||
|
||||
private void Awake()
|
||||
public override int ManagedAwakePriority => 50; // UI infrastructure
|
||||
|
||||
private new void Awake()
|
||||
{
|
||||
base.Awake(); // CRITICAL: Register with LifecycleManager!
|
||||
|
||||
// Set instance immediately so it's available before OnManagedAwake() is called
|
||||
_instance = this;
|
||||
|
||||
// TODO: Handle generic "cancel" action
|
||||
// _playerInput = FindFirstObjectByType<PlayerInput>();
|
||||
// if (_playerInput == null)
|
||||
// {
|
||||
// Logging.Warning("[UIPageController] No PlayerInput found in the scene. Cancel action might not work.");
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// // Get the Cancel action from the UI action map
|
||||
// _cancelAction = _playerInput.actions.FindAction("UI/Cancel");
|
||||
// if (_cancelAction != null)
|
||||
// {
|
||||
// _cancelAction.performed += OnCancelActionPerformed;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// Logging.Warning("[UIPageController] Cancel action not found in the input actions asset.");
|
||||
// }
|
||||
// }
|
||||
|
||||
// Register for post-boot initialization
|
||||
BootCompletionService.RegisterInitAction(InitializePostBoot);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
protected override void OnManagedAwake()
|
||||
{
|
||||
Logging.Debug("[UIPageController] Initialized");
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
base.OnDestroy();
|
||||
|
||||
// Clean up event subscription when the controller is destroyed
|
||||
if (_cancelAction != null)
|
||||
{
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
8
Assets/_Recovery.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 21ea3de9e8c22e449bf12522c31b27ed
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user