using System.Collections; using System.Collections.Generic; using Core.Domain; using Core.Ports; using TMPro; using UnityEngine; using UnityEngine.SceneManagement; namespace Infrastructure.Unity { public class GameBootstrap : MonoBehaviour { [Header("Infrastructure")] [SerializeField] private LevelGenerator levelGenerator; [SerializeField] private OrbViewAdapter orbPrefab; [SerializeField] private PlayerController playerPrefab; [SerializeField] private DeathPlaneAdapter deathPlanePrefab; [SerializeField] private SoundManager soundManager; [SerializeField] private RumbleManager rumbleManager; [SerializeField] private BeatPulseController beatPulseController; [SerializeField] private CameraController cameraController; [SerializeField] private NpcController npcPrefab; [SerializeField] private HunterNpcController hunterNpcPrefab; [SerializeField] private FloorVisibilityManager floorVisibilityManager; [Header("Ui")] [SerializeField] private TMP_Text scoreText; [SerializeField] private TMP_Text highScoreText; [SerializeField] private GameObject gameOverUi; [SerializeField] private GameObject pauseUi; [SerializeField] private GameObject startScreenUi; [Header("Settings")] [SerializeField] private float restartTime = 3f; [Header("Power Ups")] [SerializeField] private PowerUpViewAdapter powerUpPrefab; private readonly List _allTiles = new(); private readonly Dictionary _tileViews = new(); private GameSession _gameSession; private IPersistenceService _persistenceService; private InputSystem_Actions _actions; private PlayerController _playerInstance; private GameObject _currentOrbInstance; private bool _isGameRunning; private int _currentPlayerFloorIndex; private int _currentDisplayedScore; private float _inputBlockTimer; private bool _isPaused; private bool _levelGenerated; private void OnEnable() { _actions = new InputSystem_Actions(); _actions.Player.Enable(); } private void OnDisable() { _actions.Player.Disable(); beatPulseController.OnMeasure.RemoveListener(OnBeatMeasure); } private void Start() { _inputBlockTimer = 0.5f; _persistenceService = new PlayerPrefsPersistenceAdapter(); _gameSession = new GameSession(_allTiles, _persistenceService); // Set Theme based on High Score ThemeManager.CurrentTheme = ThemeManager.GetTheme(_gameSession.HighScore); var floorsCount = levelGenerator ? levelGenerator.FloorsCount : 1; if (levelGenerator) { StartCoroutine(levelGenerator.GenerateAsync(soundManager, _allTiles, _tileViews, cameraController, rumbleManager, () => { if (!floorVisibilityManager) { floorVisibilityManager = gameObject.AddComponent(); } floorVisibilityManager.Initialize(_gameSession, _allTiles, _tileViews, floorsCount); SpawnDeathPlane(); SpawnPlayer(); if (gameOverUi) gameOverUi.SetActive(false); if (startScreenUi) startScreenUi.SetActive(true); // Show start screen NOW WireEvents(); UpdateScoreUi(_gameSession.Score); _levelGenerated = true; })); } if (gameOverUi) gameOverUi.SetActive(false); if (startScreenUi) startScreenUi.SetActive(true); } private void Update() { if (_inputBlockTimer > 0f) { _inputBlockTimer -= Time.deltaTime; return; } if (_actions.Player.Pause.triggered && _isGameRunning) { TogglePause(); } if (_isPaused) return; if (!_isGameRunning && _levelGenerated) { if (_actions.Player.StartGame.triggered) { StartGameSequence(); } return; } if (_playerInstance) { var playerY = _playerInstance.transform.position.y; // Calculate current floor index based on Y height (inverse logic from Generator) // Note: Generator uses negative offsets: 0, -15, -30. // So Floor 0 is at Y=0. Floor 1 is at Y=-15. var heightDist = levelGenerator.FloorHeightDistance; var maxFloors = levelGenerator.FloorsCount; var rawFloor = Mathf.RoundToInt(-playerY / heightDist); _currentPlayerFloorIndex = Mathf.Clamp(rawFloor, 0, maxFloors - 1); _gameSession.SetPlayerFloor(_currentPlayerFloorIndex); floorVisibilityManager.UpdateFloorVisibility(_currentPlayerFloorIndex); } var dt = Time.deltaTime; if (_isGameRunning) _gameSession.Tick(dt); var dilation = _gameSession.TimeDilation; // Hard Mode: Decay faster as score increases var decayMultiplier = 1.0f + (_gameSession.Score / 500f); for (var i = _allTiles.Count - 1; i >= 0; i--) { _allTiles[i].Tick(dt * dilation * decayMultiplier); } } private void TogglePause() { _isPaused = !_isPaused; if (_isPaused) { Time.timeScale = 0f; if (soundManager) soundManager.SetPaused(true); if (pauseUi) pauseUi.SetActive(true); if (rumbleManager) rumbleManager.SetPaused(true); } else { Time.timeScale = 1f; if (soundManager) soundManager.SetPaused(false); if (pauseUi) pauseUi.SetActive(false); if (rumbleManager) rumbleManager.SetPaused(false); } } private void SpawnVisualOrb(string tileId) { if (_currentOrbInstance) Destroy(_currentOrbInstance); if (!_tileViews.TryGetValue(tileId, out var tileView)) return; if (!tileView) return; var spawnPos = tileView.transform.position + Vector3.up; var orb = Instantiate(orbPrefab, spawnPos, Quaternion.identity); orb.OnCollected += () => _gameSession.OrbCollected(); _currentOrbInstance = orb.gameObject; } private void HandleOrbReset() { if (_currentOrbInstance) { Destroy(_currentOrbInstance); } } private void UpdateScoreUi(int newScore) { if (!scoreText) return; LeanTween.cancel(scoreText.gameObject); scoreText.rectTransform.localScale = Vector3.one; LeanTween.scale(scoreText.rectTransform, Vector3.one * 1.5f, 0.5f) .setEasePunch(); LeanTween.value(scoreText.gameObject, (float val) => { var currentVal = Mathf.RoundToInt(val); var combo = _gameSession?.ComboMultiplier ?? 1; var comboText = combo > 1 ? $" x{combo}" : ""; scoreText.text = $"{currentVal}{comboText}"; }, _currentDisplayedScore, newScore, 0.5f) .setEaseOutExpo(); _currentDisplayedScore = newScore; if (highScoreText && _gameSession != null) highScoreText.text = $"BEST: {_gameSession.HighScore}"; } private void SpawnPlayer() { var spawnPos = new Vector3(0f, 5f, 0f); _playerInstance = Instantiate(playerPrefab, spawnPos, Quaternion.identity); _playerInstance.enabled = false; _playerInstance.Rigidbody.isKinematic = true; if (cameraController) { cameraController.SetTarget(_playerInstance.transform); } } private void SpawnDeathPlane() { if (!levelGenerator) return; var lowestY = -(levelGenerator.FloorsCount * levelGenerator.FloorHeightDistance) - 5f; var pos = new Vector3(levelGenerator.GridSizeX / 2f, lowestY, levelGenerator.GridSizeY / 2f); var plane = Instantiate(deathPlanePrefab, pos, Quaternion.identity); plane.transform.localScale = new Vector3(levelGenerator.GridSizeX * 2f, 1f, levelGenerator.GridSizeY * 2f); plane.OnPlayerFell += () => _gameSession.EndGame(); } private void HandleGameOver() { _isGameRunning = false; if (beatPulseController) beatPulseController.StopTracking(); if (gameOverUi) gameOverUi.SetActive(true); StartCoroutine(RestartRoutine()); } private IEnumerator RestartRoutine() { yield return new WaitForSeconds(restartTime); SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex); } private void WireEvents() { _gameSession.OnScoreChanged += UpdateScoreUi; _gameSession.OnOrbSpawned += SpawnVisualOrb; _gameSession.OnOrbReset += HandleOrbReset; _gameSession.OnGameOver += HandleGameOver; _gameSession.OnSpawnNpc += SpawnNpc; _gameSession.OnSpawnPowerUp += SpawnPowerUp; if (beatPulseController) { beatPulseController.OnMeasure.AddListener(OnBeatMeasure); } if (!soundManager) return; _gameSession.OnScoreChanged += _ => soundManager.PlayScore(); _gameSession.OnGameOver += () => { soundManager.StopMusic(); soundManager.PlayGameOver(); }; } private void SpawnNpc() { var validTiles = _allTiles.FindAll(t => t.Floor == 0 && t.CurrentState == TileState.Stable); if (validTiles.Count == 0) { validTiles = _allTiles.FindAll(t => t.CurrentState == TileState.Stable); } if (validTiles.Count == 0) return; var randomTile = validTiles[Random.Range(0, validTiles.Count)]; if (!_tileViews.TryGetValue(randomTile.Id, out var tileView)) return; if (!tileView) return; var spawnPos = tileView.transform.position + Vector3.up * 5f; if (_playerInstance && hunterNpcPrefab && Random.value < 0.3f) { var hunter = Instantiate(hunterNpcPrefab, spawnPos, Quaternion.identity); hunter.Initialize(_playerInstance, () => _gameSession.EndGame(), () => _gameSession.TimeDilation); } else if (npcPrefab) { var npc = Instantiate(npcPrefab, spawnPos, Quaternion.identity); npc.Initialize(() => _gameSession.TimeDilation, () => _gameSession.EndGame()); } soundManager.PlayNpcSpawn(); } private void StartGameSequence() { _isGameRunning = true; _currentDisplayedScore = 0; if (startScreenUi) startScreenUi.SetActive(false); if (soundManager) { soundManager.PlayGameStart(); soundManager.PlayMusic(); if (beatPulseController) beatPulseController.BeginTracking(); } if (_playerInstance) { _playerInstance.enabled = true; _playerInstance.Rigidbody.isKinematic = false; } _gameSession.StartGame(); } private void SpawnPowerUp(PowerUpType type, string tileId) { if (!_tileViews.TryGetValue(tileId, out var tileView)) return; if (!tileView) return; var spawnPos = tileView.transform.position + Vector3.up * 0.5f; var instance = Instantiate(powerUpPrefab, spawnPos, Quaternion.identity); instance.Configure(type); instance.OnCollected += (t) => { cameraController?.Shake(0.2f, 0.15f); rumbleManager?.PulseMedium(); if (t == PowerUpType.TimeSlow) { _gameSession.ActivateTimeSlow(10f); } }; } private void OnBeatMeasure() { if (_allTiles.Count == 0) return; var pulseCount = 25; for (var i = 0; i < pulseCount; i++) { var randIndex = Random.Range(0, _allTiles.Count); var tile = _allTiles[randIndex]; if (tile.Floor < _currentPlayerFloorIndex) continue; if (tile.Floor > _currentPlayerFloorIndex + 1) continue; if (tile.CurrentState != TileState.Stable) continue; if (_tileViews.TryGetValue(tile.Id, out var tileView)) { tileView.PulseEmission(Random.Range(1.2f, 2.0f)); } } } } }