Files
AppleHillsProduction/Assets/Scripts/Minigames/DivingForPictures/DivingGameManager.cs
2025-10-20 23:18:31 +02:00

1118 lines
42 KiB
C#

using AppleHills.Core.Interfaces;
using AppleHills.Core.Settings;
using Cinematics;
using Core;
using Input;
using Minigames.DivingForPictures.PictureCamera;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UI;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Playables;
using Utility;
namespace Minigames.DivingForPictures
{
public class DivingGameManager : MonoBehaviour, 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<Monster> activeMonsters = new List<Monster>();
// 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<int> OnScoreChanged;
public event Action<Monster> OnMonsterSpawned;
public event Action<Monster, int> OnPictureTaken;
public event Action<float> OnSpawnProbabilityChanged;
public event Action OnGameOver;
public event Action<int> OnRopeBroken; // Passes remaining ropes count
public event Action<float> 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;
// Event for game components to subscribe to
public event Action OnGameInitialized;
// Pause state
private bool _isPaused = false;
// List of pausable components controlled by this manager
private List<IPausable> _pausableComponents = new List<IPausable>();
// IPausable implementation
public bool IsPaused => _isPaused;
// Photo sequence state
private bool _isPhotoSequenceActive = false;
private Monster _currentPhotoTarget = null;
private Dictionary<IPausable, bool> _pauseStateBackup = new Dictionary<IPausable, bool>();
private float _capturedProximity = 0f; // New: tracks how close to target the photo was taken (0-1)
// List of components to exempt from pausing during photo sequence
private List<IPausable> _exemptFromPhotoSequencePausing = new List<IPausable>();
// Photo sequence events
public event Action<Monster> OnPhotoSequenceStarted;
public event Action<Monster, float> OnPhotoSequenceCompleted; // Now includes proximity score
public event Action<float> OnPhotoSequenceProgressUpdated;
private static DivingGameManager _instance = null;
private static bool _isQuitting = false;
public AudioSource deathAudioPlayer;
public CameraViewfinderManager cameraViewfinderManager;
public static DivingGameManager Instance => _instance;
private void Awake()
{
settings = GameManager.GetSettingsObject<IDivingMinigameSettings>();
currentSpawnProbability = settings?.BaseSpawnProbability ?? 0.2f;
if (_instance == null)
{
_instance = this;
}
else if (_instance != this)
{
Destroy(gameObject);
}
}
private void OnApplicationQuit()
{
_isQuitting = true;
}
private void Start()
{
// Find PauseMenu and subscribe to its events
PauseMenu pauseMenu = PauseMenu.Instance;
if (pauseMenu != null)
{
pauseMenu.OnGamePaused += Pause;
pauseMenu.OnGameResumed += DoResume;
Logging.Debug("[DivingGameManager] Subscribed to PauseMenu events");
}
else
{
Logging.Warning("[DivingGameManager] PauseMenu not found. Pause functionality won't work properly.");
}
// Register this manager with the global GameManager
if (GameManager.Instance != null)
{
GameManager.Instance.RegisterPausableComponent(this);
}
// Subscribe to SceneOrientationEnforcer's event
if (SceneOrientationEnforcer.Instance != null)
{
SceneOrientationEnforcer.Instance.OnOrientationCorrect += InitializeGame;
SceneOrientationEnforcer.Instance.OnOrientationIncorrect += Pause;
// If orientation is already correct, initialize right away
// This prevents issues if the orientation was already correct before subscription
if (SceneOrientationEnforcer.Instance.IsOrientationCorrect())
{
InitializeGame();
}
}
else
{
Logging.Warning("[DivingGameManager] SceneOrientationEnforcer not found. Initializing game immediately.");
InitializeGame();
}
// Subscribe to player damage events (this doesn't depend on initialization)
PlayerCollisionBehavior.OnDamageTaken += OnPlayerDamageTaken;
// Validate rope references (this doesn't depend on initialization)
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;
// Add the viewfinder manager to exempt list to ensure it keeps working during photo sequences
if (viewfinderManager is IPausable viewfinderPausable)
{
RegisterExemptFromPhotoSequencePausing(viewfinderPausable);
}
}
OnMonsterSpawned += DoMonsterSpawned;
CinematicsManager.Instance.OnCinematicStopped += EndGame;
}
private void OnDestroy()
{
// Unsubscribe from events when the manager is destroyed
PlayerCollisionBehavior.OnDamageTaken -= OnPlayerDamageTaken;
if (SceneOrientationEnforcer.Instance != null)
{
SceneOrientationEnforcer.Instance.OnOrientationCorrect -= InitializeGame;
SceneOrientationEnforcer.Instance.OnOrientationIncorrect -= Pause;
}
// Unsubscribe from PauseMenu events
PauseMenu pauseMenu = PauseMenu.Instance;
if (pauseMenu != null)
{
pauseMenu.OnGamePaused -= Pause;
pauseMenu.OnGameResumed -= DoResume;
}
// Unregister from GameManager
if (GameManager.Instance != null)
{
GameManager.Instance.UnregisterPausableComponent(this);
}
// 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()
{
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<MonsterSpawnPoint>();
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<Monster>();
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<MonsterSpawnPoint>();
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);
}
/// <summary>
/// Called when the player takes damage from any collision
/// </summary>
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.");
}
}
/// <summary>
/// Breaks the next available rope in the sequence
/// </summary>
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++;
}
}
/// <summary>
/// Manually break a rope (for testing or external events)
/// </summary>
public void ForceBreakRope()
{
if (!isGameOver)
{
OnPlayerDamageTaken();
}
}
/// <summary>
/// Triggers game over state when all ropes are broken
/// </summary>
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();
}
/// <summary>
/// Validates rope references and logs warnings if any are missing
/// </summary>
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!");
}
}
}
/// <summary>
/// Resets the rope system for a new game
/// </summary>
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.");
}
/// <summary>
/// Starts the surfacing mode - reverses trench direction and adjusts all spawned entities
/// </summary>
public void StartSurfacing()
{
if (_isSurfacing) return; // Already surfacing
_isSurfacing = true;
// 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<TrenchTileSpawner>();
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<Component>())
{
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<Component>())
{
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<BubbleSpawner>();
if (bubbleSpawner != null)
{
bubbleSpawner.StartSurfacing();
}
// 4. Find obstacle spawner and set up for velocity changes
ObstacleSpawner obstacleSpawner = FindFirstObjectByType<ObstacleSpawner>();
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}");
}
/// <summary>
/// Coroutine to animate the rock falling below the screen
/// </summary>
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;
}
/// <summary>
/// Coroutine to reset the player's X position to 0 over time
/// </summary>
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;
}
/// <summary>
/// Coroutine to handle the surfacing sequence timing
/// </summary>
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<TrenchTileSpawner>();
if (tileSpawner != null)
{
// Tell it to stop spawning new tiles
tileSpawner.StopSpawning();
Logging.Debug("[DivingGameManager] Stopped spawning new tiles after delay");
}
}
/// <summary>
/// Called when the last tile leaves the screen
/// </summary>
private void OnLastTileLeft()
{
// Play the timeline
if (surfacingTimeline != null)
{
//Instead of surfacingTimeline, play the outro cinematic
deathAudioPlayer.Stop();
CinematicsManager.Instance.LoadAndPlayCinematic("SurfacingCinematic");
CinematicsManager.Instance.ShowCinematicBackground(true);
//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()
{
// Clean up active monsters
foreach (var monster in activeMonsters.ToArray())
{
if (monster != null)
{
monster.DespawnMonster();
}
}
CinematicsManager.Instance.ShowGameOverScreen();
activeMonsters.Clear();
// Final score could be saved to player prefs or other persistence
Logging.Debug($"Final Score: {playerScore}");
}
/// <summary>
/// Starts a smooth transition to the new velocity factor
/// </summary>
/// <param name="targetFactor">Target velocity factor (e.g., -1.0 for surfacing speed)</param>
public void SetVelocityFactor(float targetFactor)
{
if (velocityTransitionCoroutine != null)
{
StopCoroutine(velocityTransitionCoroutine);
}
velocityTransitionCoroutine = StartCoroutine(TransitionVelocityFactor(targetFactor));
}
/// <summary>
/// Coroutine to smoothly transition the velocity factor over time
/// </summary>
private IEnumerator<WaitForEndOfFrame> 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);
}
/// <summary>
/// Register a component as pausable with this manager
/// </summary>
/// <param name="component">The pausable component to register</param>
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 (_isPaused)
{
component.Pause();
}
Logging.Debug($"[DivingGameManager] Registered pausable component: {(component as MonoBehaviour)?.name ?? "Unknown"}");
}
}
/// <summary>
/// Unregister a pausable component
/// </summary>
/// <param name="component">The pausable component to unregister</param>
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"}");
}
}
/// <summary>
/// Pause the game and all registered components
/// </summary>
public void Pause()
{
DoPause();
}
public void DoPause(bool turnOffGameInput = true)
{
if (_isPaused) return; // Already paused
_isPaused = true;
// 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.");
}
/// <summary>
/// Resume the game and all registered components
/// </summary>
public void DoResume()
{
if (!_isPaused) return; // Already running
_isPaused = false;
// 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
/// <summary>
/// Called when the viewfinder animation phase changes to reverse (zoom out)
/// </summary>
private void OnReverseAnimationStarted()
{
Logging.Debug("[DivingGameManager] Viewfinder animation entering reverse (zoom-out) phase");
}
/// <summary>
/// Called when proximity value is updated during animation
/// </summary>
private void OnProximityUpdated(float proximity)
{
// Store the current proximity value for potential use in scoring
_capturedProximity = proximity;
}
/// <summary>
/// Called when the player taps during the viewfinder animation
/// </summary>
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();
}
/// <summary>
/// Takes the picture with the current proximity value for scoring
/// </summary>
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<FlashBehaviour>();
if (flash != null)
{
flash.TriggerFlash();
cameraViewfinderManager.PlayShutterSound();
}
}
// Complete the sequence
CompletePhotoSequence();
}
/// <summary>
/// Calculates the score for a picture based on proximity to target and monster depth
/// </summary>
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);
}
/// <summary>
/// Handles completion of the viewfinder animation
/// </summary>
private void OnViewfinderAnimationCompleted()
{
if (!_isPhotoSequenceActive || _currentPhotoTarget == null)
return;
// Take the picture at whatever the final proximity was
TakePicture();
}
/// <summary>
/// Cleans up and completes the picture-taking sequence
/// </summary>
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}");
}
/// <summary>
/// Register a component to be exempt from pausing during photo sequence
/// </summary>
public void RegisterExemptFromPhotoSequencePausing(IPausable component)
{
if (!_exemptFromPhotoSequencePausing.Contains(component))
{
_exemptFromPhotoSequencePausing.Add(component);
}
}
#endregion
#region Game Initialization
/// <summary>
/// Initializes the game components once the orientation is correct.
/// This is called by SceneOrientationEnforcer when the device is properly oriented.
/// </summary>
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<TrenchTileSpawner>();
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();
}
}
}
}
}