using System; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.SceneManagement; using AppleHills.Core.Settings; using Bootstrap; using Core; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; using Utils; namespace PuzzleS { /// /// Manages puzzle step registration, dependency management, and step completion for the puzzle system. /// public class PuzzleManager : MonoBehaviour { private static PuzzleManager _instance; private static bool _isQuitting; [SerializeField] private float proximityCheckInterval = 0.02f; // Reference to player transform for proximity calculations private Transform _playerTransform; private Coroutine _proximityCheckCoroutine; // Settings reference private IInteractionSettings _interactionSettings; // Current level puzzle data private PuzzleLevelDataSO _currentLevelData; private AsyncOperationHandle _levelDataLoadOperation; private bool _isDataLoaded = false; // Store registered behaviors that are waiting for data to be loaded private List _registeredBehaviours = new List(); /// /// Singleton instance of the PuzzleManager. /// public static PuzzleManager Instance => _instance; // Events to notify about step lifecycle public event Action OnStepCompleted; public event Action OnStepUnlocked; public event Action OnLevelDataLoaded; public event Action OnAllPuzzlesComplete; private HashSet _completedSteps = new HashSet(); private HashSet _unlockedSteps = new HashSet(); // Registration for ObjectiveStepBehaviour private Dictionary _stepBehaviours = new Dictionary(); void Awake() { _instance = this; // Initialize settings reference _interactionSettings = GameManager.GetSettingsObject(); // Register for post-boot initialization BootCompletionService.RegisterInitAction(InitializePostBoot); } private void InitializePostBoot() { // Subscribe to SceneManagerService events after boot is complete SceneManagerService.Instance.SceneLoadCompleted += OnSceneLoadCompleted; SceneManagerService.Instance.SceneLoadStarted += OnSceneLoadStarted; // Find player transform _playerTransform = GameObject.FindGameObjectWithTag("Player")?.transform; // Start proximity check coroutine StartProximityChecks(); // Load puzzle data for the current scene if not already loading if (_currentLevelData == null && !_isDataLoaded) { LoadPuzzleDataForCurrentScene(); } Logging.Debug("[PuzzleManager] Subscribed to SceneManagerService events"); } void OnDestroy() { StopProximityChecks(); // Unsubscribe from scene manager events if (SceneManagerService.Instance != null) { SceneManagerService.Instance.SceneLoadCompleted -= OnSceneLoadCompleted; SceneManagerService.Instance.SceneLoadStarted -= OnSceneLoadStarted; } // Release addressable handle if needed if (_levelDataLoadOperation.IsValid()) { Addressables.Release(_levelDataLoadOperation); } } /// /// Called when a scene is starting to load /// 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"); } /// /// Called when a scene is loaded /// public void OnSceneLoadCompleted(string sceneName) { // Skip for non-gameplay scenes if (sceneName == "BootstrapScene" || string.IsNullOrEmpty(sceneName)) { return; } Logging.Debug($"[Puzzles] Scene loaded: {sceneName}, loading puzzle data"); LoadPuzzleDataForCurrentScene(sceneName); // Find player transform again in case it changed with scene load _playerTransform = GameObject.FindGameObjectWithTag("Player")?.transform; // Restart proximity checks StartProximityChecks(); } /// /// Load puzzle data for the current scene /// private void LoadPuzzleDataForCurrentScene(string sceneName = null) { string currentScene = sceneName ?? SceneManagerService.Instance.CurrentGameplayScene; if (string.IsNullOrEmpty(currentScene)) { Logging.Warning("[Puzzles] Cannot load puzzle data: Current scene name is empty"); return; } _isDataLoaded = false; string addressablePath = $"Puzzles/{currentScene}"; Logging.Debug($"[Puzzles] Loading puzzle data from addressable: {addressablePath}"); // Release previous handle if needed if (_levelDataLoadOperation.IsValid()) { Addressables.Release(_levelDataLoadOperation); } // Check if the addressable exists before trying to load it if (!AppleHillsUtils.AddressableKeyExists(addressablePath)) { Logging.Warning($"[Puzzles] Puzzle data does not exist for scene: {currentScene}"); _isDataLoaded = true; // Mark as loaded but with no data _currentLevelData = null; return; } // Load the level data asset _levelDataLoadOperation = Addressables.LoadAssetAsync(addressablePath); _levelDataLoadOperation.Completed += handle => { if (handle.Status == AsyncOperationStatus.Succeeded) { _currentLevelData = handle.Result; Logging.Debug($"[Puzzles] Loaded level data: {_currentLevelData.levelId} with {_currentLevelData.allSteps.Count} steps"); // Reset state _completedSteps.Clear(); _unlockedSteps.Clear(); // Unlock initial steps UnlockInitialSteps(); // Update all registered behaviors now that data is loaded UpdateAllRegisteredBehaviors(); // Mark data as loaded _isDataLoaded = true; // Notify listeners OnLevelDataLoaded?.Invoke(_currentLevelData); } else { Logging.Warning($"[Puzzles] Failed to load puzzle data for {currentScene}: {handle.OperationException?.Message}"); _isDataLoaded = true; // Mark as loaded but with error _currentLevelData = null; } }; } /// /// Start the proximity check coroutine. /// private void StartProximityChecks() { StopProximityChecks(); _proximityCheckCoroutine = StartCoroutine(CheckProximityRoutine()); } /// /// Stop the proximity check coroutine. /// private void StopProximityChecks() { if (_proximityCheckCoroutine != null) { StopCoroutine(_proximityCheckCoroutine); _proximityCheckCoroutine = null; } } /// /// Coroutine that periodically checks player proximity to all puzzle steps. /// private IEnumerator CheckProximityRoutine() { WaitForSeconds wait = new WaitForSeconds(proximityCheckInterval); while (true) { if (_playerTransform != null) { // Get the proximity threshold from settings directly using our settings reference float proximityThreshold = _interactionSettings?.DefaultPuzzlePromptRange ?? 3.0f; // Check distance to each step behavior foreach (var kvp in _stepBehaviours) { if (kvp.Value == null) continue; if (IsPuzzleStepCompleted(kvp.Key.stepId)) continue; float distance = Vector3.Distance(_playerTransform.position, kvp.Value.transform.position); // Determine the proximity state - only Close or Far now ObjectiveStepBehaviour.ProximityState state = (distance <= proximityThreshold) ? ObjectiveStepBehaviour.ProximityState.Close : ObjectiveStepBehaviour.ProximityState.Far; // Update the step's proximity state kvp.Value.UpdateProximityState(state); } } yield return wait; } } /// /// Update all registered behaviors with their current state /// private void UpdateAllRegisteredBehaviors() { foreach (var behaviour in _registeredBehaviours) { if (behaviour == null) continue; // Only update if the step is in our dictionary bool stepUnlocked = IsStepUnlocked(behaviour.stepData); if (stepUnlocked) { UpdateStepState(behaviour); } } } /// /// Registers a step behaviour with the manager. /// /// The step behaviour to register. public void RegisterStepBehaviour(ObjectiveStepBehaviour behaviour) { if (behaviour?.stepData == null) return; // Always add to our registered behaviors list if (!_registeredBehaviours.Contains(behaviour)) { _registeredBehaviours.Add(behaviour); } // Add to the step behaviours dictionary if not already there if (!_stepBehaviours.ContainsKey(behaviour.stepData)) { _stepBehaviours.Add(behaviour.stepData, behaviour); Logging.Debug($"[Puzzles] Registered step: {behaviour.stepData.stepId} on {behaviour.gameObject.name}"); // Only update state if data is already loaded if (_isDataLoaded && _currentLevelData != null) { UpdateStepState(behaviour); } // Otherwise, the state will be updated when data loads in UpdateAllRegisteredBehaviors } } /// /// Updates a step's state based on the current puzzle state. /// private void UpdateStepState(ObjectiveStepBehaviour behaviour) { if (behaviour?.stepData == null) return; // If step is already completed, ignore if (_completedSteps.Contains(behaviour.stepData)) return; // If step is already unlocked, update the behaviour if (_unlockedSteps.Contains(behaviour.stepData)) { behaviour.UnlockStep(); } else { // Make sure it's locked behaviour.LockStep(); } } /// /// Unregisters a step behaviour from the manager. /// /// The step behaviour to unregister. public void UnregisterStepBehaviour(ObjectiveStepBehaviour behaviour) { if (behaviour?.stepData == null) return; _stepBehaviours.Remove(behaviour.stepData); _registeredBehaviours.Remove(behaviour); Logging.Debug($"[Puzzles] Unregistered step: {behaviour.stepData.stepId} on {behaviour.gameObject.name}"); } /// /// Unlocks all initial steps (those with no dependencies) /// private void UnlockInitialSteps() { if (_currentLevelData == null) return; // Unlock initial steps foreach (var step in _currentLevelData.initialSteps) { UnlockStep(step); } Logging.Debug($"[Puzzles] Unlocked {_unlockedSteps.Count} initial steps"); } /// /// Called when a step is completed. Unlocks dependent steps if their dependencies are met. /// /// The completed step. public void MarkPuzzleStepCompleted(PuzzleStepSO step) { if (_completedSteps.Contains(step)) return; if (_currentLevelData == null) return; _completedSteps.Add(step); Logging.Debug($"[Puzzles] Step completed: {step.stepId}"); // Broadcast completion OnStepCompleted?.Invoke(step); // Unlock steps that are unlocked by this step foreach (var unlockStep in _currentLevelData.GetUnlockedSteps(step)) { if (AreStepDependenciesMet(unlockStep)) { Logging.Debug($"[Puzzles] Unlocking step {unlockStep.stepId} after completing {step.stepId}"); UnlockStep(unlockStep); } else { Logging.Debug($"[Puzzles] Step {unlockStep.stepId} not unlocked yet, waiting for other dependencies"); } } // Check if all puzzle steps are now complete CheckPuzzleCompletion(); } /// /// Checks if all dependencies for a step are met. /// /// The step to check. /// True if all dependencies are met, false otherwise. private bool AreStepDependenciesMet(PuzzleStepSO step) { if (_currentLevelData == null || step == null) return false; // If it's an initial step, it has no dependencies if (_currentLevelData.IsInitialStep(step)) return true; // Check if dependencies are met using pre-processed data if (_currentLevelData.stepDependencies.TryGetValue(step.stepId, out string[] dependencies)) { foreach (var depId in dependencies) { // Find the dependency step bool dependencyMet = false; foreach (var completedStep in _completedSteps) { if (completedStep.stepId == depId) { dependencyMet = true; break; } } if (!dependencyMet) return false; } } return true; } /// /// Unlocks a specific step and notifies its behaviour. /// /// The step to unlock. private void UnlockStep(PuzzleStepSO step) { if (_unlockedSteps.Contains(step)) return; _unlockedSteps.Add(step); if (_stepBehaviours.TryGetValue(step, out var behaviour)) { behaviour.UnlockStep(); } Logging.Debug($"[Puzzles] Step unlocked: {step.stepId}"); // Broadcast unlock OnStepUnlocked?.Invoke(step); } /// /// Checks if the puzzle is complete (all steps in level finished). /// private void CheckPuzzleCompletion() { if (_currentLevelData == null) return; if (_currentLevelData.IsLevelComplete(_completedSteps)) { Logging.Debug("[Puzzles] All puzzles complete! Level finished."); // Fire level complete event OnAllPuzzlesComplete?.Invoke(_currentLevelData); } } /// /// Returns whether a step is already unlocked. /// public bool IsStepUnlocked(PuzzleStepSO step) { return _unlockedSteps.Contains(step); } /// /// Checks if a puzzle step with the specified ID has been completed /// /// The ID of the puzzle step to check /// True if the step has been completed, false otherwise public bool IsPuzzleStepCompleted(string stepId) { return _completedSteps.Any(step => step.stepId == stepId); } /// /// Get the current level puzzle data /// public PuzzleLevelDataSO GetCurrentLevelData() { return _currentLevelData; } /// /// Checks if puzzle data is loaded /// /// True if data loading has completed (whether successful or not) public bool IsDataLoaded() { return _isDataLoaded; } void OnApplicationQuit() { _isQuitting = true; } } }