using System.Collections.Generic; using UnityEngine; public class GameManager : MonoBehaviour { public static GameManager Instance { get; private set; } [Header("Level Flow")] [SerializeField] private GameObject winScreenPrefab; [SerializeField] private GameObject gameOverScreenPrefab; [Header("Ball Queue")] [SerializeField] private GameObject ballPrefab; [SerializeField] private int initialBallCount = 5; [SerializeField] private Vector3 ballSpawnOffset = new Vector3(0f, 0.7f, 0f); [SerializeField] private Transform reserveAnchor; [SerializeField] private float reserveBallSpacing = 0.5f; [SerializeField] private Vector2 reserveStartViewport = new Vector2(0.92f, 0.12f); [Header("Random Effects")] [SerializeField] private BallEffectDefinition[] randomEffectPool; private readonly Queue _reserveBalls = new Queue(); private Transform _paddle; private Camera _gameCamera; private bool _gameWon; private bool _gameOver; private BallScript _currentQueueBall; private int _remainingBlockCount; private UpgradeState _upgrades; void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; Time.timeScale = 1f; Time.fixedDeltaTime = 0.01f; _upgrades = new UpgradeState(); } void Start() { ResolveSceneReferences(); _remainingBlockCount = FindObjectsByType(FindObjectsSortMode.None).Length; InitializeBallQueue(); } public void OnBallEnteredDeathZone(BallScript ball) { if (_gameWon || _gameOver || ball == null) return; bool wasCurrentQueueBall = ball == _currentQueueBall; Destroy(ball.gameObject); if (HasOtherBallsInPlay(ball)) return; if (wasCurrentQueueBall) _currentQueueBall = null; if (_reserveBalls.Count > 0) { MoveNextQueuedBallToPaddle(); return; } ShowGameOver(); } private bool HasOtherBallsInPlay(BallScript excludedBall) { BallScript[] balls = FindObjectsByType(FindObjectsSortMode.None); for (int i = 0; i < balls.Length; i++) { BallScript otherBall = balls[i]; if (otherBall == null || otherBall == excludedBall) continue; if (otherBall.IsInPlay) return true; } return false; } /// Called by BlockScript when a block is destroyed. public void OnBlockDestroyed() { if (_gameWon || _gameOver) return; _remainingBlockCount = Mathf.Max(0, _remainingBlockCount - 1); if (_remainingBlockCount == 0) WinGame(); } private void InitializeBallQueue() { if (ballPrefab == null) { Debug.LogWarning("GameManager: ballPrefab is not assigned. Cannot create ball queue."); return; } if (_paddle == null) { Debug.LogWarning("GameManager: Paddle not found. Tag the paddle as 'Paddle' or ensure PaddleScript exists in scene."); return; } if (_gameCamera == null) { Debug.LogWarning("GameManager: No camera found for reserve-ball placement."); return; } if (initialBallCount <= 0) { Debug.LogWarning("GameManager: initialBallCount must be > 0."); return; } Debug.Log($"GameManager: Initializing {initialBallCount} balls. Random effect pool has {(randomEffectPool?.Length ?? 0)} effects."); for (int i = 0; i < initialBallCount; i++) { Vector3 reservePosition; if (reserveAnchor != null) { // Preferred placement: line reserve balls to the right from an explicit scene anchor. reservePosition = reserveAnchor.position + Vector3.right * (i * reserveBallSpacing); } else { // Fallback placement if no anchor is assigned. Vector3 reserveStartWorld = _gameCamera.ViewportToWorldPoint( new Vector3(reserveStartViewport.x, reserveStartViewport.y, Mathf.Abs(_gameCamera.transform.position.z))); reservePosition = reserveStartWorld + Vector3.right * (i * reserveBallSpacing); } reservePosition.z = 0f; GameObject ballObject = Instantiate(ballPrefab, reservePosition, Quaternion.identity); BallScript ball = ballObject.GetComponent(); if (ball == null) { Debug.LogWarning("GameManager: Spawned ballPrefab has no BallScript component."); Destroy(ballObject); continue; } ball.SetQueueManaged(true); ball.SetPaddleReference(_paddle, ballSpawnOffset); BallEffectDefinition effect = GetRandomEffect(); Debug.Log($"GameManager: Ball {i} assigned effect: {(effect != null ? effect.EffectType.ToString() : "NULL")}"); ball.SetEffectDefinition(effect); ball.ParkAtPosition(reservePosition); _reserveBalls.Enqueue(ball); } MoveNextQueuedBallToPaddle(); } private void ResolveSceneReferences() { _paddle = GameObject.FindGameObjectWithTag("Paddle")?.transform; if (_paddle == null) { PaddleScript paddleScript = FindFirstObjectByType(); if (paddleScript != null) _paddle = paddleScript.transform; } if (_paddle == null) { GameObject paddleByName = GameObject.Find("Paddle"); if (paddleByName != null) _paddle = paddleByName.transform; } _gameCamera = Camera.main; if (_gameCamera == null) _gameCamera = FindFirstObjectByType(); } private void MoveNextQueuedBallToPaddle() { while (_reserveBalls.Count > 0) { BallScript nextBall = _reserveBalls.Dequeue(); if (nextBall == null) continue; _currentQueueBall = nextBall; _currentQueueBall.PrepareOnPaddle(); return; } ShowGameOver(); } private BallEffectDefinition GetRandomEffect() { if (randomEffectPool == null || randomEffectPool.Length == 0) return null; int idx = Random.Range(0, randomEffectPool.Length); BallEffectDefinition baseDefinition = randomEffectPool[idx]; // Apply active upgrades to the definition return UpgradeManager.ApplyUpgrades(baseDefinition, _upgrades); } private void WinGame() { _gameWon = true; Time.timeScale = 0f; if (winScreenPrefab != null) Instantiate(winScreenPrefab); else Debug.Log("YOU WIN! All blocks destroyed!"); } private void ShowGameOver() { if (_gameOver || _gameWon) return; _gameOver = true; Time.timeScale = 0f; if (gameOverScreenPrefab != null) Instantiate(gameOverScreenPrefab); else Debug.Log("GAME OVER! No balls remaining."); } #region Upgrade API /// Get the current upgrade state (for debugging or UI display). public UpgradeState GetUpgradeState() => _upgrades; /// Increase explosive effect radius. public void UpgradeExplosiveRadius(float radiusBonus) { _upgrades.ExplosiveRadiusBonus += radiusBonus; Debug.Log($"Upgraded Explosive Radius: +{radiusBonus} (Total: +{_upgrades.ExplosiveRadiusBonus})"); } /// Increase explosive damage multiplier. public void UpgradeExplosiveDamage(float multiplier) { _upgrades.ExplosiveDamageMultiplier *= multiplier; Debug.Log($"Upgraded Explosive Damage: x{multiplier} (Total: x{_upgrades.ExplosiveDamageMultiplier:F2})"); } /// Increase split angle diversity. public void UpgradeSplitAngle(float angleBonus) { _upgrades.SplitAngleBonus += angleBonus; Debug.Log($"Upgraded Split Angle: +{angleBonus} (Total: +{_upgrades.SplitAngleBonus})"); } /// Increase split child projectile speed. public void UpgradeSplitSpeed(float multiplier) { _upgrades.SplitSpeedMultiplier *= multiplier; Debug.Log($"Upgraded Split Speed: x{multiplier} (Total: x{_upgrades.SplitSpeedMultiplier:F2})"); } /// Increase split child projectile damage. public void UpgradeSplitDamage(float multiplier) { _upgrades.SplitDamageMultiplier *= multiplier; Debug.Log($"Upgraded Split Damage: x{multiplier} (Total: x{_upgrades.SplitDamageMultiplier:F2})"); } /// Increase piercing hits capacity. public void UpgradePiercingHits(int hitBonus) { _upgrades.PiercingHitsBonus += hitBonus; Debug.Log($"Upgraded Piercing Hits: +{hitBonus} (Total: +{_upgrades.PiercingHitsBonus})"); } /// Increase homing turn rate (aggressiveness). public void UpgradeHomingTurnRate(float multiplier) { _upgrades.HomingTurnRateMultiplier *= multiplier; Debug.Log($"Upgraded Homing Turn Rate: x{multiplier} (Total: x{_upgrades.HomingTurnRateMultiplier:F2})"); } /// Increase teleport distance per jump. public void UpgradeTeleportDistance(float multiplier) { _upgrades.TeleportDistanceMultiplier *= multiplier; Debug.Log($"Upgraded Teleport Distance: x{multiplier} (Total: x{_upgrades.TeleportDistanceMultiplier:F2})"); } /// Reset all upgrades to their default values. public void ResetAllUpgrades() { _upgrades.ResetAll(); Debug.Log("All upgrades reset to default values."); } #endregion }