using AppleHills.Core.Interfaces; 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 Minigames.DivingForPictures.Bubbles; using UI.Core; using UnityEngine; using UnityEngine.Events; using UnityEngine.Playables; using Svg; namespace Minigames.DivingForPictures { public class DivingGameManager : ManagedBehaviour, IPausable { [Header("Monster Prefabs")] [Tooltip("Array of monster prefabs to spawn randomly")] [SerializeField] private GameObject[] monsterPrefabs; [Header("Rope Damage System")] [Tooltip("Ropes that will break one by one as player takes damage")] [SerializeField] private RopeBreaker[] playerRopes; [Header("Surfacing Settings")] [Tooltip("Reference to the PlayableDirector that will play the surfacing timeline")] [SerializeField] private PlayableDirector surfacingTimeline; [Header("Flash Effect Reference")] [Tooltip("Reference to the Flash Effect when the picture is taken")] [SerializeField] private GameObject flashRef; public CameraViewfinderManager viewfinderManager; // Settings reference private IDivingMinigameSettings _settings; // Private state variables private int _playerScore = 0; private float _currentSpawnProbability; private float _lastSpawnTime = -100f; private float _timeSinceLastSpawn = 0f; private List _activeMonsters = new List(); // Velocity management // Velocity state tracking private float _currentVelocityFactor = 1.0f; // 1.0 = normal descent speed, -1.0 * surfacingSpeedFactor = full surfacing speed private Coroutine _velocityTransitionCoroutine; private Coroutine _surfacingSequenceCoroutine; // Public properties public int PlayerScore => _playerScore; public float CurrentVelocityFactor => _currentVelocityFactor; // Events public event Action OnScoreChanged; public event Action OnMonsterSpawned; public event Action OnPictureTaken; public event Action OnSpawnProbabilityChanged; public event Action OnGameOver; public event Action OnRopeBroken; // Passes remaining ropes count public event Action OnVelocityFactorChanged; // Private state variables for rope system private int _currentRopeIndex = 0; private bool _isGameOver = false; private bool _isSurfacing = false; // Initialization state private bool _isGameInitialized = false; // Used to track if we're currently surfacing public bool IsSurfacing => _isSurfacing; // TODO: Get rid of this in favor of proper game pausing? public event Action OnGameInitialized; // List of pausable components controlled by this manager private List _pausableComponents = new List(); // Photo sequence state private bool _isPhotoSequenceActive = false; private Monster _currentPhotoTarget = null; private float _capturedProximity = 0f; // List of components to exempt from pausing during photo sequence private List _exemptFromPhotoSequencePausing = new List(); // Photo sequence events public event Action OnPhotoSequenceStarted; public event Action OnPhotoSequenceCompleted; // Now includes proximity score private static DivingGameManager _instance = null; public AudioSource deathAudioPlayer; public CameraViewfinderManager cameraViewfinderManager; public static DivingGameManager Instance => _instance; public override bool AutoRegisterPausable => true; // Automatic GameManager registration internal override void OnManagedAwake() { if (_instance == null) { _instance = this; } else if (_instance != this) { Destroy(gameObject); } } internal override void OnManagedStart() { _settings = GameManager.GetSettingsObject(); _currentSpawnProbability = _settings?.BaseSpawnProbability ?? 0.2f; // Ensure any previous run state is reset when this manager awakes _isGameOver = false; Logging.Debug("[DivingGameManager] Initialized"); } internal override void OnSceneReady() { InitializeGame(); // Subscribe to scene-specific events CinematicsManager.Instance.OnCinematicStopped += EndGame; PlayerCollisionBehavior.OnDamageTaken += OnPlayerDamageTaken; OnMonsterSpawned += DoMonsterSpawned; // Validate rope references ValidateRopeReferences(); viewfinderManager = CameraViewfinderManager.Instance; // Subscribe to viewfinder events if found if (viewfinderManager != null) { viewfinderManager.OnAnimationCompleted += OnViewfinderAnimationCompleted; viewfinderManager.OnViewfinderTapped += OnViewfinderTapped; viewfinderManager.OnProximityUpdated += OnProximityUpdated; viewfinderManager.OnViewfinderTappedDuringAnimation += OnViewfinderTappedDuringAnimation; viewfinderManager.OnReverseAnimationStarted += OnReverseAnimationStarted; // TODO: Give this a second look and make sure this is correct // Add the viewfinder manager to exempt list to ensure it keeps working during photo sequences if (viewfinderManager is IPausable viewfinderPausable) { RegisterExemptFromPhotoSequencePausing(viewfinderPausable); } } } internal override void OnManagedDestroy() { // Unsubscribe from events when the manager is destroyed PlayerCollisionBehavior.OnDamageTaken -= OnPlayerDamageTaken; OnMonsterSpawned -= DoMonsterSpawned; if (CinematicsManager.Instance != null) { CinematicsManager.Instance.OnCinematicStopped -= EndGame; } // Unregister all pausable components _pausableComponents.Clear(); // Unsubscribe from viewfinder events if (viewfinderManager != null) { viewfinderManager.OnAnimationCompleted -= OnViewfinderAnimationCompleted; viewfinderManager.OnViewfinderTapped -= OnViewfinderTapped; viewfinderManager.OnProximityUpdated -= OnProximityUpdated; viewfinderManager.OnViewfinderTappedDuringAnimation -= OnViewfinderTappedDuringAnimation; viewfinderManager.OnReverseAnimationStarted -= OnReverseAnimationStarted; } } private void Update() { if(_settings == null) return; _timeSinceLastSpawn += Time.deltaTime; // Gradually increase spawn probability over time float previousProbability = _currentSpawnProbability; if (_currentSpawnProbability < _settings.MaxSpawnProbability) { _currentSpawnProbability += _settings.ProbabilityIncreaseRate * Time.deltaTime; _currentSpawnProbability = Mathf.Min(_currentSpawnProbability, _settings.MaxSpawnProbability); // Only fire event if probability changed significantly if (Mathf.Abs(_currentSpawnProbability - previousProbability) > 0.01f) { OnSpawnProbabilityChanged?.Invoke(_currentSpawnProbability); } } } private void OnTileSpawned(GameObject tile) { // Check for spawn points in the new tile MonsterSpawnPoint[] spawnPoints = tile.GetComponentsInChildren(); if (spawnPoints.Length == 0) return; // If we're surfacing, don't spawn new monsters if (_isSurfacing) return; bool forceSpawn = _timeSinceLastSpawn >= _settings.GuaranteedSpawnTime; bool onCooldown = _timeSinceLastSpawn < _settings.SpawnCooldown; // Don't spawn if on cooldown, unless forced if (onCooldown && !forceSpawn) return; // Check probability or forced spawn if (forceSpawn || UnityEngine.Random.value <= _currentSpawnProbability) { // Pick a random spawn point from this tile MonsterSpawnPoint spawnPoint = spawnPoints[UnityEngine.Random.Range(0, spawnPoints.Length)]; // Spawn the monster at the spawn point and parent it SpawnMonster(spawnPoint.transform); // Reset timer and adjust probability _lastSpawnTime = Time.time; _timeSinceLastSpawn = 0f; _currentSpawnProbability = _settings.BaseSpawnProbability; OnSpawnProbabilityChanged?.Invoke(_currentSpawnProbability); } } private void SpawnMonster(Transform spawnPoint) { Logging.Debug("Spawning monster: " + spawnPoint.name); if (monsterPrefabs.Length == 0) { Logging.Warning("No monster prefabs assigned to DivingGameManager."); return; } // Select random monster prefab GameObject prefab = monsterPrefabs[UnityEngine.Random.Range(0, monsterPrefabs.Length)]; // Instantiate monster at spawn point position GameObject monsterObj = Instantiate(prefab, spawnPoint.position, Quaternion.identity); Monster monster = monsterObj.GetComponent(); if (monster != null) { // Parent the monster to the spawn point so it moves with the tile monsterObj.transform.SetParent(spawnPoint); // Flip the spawn point in X axis if facesLeft is true MonsterSpawnPoint msp = spawnPoint.GetComponent(); if (msp != null && msp.facesLeft) { Vector3 scale = spawnPoint.localScale; scale.x *= -1f; spawnPoint.localScale = scale; } // Fire event OnMonsterSpawned?.Invoke(monster); } else { Debug.LogError($"Monster prefab {prefab.name} does not have a Monster component!"); Destroy(monsterObj); } } private void DoPictureTaken(Monster monster) { // Calculate points based on depth int depthBonus = Mathf.FloorToInt(Mathf.Abs(monster.transform.position.y) * _settings.DepthMultiplier); int pointsAwarded = _settings.BasePoints + depthBonus; // Add score _playerScore += pointsAwarded; // Fire events OnScoreChanged?.Invoke(_playerScore); OnPictureTaken?.Invoke(monster, pointsAwarded); } /// /// Called when the player takes damage from any collision /// private void OnPlayerDamageTaken() { if (_isGameOver) return; // Break the next rope in sequence BreakNextRope(); // Check if all ropes are broken if (_currentRopeIndex >= playerRopes.Length) { TriggerGameOver(); deathAudioPlayer.Play(); } else { // Notify listeners about rope break and remaining ropes int remainingRopes = playerRopes.Length - _currentRopeIndex; OnRopeBroken?.Invoke(remainingRopes); Logging.Debug($"[DivingGameManager] Rope broken! {remainingRopes} ropes remaining."); } } /// /// Breaks the next available rope in the sequence /// private void BreakNextRope() { if (_currentRopeIndex < playerRopes.Length) { RopeBreaker ropeToBreak = playerRopes[_currentRopeIndex]; if (ropeToBreak != null) { // Let the RopeBreaker component handle the breaking, effects, and sounds ropeToBreak.BreakRope(); } else { Logging.Warning($"[DivingGameManager] Rope at index {_currentRopeIndex} is null!"); } // Move to the next rope regardless if current was null _currentRopeIndex++; } } /// /// Manually break a rope (for testing or external events) /// public void ForceBreakRope() { if (!_isGameOver) { OnPlayerDamageTaken(); } } /// /// Triggers game over state when all ropes are broken /// private void TriggerGameOver() { if (_isGameOver) return; _isGameOver = true; Logging.Debug("[DivingGameManager] Game Over! All ropes broken. Starting surfacing sequence..."); // Fire game over event OnGameOver?.Invoke(); // Start surfacing instead of directly ending the game StartSurfacing(); } /// /// Validates rope references and logs warnings if any are missing /// private void ValidateRopeReferences() { if (playerRopes == null || playerRopes.Length == 0) { Logging.Warning("[DivingGameManager] No ropes assigned to break! Damage system won't work properly."); return; } for (int i = 0; i < playerRopes.Length; i++) { if (playerRopes[i] == null) { Logging.Warning($"[DivingGameManager] Rope at index {i} is null!"); } } } /// /// Resets the rope system for a new game /// public void ResetRopeSystem() { // Reset rope state _currentRopeIndex = 0; _isGameOver = false; // Restore all broken ropes if (playerRopes != null) { foreach (var rope in playerRopes) { if (rope != null) { rope.RestoreRope(); } } } Logging.Debug("[DivingGameManager] Rope system reset."); } /// /// Starts the surfacing mode - reverses trench direction and adjusts all spawned entities /// public void StartSurfacing() { if (_isSurfacing) return; // Already surfacing _isSurfacing = true; // TODO: Call it here? UIPageController.Instance.HideAllUI(); // 1. Initiate smooth velocity transition to surfacing speed float targetVelocityFactor = -1.0f * _settings.SurfacingSpeedFactor; SetVelocityFactor(targetVelocityFactor); // 2. Find and notify trench tile spawner about direction change (for spawning/despawning logic) TrenchTileSpawner tileSpawner = FindFirstObjectByType(); if (tileSpawner != null) { // Subscribe to velocity changes if not already subscribed OnVelocityFactorChanged -= tileSpawner.OnVelocityFactorChanged; OnVelocityFactorChanged += tileSpawner.OnVelocityFactorChanged; // Subscribe to the last tile event tileSpawner.onLastTileLeft.RemoveListener(OnLastTileLeft); tileSpawner.onLastTileLeft.AddListener(OnLastTileLeft); // Tell spawner to reverse spawn/despawn logic tileSpawner.StartSurfacing(); // Immediately send current velocity factor tileSpawner.OnVelocityFactorChanged(_currentVelocityFactor); } // Handle the Rock object - disable components and animate it falling offscreen GameObject rockObject = GameObject.FindGameObjectWithTag("Rock"); if (rockObject != null) { // Disable all components except Transform on the rock object (not its children) foreach (Component component in rockObject.GetComponents()) { if (!(component is Transform)) { if (component is Behaviour behaviour) { behaviour.enabled = false; } } } // Start coroutine to animate the rock falling offscreen StartCoroutine(MoveRockOffscreen(rockObject.transform)); Logging.Debug("[DivingGameManager] Disabled rock components and animating it offscreen"); } // Handle the Player object - disable components and reset X position GameObject playerObject = GameObject.FindGameObjectWithTag("Player"); if (playerObject != null) { // Disable all components except Transform, Animator, and PlayerBlinkBehavior on the player object foreach (Component component in playerObject.GetComponents()) { if (!(component is Transform) && !(component is Animator) && !(component is PlayerBlinkBehavior)) { if (component is Behaviour behaviour) { behaviour.enabled = false; } } } // Start coroutine to reset X position to 0 over 1 second StartCoroutine(ResetPlayerPosition(playerObject.transform)); Logging.Debug("[DivingGameManager] Disabled player components (keeping Animator and PlayerBlinkBehavior) and resetting position"); } // 3. Find bubble spawner and slow down existing bubbles (no velocity management needed) BubbleSpawner bubbleSpawner = FindFirstObjectByType(); if (bubbleSpawner != null) { bubbleSpawner.StartSurfacing(); } // 4. Find obstacle spawner and set up for velocity changes ObstacleSpawner obstacleSpawner = FindFirstObjectByType(); if (obstacleSpawner != null) { // Subscribe to velocity changes OnVelocityFactorChanged -= obstacleSpawner.OnVelocityFactorChanged; OnVelocityFactorChanged += obstacleSpawner.OnVelocityFactorChanged; // Tell spawner to reverse spawn/despawn logic obstacleSpawner.StartSurfacing(); // Immediately send current velocity factor obstacleSpawner.OnVelocityFactorChanged(_currentVelocityFactor); } // Start the surfacing sequence coroutine if (_surfacingSequenceCoroutine != null) { StopCoroutine(_surfacingSequenceCoroutine); } _surfacingSequenceCoroutine = StartCoroutine(SurfacingSequence()); Logging.Debug($"[DivingGameManager] Started surfacing with target velocity factor: {targetVelocityFactor}"); } /// /// Coroutine to animate the rock falling below the screen /// private IEnumerator MoveRockOffscreen(Transform rockTransform) { Vector3 startPosition = rockTransform.position; // Calculate position below the screen UnityEngine.Camera mainCamera = UnityEngine.Camera.main; if (mainCamera == null) { Logging.Warning("[DivingGameManager] Cannot find main camera to calculate offscreen position"); yield break; } // Get a position below the bottom of the screen Vector3 offscreenPosition = mainCamera.ViewportToWorldPoint(new Vector3(0.5f, -0.2f, mainCamera.nearClipPlane)); Vector3 targetPosition = new Vector3(startPosition.x, offscreenPosition.y, startPosition.z); float duration = 2.0f; // Animation duration in seconds float elapsed = 0f; while (elapsed < duration) { elapsed += Time.deltaTime; float t = Mathf.Clamp01(elapsed / duration); // Use an easing function that accelerates to simulate falling float easedT = t * t; // Quadratic easing rockTransform.position = Vector3.Lerp(startPosition, targetPosition, easedT); yield return null; } // Ensure final position is exactly at target rockTransform.position = targetPosition; } /// /// Coroutine to reset the player's X position to 0 over time /// private IEnumerator ResetPlayerPosition(Transform playerTransform) { Vector3 startPosition = playerTransform.position; Vector3 targetPosition = new Vector3(0f, startPosition.y, startPosition.z); float duration = 1.0f; // Reset duration in seconds (as requested) float elapsed = 0f; while (elapsed < duration) { elapsed += Time.deltaTime; float t = Mathf.Clamp01(elapsed / duration); // Use smooth step for more natural movement float smoothT = Mathf.SmoothStep(0f, 1f, t); playerTransform.position = Vector3.Lerp(startPosition, targetPosition, smoothT); yield return null; } // Ensure final position is exactly at target playerTransform.position = targetPosition; } /// /// Coroutine to handle the surfacing sequence timing /// private IEnumerator SurfacingSequence() { // Wait for the configured delay yield return new WaitForSeconds(_settings.SurfacingSpawnDelay); // Find tile spawner and tell it to stop spawning TrenchTileSpawner tileSpawner = FindFirstObjectByType(); if (tileSpawner != null) { // Tell it to stop spawning new tiles tileSpawner.StopSpawning(); Logging.Debug("[DivingGameManager] Stopped spawning new tiles after delay"); } } /// /// Called when the last tile leaves the screen /// private void OnLastTileLeft() { // Play the timeline if (surfacingTimeline != null) { //Instead of surfacingTimeline, play the outro cinematic deathAudioPlayer.Stop(); CinematicsManager.Instance.LoadAndPlayCinematic("SurfacingCinematic",true); CinematicsManager.Instance.ChangeCinematicBackgroundColour(new Color(0.5058824f, 0.7803922f, 0.8862746f, 1f)); //PlayerHudManager.Instance.ResizeCinematicPlayer(); //surfacingTimeline.Play(); //Logging.Debug("[DivingGameManager] Last tile left the screen, playing timeline"); } else { Logging.Warning("[DivingGameManager] No surfacing timeline assigned!"); } } // Call this when the game ends public void EndGame() { CinematicsManager.Instance.OnCinematicStopped -= EndGame; // Start the end game sequence that grants a booster, waits for the UI animation, then shows Game Over. StartCoroutine(EndGameSequence()); } private IEnumerator EndGameSequence() { // Clean up active monsters foreach (var monster in _activeMonsters.ToArray()) { if (monster != null) { monster.DespawnMonster(); } } _activeMonsters.Clear(); // 1) Call the booster pack giver if available bool completed = false; var giver = UI.CardSystem.MinigameBoosterGiver.Instance; if (giver != null) { UIPageController.Instance.ShowAllUI(); Logging.Debug("[DivingGameManager] Starting booster giver sequence"); giver.GiveBooster(() => { completed = true; Logging.Debug("[DivingGameManager] Booster giver completed callback received"); }); // 2) Wait for it to finish (NO timeout - wait indefinitely for user interaction) while (!completed) { yield return null; } Logging.Debug("[DivingGameManager] Booster giver sequence finished, proceeding to game over"); } else { // If no giver is present, proceed immediately Logging.Debug("[DivingGameManager] MinigameBoosterGiver not found; skipping booster animation."); } // 3) Only then show the game over screen CinematicsManager.Instance.ShowGameOverScreen(); // Final score could be saved to player prefs or other persistence Logging.Debug($"Final Score: {_playerScore}"); } /// /// Starts a smooth transition to the new velocity factor /// /// Target velocity factor (e.g., -1.0 for surfacing speed) public void SetVelocityFactor(float targetFactor) { if (_velocityTransitionCoroutine != null) { StopCoroutine(_velocityTransitionCoroutine); } _velocityTransitionCoroutine = StartCoroutine(TransitionVelocityFactor(targetFactor)); } /// /// Coroutine to smoothly transition the velocity factor over time /// private IEnumerator TransitionVelocityFactor(float targetFactor) { float startFactor = _currentVelocityFactor; float elapsed = 0f; while (elapsed < _settings.SpeedTransitionDuration) { elapsed += Time.deltaTime; float t = Mathf.Clamp01(elapsed / _settings.SpeedTransitionDuration); // Smooth step interpolation float smoothStep = t * t * (3f - 2f * t); _currentVelocityFactor = Mathf.Lerp(startFactor, targetFactor, smoothStep); // Notify listeners about the velocity factor change OnVelocityFactorChanged?.Invoke(_currentVelocityFactor); yield return null; } _currentVelocityFactor = targetFactor; // Final assignment to ensure exact target value OnVelocityFactorChanged?.Invoke(_currentVelocityFactor); } /// /// Register a component as pausable with this manager /// /// The pausable component to register public void RegisterPausableComponent(IPausable component) { if (component != null && !_pausableComponents.Contains(component)) { _pausableComponents.Add(component); // If the game is already paused, pause the component immediately if (GameManager.Instance.IsPaused) { component.Pause(); } Logging.Debug($"[DivingGameManager] Registered pausable component: {(component as MonoBehaviour)?.name ?? "Unknown"}"); } } /// /// Unregister a pausable component /// /// The pausable component to unregister public void UnregisterPausableComponent(IPausable component) { if (component != null && _pausableComponents.Contains(component)) { _pausableComponents.Remove(component); Logging.Debug($"[DivingGameManager] Unregistered pausable component: {(component as MonoBehaviour)?.name ?? "Unknown"}"); } } /// /// Pause the game and all registered components /// public void Pause() { // Ignore pause requests once the game has reached Game Over if (_isGameOver) return; DoPause(); } public void DoPause(bool turnOffGameInput = true) { // Ignore pause requests once the game has ended if (_isGameOver) return; // Pause all registered components foreach (var component in _pausableComponents) { component.Pause(); } // Change input mode to UI when menu is open if(turnOffGameInput) InputManager.Instance.SetInputMode(InputMode.GameAndUI); Logging.Debug($"[DivingGameManager] Game paused. Paused {_pausableComponents.Count} components."); } /// /// Resume the game and all registered components /// public void DoResume() { // Ignore resume requests once the game has ended if (_isGameOver) return; // Resume all registered components foreach (var component in _pausableComponents) { component.DoResume(); } // Change input mode to UI when menu is open InputManager.Instance.SetInputMode(InputMode.GameAndUI); Logging.Debug($"[DivingGameManager] Game resumed. Resumed {_pausableComponents.Count} components."); } #region Photo Sequence Methods /// /// Called when the viewfinder animation phase changes to reverse (zoom out) /// private void OnReverseAnimationStarted() { Logging.Debug("[DivingGameManager] Viewfinder animation entering reverse (zoom-out) phase"); } /// /// Called when proximity value is updated during animation /// private void OnProximityUpdated(float proximity) { // Store the current proximity value for potential use in scoring _capturedProximity = proximity; } /// /// Called when the player taps during the viewfinder animation /// private void OnViewfinderTappedDuringAnimation(float proximity) { if (!_isPhotoSequenceActive || _currentPhotoTarget == null) return; // Store the proximity value at the time of tap for scoring _capturedProximity = proximity; Logging.Debug($"[DivingGameManager] Player tapped during animation! Proximity: {proximity:F2}"); // Take the picture at the current proximity TakePicture(); } /// /// Takes the picture with the current proximity value for scoring /// private void TakePicture() { if (!_isPhotoSequenceActive || _currentPhotoTarget == null) return; // Notify the monster that its picture was taken _currentPhotoTarget.NotifyPictureTaken(); // Calculate score based on proximity and depth CalculateScore(_currentPhotoTarget, _capturedProximity); //Trigger the Flash Effect if (flashRef != null) { var flash = flashRef.GetComponent(); if (flash != null) { flash.TriggerFlash(); cameraViewfinderManager.PlayShutterSound(); } } // Complete the sequence CompletePhotoSequence(); } /// /// Calculates the score for a picture based on proximity to target and monster depth /// private void CalculateScore(Monster monster, float proximity) { if (monster == null) return; // Calculate base points from depth int depthBonus = Mathf.FloorToInt(Mathf.Abs(monster.transform.position.y) * _settings.DepthMultiplier); // Apply proximity multiplier (0-100%) float proximityMultiplier = Mathf.Clamp01(proximity); // Ensure it's in 0-1 range int proximityBonus = Mathf.RoundToInt(_settings.BasePoints * proximityMultiplier); // Calculate total score int pointsAwarded = _settings.BasePoints + proximityBonus + depthBonus; Logging.Debug($"[DivingGameManager] Picture score calculation: base={proximityBonus} (proximity={proximity:F2}), " + $"depth bonus={depthBonus}, total={pointsAwarded}"); // Add score _playerScore += pointsAwarded; // Fire events OnScoreChanged?.Invoke(_playerScore); OnPictureTaken?.Invoke(monster, pointsAwarded); } /// /// Handles completion of the viewfinder animation /// private void OnViewfinderAnimationCompleted() { if (!_isPhotoSequenceActive || _currentPhotoTarget == null) return; // Take the picture at whatever the final proximity was TakePicture(); } /// /// Cleans up and completes the picture-taking sequence /// private void CompletePhotoSequence() { if (!_isPhotoSequenceActive) return; // Notify listeners that photo sequence has completed (with proximity score) if (_currentPhotoTarget != null) { OnPhotoSequenceCompleted?.Invoke(_currentPhotoTarget, _capturedProximity); // We no longer despawn the monster after taking a picture // _currentPhotoTarget.Despawn(false); // Just mark the photo sequence as no longer in progress _currentPhotoTarget.NotifyPictureTaken(); } DoResume(); // Reset state _isPhotoSequenceActive = false; _currentPhotoTarget = null; Logging.Debug($"[DivingGameManager] Completed photo sequence with proximity score: {_capturedProximity:F2}"); } /// /// Register a component to be exempt from pausing during photo sequence /// public void RegisterExemptFromPhotoSequencePausing(IPausable component) { if (!_exemptFromPhotoSequencePausing.Contains(component)) { _exemptFromPhotoSequencePausing.Add(component); } } #endregion #region Game Initialization /// /// Initializes the game components once the orientation is correct. /// This is called by SceneOrientationEnforcer when the device is properly oriented. /// public void InitializeGame() { if (_isGameInitialized) { DoResume(); return; } // Prevent double initialization if (_isGameInitialized) return; Logging.Debug("[DivingGameManager] Initializing game"); // Subscribe to tile spawned event TrenchTileSpawner tileSpawner = FindFirstObjectByType(); if (tileSpawner != null) { tileSpawner.onTileSpawned.AddListener(OnTileSpawned); } else { Logging.Warning("No TrenchTileSpawner found in scene. Monster spawning won't work."); } // Mark as initialized _isGameInitialized = true; // Notify all listeners that the game is initialized OnGameInitialized?.Invoke(); } #endregion private void DoMonsterSpawned(Monster monster) { if (monster != null) { // Subscribe to monster enter/exit events monster.OnPlayerEnterDetectionRange += OnPlayerEnterMonsterRange; monster.OnPlayerExitDetectionRange += OnPlayerExitMonsterRange; monster.OnMonsterDespawned += DoMonsterDespawned; // Add to active monsters list _activeMonsters.Add(monster); } } private void DoMonsterDespawned(Monster monster) { // Remove from active list _activeMonsters.Remove(monster); // Unsubscribe from monster events if (monster != null) { monster.OnPlayerEnterDetectionRange -= OnPlayerEnterMonsterRange; monster.OnPlayerExitDetectionRange -= OnPlayerExitMonsterRange; monster.OnMonsterDespawned -= DoMonsterDespawned; } } // Handles player entering monster detection range private void OnPlayerEnterMonsterRange(Monster monster) { if (monster != null && !_isPhotoSequenceActive) { // Store current target for later use _currentPhotoTarget = monster; // Show the full-screen viewfinder (first step in two-step process) if (viewfinderManager != null) { viewfinderManager.ShowFullScreenViewfinder(); Logging.Debug($"[DivingGameManager] Player entered range of monster {monster.name}, showing full-screen viewfinder"); } } } // Handles player exiting monster detection range private void OnPlayerExitMonsterRange(Monster monster) { // Only hide viewfinder if we're not already in a photo sequence if (!_isPhotoSequenceActive && monster == _currentPhotoTarget) { // Hide the viewfinder if (viewfinderManager != null) { viewfinderManager.HideViewfinder(); Logging.Debug($"[DivingGameManager] Player exited range of monster {monster.name}, hiding viewfinder"); } // Clear current target _currentPhotoTarget = null; } } // Called when the player taps on the viewfinder private void OnViewfinderTapped() { // Only proceed if we have a valid target and not already in a sequence if (_currentPhotoTarget != null && !_isPhotoSequenceActive && _currentPhotoTarget.IsPlayerInDetectionRange) { // Pause the game immediately DoPause(false); Logging.Debug($"[DivingGameManager] Pausing game before starting viewfinder animation"); // Mark the photo sequence as active _isPhotoSequenceActive = true; // Mark monster as in photo sequence _currentPhotoTarget.SetPhotoSequenceInProgress(); // Reset captured proximity _capturedProximity = 0f; // Notify listeners that photo sequence has started OnPhotoSequenceStarted?.Invoke(_currentPhotoTarget); // Start the complete animation sequence to target monster if (viewfinderManager != null) { viewfinderManager.StartViewfinderSequence(_currentPhotoTarget.transform); Logging.Debug($"[DivingGameManager] Viewfinder tapped for monster {_currentPhotoTarget.name}, starting animation sequence"); } else { Debug.LogError("[DivingGameManager] No ViewfinderManager available!"); CompletePhotoSequence(); } } } } }