using UnityEngine; using System.Collections.Generic; using System; using System.Collections; using UnityEngine.Events; using UnityEngine.Playables; namespace Minigames.DivingForPictures { public class DivingGameManager : MonoBehaviour { [Header("Monster Prefabs")] [Tooltip("Array of monster prefabs to spawn randomly")] [SerializeField] private GameObject[] monsterPrefabs; [Header("Spawn Probability")] [Tooltip("Base chance (0-1) of spawning a monster on each tile")] [SerializeField] private float baseSpawnProbability = 0.2f; [Tooltip("Maximum chance (0-1) of spawning a monster")] [SerializeField] private float maxSpawnProbability = 0.5f; [Tooltip("How fast the probability increases per second")] [SerializeField] private float probabilityIncreaseRate = 0.01f; [Header("Spawn Timing")] [Tooltip("Force a spawn after this many seconds without spawns")] [SerializeField] private float guaranteedSpawnTime = 30f; [Tooltip("Minimum time between monster spawns")] [SerializeField] private float spawnCooldown = 5f; [Header("Scoring")] [Tooltip("Base points for taking a picture")] [SerializeField] private int basePoints = 100; [Tooltip("Additional points per depth unit")] [SerializeField] private int depthMultiplier = 10; [Header("Rope Damage System")] [Tooltip("Ropes that will break one by one as player takes damage")] [SerializeField] private RopeBreaker[] playerRopes; [Header("Surfacing Settings")] [Tooltip("Duration in seconds for speed transition when surfacing")] [SerializeField] private float speedTransitionDuration = 2.0f; [Tooltip("Factor to multiply speed by when surfacing (usually 1.0 for same speed)")] [SerializeField] private float surfacingSpeedFactor = 3.0f; [Tooltip("How long to continue spawning tiles after surfacing begins (seconds)")] [SerializeField] private float surfacingSpawnDelay = 5.0f; [Tooltip("Reference to the PlayableDirector that will play the surfacing timeline")] [SerializeField] private PlayableDirector surfacingTimeline; // 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; // Used to track if we're currently surfacing public bool IsSurfacing => _isSurfacing; private void Awake() { currentSpawnProbability = baseSpawnProbability; } private void Start() { // Subscribe to tile spawned event TrenchTileSpawner tileSpawner = FindFirstObjectByType(); if (tileSpawner != null) { tileSpawner.onTileSpawned.AddListener(OnTileSpawned); } else { Debug.LogWarning("No TrenchTileSpawner found in scene. Monster spawning won't work."); } // Subscribe to player damage events PlayerCollisionBehavior.OnDamageTaken += OnPlayerDamageTaken; // Validate rope references ValidateRopeReferences(); } private void OnDestroy() { // Unsubscribe from events when the manager is destroyed PlayerCollisionBehavior.OnDamageTaken -= OnPlayerDamageTaken; } private void Update() { timeSinceLastSpawn += Time.deltaTime; // Gradually increase spawn probability over time float previousProbability = currentSpawnProbability; if (currentSpawnProbability < maxSpawnProbability) { currentSpawnProbability += probabilityIncreaseRate * Time.deltaTime; currentSpawnProbability = Mathf.Min(currentSpawnProbability, 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 >= guaranteedSpawnTime; bool onCooldown = timeSinceLastSpawn < 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 = baseSpawnProbability; OnSpawnProbabilityChanged?.Invoke(currentSpawnProbability); } } private void SpawnMonster(Transform spawnPoint) { if (monsterPrefabs.Length == 0) { Debug.LogWarning("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); // Subscribe to monster events monster.OnPictureTaken += OnMonsterPictureTaken; monster.OnMonsterDespawned += OnMonsterDespawned; // Add to active monsters list activeMonsters.Add(monster); // Fire event OnMonsterSpawned?.Invoke(monster); } else { Debug.LogError($"Monster prefab {prefab.name} does not have a Monster component!"); Destroy(monsterObj); } } private void OnMonsterPictureTaken(Monster monster) { // Calculate points based on depth int depthBonus = Mathf.FloorToInt(Mathf.Abs(monster.transform.position.y) * depthMultiplier); int pointsAwarded = basePoints + depthBonus; // Add score playerScore += pointsAwarded; // Fire events OnScoreChanged?.Invoke(playerScore); OnPictureTaken?.Invoke(monster, pointsAwarded); } private void OnMonsterDespawned(Monster monster) { // Remove from active list activeMonsters.Remove(monster); // Unsubscribe from events monster.OnPictureTaken -= OnMonsterPictureTaken; monster.OnMonsterDespawned -= OnMonsterDespawned; } /// /// 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(); } else { // Notify listeners about rope break and remaining ropes int remainingRopes = playerRopes.Length - currentRopeIndex; OnRopeBroken?.Invoke(remainingRopes); Debug.Log($"[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 { Debug.LogWarning($"[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; Debug.Log("[DivingGameManager] Game Over! All ropes broken."); // Fire game over event OnGameOver?.Invoke(); // Call the existing end game method EndGame(); } /// /// Validates rope references and logs warnings if any are missing /// private void ValidateRopeReferences() { if (playerRopes == null || playerRopes.Length == 0) { Debug.LogWarning("[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) { Debug.LogWarning($"[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(); } } } Debug.Log("[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; // 1. Initiate smooth velocity transition to surfacing speed float targetVelocityFactor = -1.0f * 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); } // 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()); Debug.Log($"[DivingGameManager] Started surfacing with target velocity factor: {targetVelocityFactor}"); } /// /// Coroutine to handle the surfacing sequence timing /// private IEnumerator SurfacingSequence() { // Wait for the configured delay yield return new WaitForSeconds(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(); Debug.Log("[DivingGameManager] Stopped spawning new tiles after delay"); } } /// /// Called when the last tile leaves the screen /// private void OnLastTileLeft() { // Play the timeline if (surfacingTimeline != null) { surfacingTimeline.Play(); Debug.Log("[DivingGameManager] Last tile left the screen, playing timeline"); } else { Debug.LogWarning("[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(); } } activeMonsters.Clear(); // Final score could be saved to player prefs or other persistence Debug.Log($"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 < speedTransitionDuration) { elapsed += Time.deltaTime; float t = Mathf.Clamp01(elapsed / 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); } } }