using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; using UnityEngine.Serialization; using Pooling; using AppleHills.Core.Settings; using Utils; using AppleHills.Core.Interfaces; namespace Minigames.DivingForPictures { /// /// Spawns and manages trench wall tiles for the endless descender minigame. /// public class TrenchTileSpawner : MonoBehaviour, IPausable { [Header("Tile Prefabs")] [Tooltip("List of possible trench tile prefabs.")] [SerializeField] private List tilePrefabs; [Header("Initial Tile")] [Tooltip("Prefab for the initial trench tile. This will always be spawned first.")] [SerializeField] private GameObject initialTilePrefab; [Header("Events")] [FormerlySerializedAs("OnTileSpawned")] public UnityEvent onTileSpawned; [FormerlySerializedAs("OnTileDestroyed")] public UnityEvent onTileDestroyed; // Settings references private IDivingMinigameSettings _settings; private DivingDeveloperSettings _devSettings; // Private fields private readonly Dictionary _tileHeights = new Dictionary(); private readonly List _activeTiles = new List(); private readonly Dictionary _tileLastUsed = new Dictionary(); private int _spawnCounter; private float _speedUpTimer; private UnityEngine.Camera _mainCamera; private float _screenBottom; private float _screenTop; private TrenchTilePool _tilePool; // Current velocity for tile movement private float _currentVelocity; private const float TileSpawnZ = -1f; private const float DefaultTileHeight = 5f; // Direction state private bool _isSurfacing = false; private bool _stopSpawning = false; // Event triggered when the last tile leaves the screen after stopping spawning public UnityEvent onLastTileLeft = new UnityEvent(); // Velocity management private float _baseMoveSpeed; private float _velocityFactor = 1.0f; // Coroutine references private Coroutine _movementCoroutine; private Coroutine _tileDestructionCoroutine; private Coroutine _tileSpawningCoroutine; private Coroutine _speedRampingCoroutine; // Screen normalization private float _screenNormalizationFactor = 1.0f; // Tracks if a floating area in the middle is currently active private bool isFloatingAreaActive = false; // Current depth of the trench private int _currentDepth = 0; [System.Serializable] public struct DepthDifficultyRange { public int minDepth; public int maxDepth; public int difficulty; } [Header("Difficulty Depths")] [Tooltip("Configure depth ranges and their corresponding difficulty levels.")] [SerializeField] private List depthDifficultyRanges = new List { new DepthDifficultyRange { minDepth = 0, maxDepth = 10, difficulty = 1 }, new DepthDifficultyRange { minDepth = 11, maxDepth = 20, difficulty = 2 }, new DepthDifficultyRange { minDepth = 21, maxDepth = 30, difficulty = 3 }, new DepthDifficultyRange { minDepth = 31, maxDepth = 40, difficulty = 4 }, new DepthDifficultyRange { minDepth = 41, maxDepth = int.MaxValue, difficulty = 5 } }; public int CurrentDifficulty { get { foreach (var range in depthDifficultyRanges) { if (_currentDepth >= range.minDepth && _currentDepth <= range.maxDepth) return range.difficulty; } return 1; // Default fallback } } // Pause state private bool _isPaused = false; // IPausable implementation public bool IsPaused => _isPaused; private void Awake() { _mainCamera = UnityEngine.Camera.main; // Get settings from GameManager _settings = GameManager.GetSettingsObject(); _devSettings = GameManager.GetDeveloperSettings(); if (_settings == null) { Debug.LogError("[TrenchTileSpawner] Failed to load diving minigame settings!"); } if (_devSettings == null) { Debug.LogError("[TrenchTileSpawner] Failed to load diving developer settings!"); } _baseMoveSpeed = _settings?.NormalizedMoveSpeed ?? 3f; // Store the original base speed // Calculate tile heights for each prefab CalculateTileHeights(); // Ensure all prefabs have Tile components ValidateTilePrefabs(); if (_devSettings != null && _devSettings.TrenchTileUseObjectPooling) { InitializeObjectPool(); } } // Validate that all prefabs have Tile components private void ValidateTilePrefabs() { for (int i = 0; i < tilePrefabs.Count; i++) { if (tilePrefabs[i] == null) continue; // Check if the prefab has a Tile component if (tilePrefabs[i].GetComponent() == null) { Debug.LogWarning($"Prefab {tilePrefabs[i].name} does not have a Tile component. Adding one automatically."); // Add the Tile component if it doesn't exist tilePrefabs[i].AddComponent(); } } } private void Start() { DivingGameManager.Instance.OnGameInitialized += Initialize; // Register with the DivingGameManager for pause/resume events DivingGameManager.Instance.RegisterPausableComponent(this); // If game is already initialized, initialize immediately if (DivingGameManager.Instance.GetType().GetField("_isGameInitialized", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(DivingGameManager.Instance) is bool isInitialized && isInitialized) { Initialize(); } } private void OnDestroy() { DivingGameManager.Instance.UnregisterPausableComponent(this); } /// /// Pauses the spawner and all associated processes /// public void Pause() { if (_isPaused) return; // Already paused _isPaused = true; // Stop all active coroutines but save their references if (_movementCoroutine != null) { StopCoroutine(_movementCoroutine); _movementCoroutine = null; } if (_tileDestructionCoroutine != null) { StopCoroutine(_tileDestructionCoroutine); _tileDestructionCoroutine = null; } if (_tileSpawningCoroutine != null) { StopCoroutine(_tileSpawningCoroutine); _tileSpawningCoroutine = null; } if (_speedRampingCoroutine != null) { StopCoroutine(_speedRampingCoroutine); _speedRampingCoroutine = null; } Debug.Log("[TrenchTileSpawner] Paused"); } /// /// Resumes the spawner and all associated processes /// public void DoResume() { if (!_isPaused) return; // Already running _isPaused = false; // Restart all necessary coroutines StartMovementCoroutine(); StartTileDestructionCoroutine(); StartTileSpawningCoroutine(); StartSpeedRampingCoroutine(); Debug.Log("[TrenchTileSpawner] Resumed"); } /// /// Initializes the tile spawner when triggered by DivingGameManager /// private void Initialize() { // Calculate screen bounds and normalization factor CalculateScreenBounds(); CalculateScreenNormalizationFactor(); // Spawn initial tiles to fill the screen SpawnInitialTiles(); // Initialize velocity and apply screen normalization _baseMoveSpeed = _settings?.NormalizedMoveSpeed ?? 3f; _currentVelocity = _baseMoveSpeed * Time.fixedDeltaTime * _screenNormalizationFactor; // Start all coroutines StartCoroutine(VelocityCalculationRoutine()); StartMovementCoroutine(); StartTileDestructionCoroutine(); StartTileSpawningCoroutine(); StartSpeedRampingCoroutine(); Debug.Log("[TrenchTileSpawner] Initialized with normalized speed"); } /// /// Gets the current velocity of the tiles /// /// The current upward velocity of the tiles public float GetCurrentVelocity() { return _currentVelocity; } /// /// Sets a custom velocity, overriding the calculated one /// /// The new velocity value public void SetVelocity(float velocity) { _currentVelocity = velocity; } /// /// Coroutine that periodically calculates the velocity based on game state /// private IEnumerator VelocityCalculationRoutine() { while (true) { CalculateVelocity(); yield return new WaitForSeconds(_settings.VelocityCalculationInterval); } } /// /// Calculates the current velocity based on move speed /// private void CalculateVelocity() { _currentVelocity = _baseMoveSpeed * Time.fixedDeltaTime; } /// /// Calculate height values for all tile prefabs /// private void CalculateTileHeights() { foreach (var prefab in tilePrefabs) { if (prefab == null) { Debug.LogError("Null prefab found in tilePrefabs list!"); return; } Renderer renderer = prefab.GetComponentInChildren(); if (renderer != null) { _tileHeights[prefab] = renderer.bounds.size.y; } else { // Fallback in case no renderer is found _tileHeights[prefab] = DefaultTileHeight; Debug.LogWarning($"No renderer found in prefab {prefab.name}. Using default height of {DefaultTileHeight}."); } } } /// /// Initialize the object pool system /// private void InitializeObjectPool() { // Create the tile pool GameObject poolGO = new GameObject("TrenchTilePool"); poolGO.transform.SetParent(transform); _tilePool = poolGO.AddComponent(); // Set up the pool configuration using developer settings _tilePool.maxPerPrefabPoolSize = _devSettings.TrenchTileMaxPerPrefabPoolSize; _tilePool.totalMaxPoolSize = _devSettings.TrenchTileTotalMaxPoolSize; // Convert the GameObject list to a Tile list List prefabTiles = new List(tilePrefabs.Count); foreach (var prefab in tilePrefabs) { if (prefab != null) { Tile tileComponent = prefab.GetComponent(); if (tileComponent != null) { prefabTiles.Add(tileComponent); } else { Debug.LogError($"Prefab {prefab.name} is missing a Tile component!"); } } } // Initialize the pool with the tile component list _tilePool.Initialize(prefabTiles); // Periodically trim the pool to optimize memory usage InvokeRepeating(nameof(TrimExcessPooledTiles), 10f, 30f); } /// /// Spawn the initial set of tiles /// private void SpawnInitialTiles() { // Calculate starting Y position - moved 2 tiles up from the bottom of the screen float startingY = _screenBottom; // If we have prefab tiles with known heights, use their average height for offset calculation float tileHeightEstimate = DefaultTileHeight; if (tilePrefabs != null && tilePrefabs.Count > 0 && tilePrefabs[0] != null) { if (_tileHeights.TryGetValue(tilePrefabs[0], out float height)) { tileHeightEstimate = height; } } // Move starting position up by 2 tile heights startingY += tileHeightEstimate * 2; for (int i = 0; i < _settings.InitialTileCount; i++) { float y = startingY; // Calculate proper Y position based on previous tiles if (i > 0 && _activeTiles.Count > 0) { GameObject prevTile = _activeTiles[_activeTiles.Count - 1]; float prevHeight = GetTileHeight(prevTile); y = prevTile.transform.position.y - prevHeight; } if (i == 0 && initialTilePrefab != null) { SpawnSpecificTileAtY(initialTilePrefab, y); } else { SpawnTileAtY(y); } } } /// /// Calculate the screen bounds in world space /// private void CalculateScreenBounds() { if (_mainCamera == null) { _mainCamera = UnityEngine.Camera.main; if (_mainCamera == null) { Debug.LogError("No main camera found!"); return; } } Vector3 bottom = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 0f, _mainCamera.nearClipPlane)); Vector3 top = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 1f, _mainCamera.nearClipPlane)); _screenBottom = bottom.y; _screenTop = top.y; } /// /// Calculates the screen normalization factor based on current screen height /// private void CalculateScreenNormalizationFactor() { // Get reference height from settings with fallback if not available float referenceHeight = 1080f; // Default fallback value if (_settings != null) { referenceHeight = _settings.ReferenceScreenHeight; } // Calculate normalization factor based on screen height _screenNormalizationFactor = Screen.height / referenceHeight; Debug.Log($"[TrenchTileSpawner] Screen normalization factor: {_screenNormalizationFactor} (Screen height: {Screen.height}, Reference: {referenceHeight})"); } /// /// Called when the velocity factor changes from the DivingGameManager /// public void OnVelocityFactorChanged(float velocityFactor) { _velocityFactor = velocityFactor; // Update the actual move speed based on the velocity factor // This keeps the original move speed intact for game logic _baseMoveSpeed = _settings.NormalizedMoveSpeed * Mathf.Abs(_velocityFactor); // Recalculate velocity immediately CalculateVelocity(); Debug.Log($"[TrenchTileSpawner] Velocity factor updated to {_velocityFactor:F2}, moveSpeed: {_baseMoveSpeed:F2}"); } /// /// Reverses direction to start surfacing /// public void StartSurfacing() { if (_isSurfacing) return; // Already surfacing // Set surfacing flag for spawn/despawn logic _isSurfacing = true; // Reverse the active tiles array to maintain consistent indexing logic _activeTiles.Reverse(); Debug.Log("[TrenchTileSpawner] Started surfacing - reversed array order"); } /// /// Stops spawning new tiles /// public void StopSpawning() { _stopSpawning = true; } /// /// Starts the movement coroutine if it's not already running /// private void StartMovementCoroutine() { if (_movementCoroutine == null && !_isPaused) { _movementCoroutine = StartCoroutine(MoveActiveTilesRoutine()); } } /// /// Coroutine that handles obstacle movement using normalized screen-relative speed /// private IEnumerator MoveActiveTilesRoutine() { Debug.Log($"[TrenchTileSpawner] Started movement coroutine with normalized speed: {_baseMoveSpeed:F3}"); while (enabled && gameObject.activeInHierarchy && !_isPaused) { // Skip if no active tiles if (_activeTiles.Count == 0) { yield return null; continue; } // Use velocity factor sign to determine direction Vector3 direction = Vector3.up * Mathf.Sign(_velocityFactor); // Apply normalized movement using the shared utility method float speed = AppleHillsUtils.CalculateNormalizedMovementSpeed(_baseMoveSpeed); // Move all active tiles foreach (var tile in _activeTiles) { if (tile != null) { // Apply movement in correct direction tile.transform.position += direction * speed; } } // Wait for next frame yield return null; } // Clear coroutine reference when stopped _movementCoroutine = null; } /// /// Coroutine that checks for tiles to destroy periodically /// private IEnumerator TileDestructionRoutine() { const float checkInterval = 0.5f; // Check every half second Debug.Log($"[TrenchTileSpawner] Started tile destruction coroutine with interval: {checkInterval}s"); while (enabled && gameObject.activeInHierarchy && !_isPaused) { // Check and handle tile destruction if (_activeTiles.Count > 0) { GameObject topTile = _activeTiles[0]; if (topTile == null) { _activeTiles.RemoveAt(0); } else { float tileHeight = GetTileHeight(topTile); bool shouldDestroy; if (_isSurfacing) { // When surfacing, destroy tiles at the bottom shouldDestroy = topTile.transform.position.y + tileHeight / 2 < _screenBottom - _settings.TileSpawnBuffer; } else { // When descending, destroy tiles at the top shouldDestroy = topTile.transform.position.y - tileHeight / 2 > _screenTop + _settings.TileSpawnBuffer; } if (shouldDestroy) { _activeTiles.RemoveAt(0); onTileDestroyed?.Invoke(topTile); if (_devSettings != null && _devSettings.TrenchTileUseObjectPooling && _tilePool != null) { // Find the prefab index for this tile int prefabIndex = GetPrefabIndex(topTile); if (prefabIndex >= 0) { _tilePool.ReturnTile(topTile, prefabIndex); } else { Destroy(topTile); } } else { Destroy(topTile); } } } } // Wait for the next check interval yield return new WaitForSeconds(checkInterval); } // Clear coroutine reference when stopped _tileDestructionCoroutine = null; } /// /// Coroutine that checks if new tiles need to be spawned periodically /// private IEnumerator TileSpawningRoutine() { const float checkInterval = 0.2f; // Check every fifth of a second Debug.Log($"[TrenchTileSpawner] Started tile spawning coroutine with interval: {checkInterval}s"); while (enabled && gameObject.activeInHierarchy && !_isPaused && !_stopSpawning) { // Check if we need to spawn new tiles if (_activeTiles.Count == 0) { // If we have no active tiles and spawning is stopped, trigger the event if (_stopSpawning) { onLastTileLeft.Invoke(); } } else { GameObject bottomTile = _activeTiles[^1]; if (bottomTile == null) { _activeTiles.RemoveAt(_activeTiles.Count - 1); } else { // Get the tile height once to use in all calculations float tileHeight = GetTileHeight(bottomTile); // If we're in stop spawning mode, check if last tile is leaving if (_stopSpawning) { // Check if this is the last tile, and if it's about to leave the screen if (_activeTiles.Count == 1) { bool isLastTileLeaving; if (_isSurfacing) { // When surfacing, check if bottom of tile is above top of screen isLastTileLeaving = bottomTile.transform.position.y - tileHeight / 2 > _screenTop + _settings.TileSpawnBuffer; } else { // When descending, check if top of tile is below bottom of screen isLastTileLeaving = bottomTile.transform.position.y + tileHeight / 2 < _screenBottom - _settings.TileSpawnBuffer; } if (isLastTileLeaving) { onLastTileLeft.Invoke(); } } } else { // Normal spawning mode bool shouldSpawn; float newY; if (_isSurfacing) { // When surfacing, spawn new tiles at the top float topEdge = bottomTile.transform.position.y + tileHeight / 2; shouldSpawn = topEdge < _screenTop + _settings.TileSpawnBuffer; newY = bottomTile.transform.position.y + tileHeight; } else { // When descending, spawn new tiles at the bottom float bottomEdge = bottomTile.transform.position.y - tileHeight / 2; shouldSpawn = bottomEdge > _screenBottom - _settings.TileSpawnBuffer; newY = bottomTile.transform.position.y - tileHeight; } if (shouldSpawn) { SpawnTileAtY(newY); } } } } // Wait for the next check interval yield return new WaitForSeconds(checkInterval); } // Clear coroutine reference when stopped _tileSpawningCoroutine = null; } /// /// Coroutine that handles increasing the movement speed over time /// private IEnumerator SpeedRampingRoutine() { const float checkInterval = 1.0f; // Check once per second Debug.Log($"[TrenchTileSpawner] Started speed ramping coroutine with interval: {checkInterval}s"); while (enabled && gameObject.activeInHierarchy && !_isPaused) { // Increase the base move speed up to the maximum _baseMoveSpeed = Mathf.Min(_baseMoveSpeed + _settings.SpeedUpFactor, _settings.MaxNormalizedMoveSpeed); // Recalculate velocity with the new base speed CalculateVelocity(); // Wait for the next check interval yield return new WaitForSeconds(checkInterval); } // Clear coroutine reference when stopped _speedRampingCoroutine = null; } /// /// Checks if two tiles are fully compatible for spawning, following clarified floating area rules. /// private bool AreTilesFullyCompatible(Tile prev, Tile next) { // Path compatibility: at least one path is open in both Out (prev) and In (next) bool pathCompatible = (prev.pathOutLeft && next.pathInLeft) || (prev.pathOutCenter && next.pathInCenter) || (prev.pathOutRight && next.pathInRight); // --- Updated floating area rules --- // Continue tile: can only spawn after Start, and must be followed by End if (next.continuesFloatingAreaMiddle) { if (!prev.hasFloatingAreaMiddle) return false; } // End tile: can only spawn after Start or Continue if (next.endsFloatingAreaMiddle) { if (!(prev.hasFloatingAreaMiddle || prev.continuesFloatingAreaMiddle)) return false; } // After a Start tile, only Continue or End can follow if (prev.hasFloatingAreaMiddle) { if (!(next.continuesFloatingAreaMiddle || next.endsFloatingAreaMiddle)) return false; } // After a Continue tile, only End can follow if (prev.continuesFloatingAreaMiddle) { if (!next.endsFloatingAreaMiddle) return false; } // Otherwise, normal tiles are always allowed return pathCompatible; } /// /// Gets a weighted random index among fully compatible tiles, prioritizing least recently used. /// private int GetWeightedCompatibleTileIndex(Tile prevTile) { var compatibleIndices = new List(); int currentDifficulty = CurrentDifficulty; for (int i = 0; i < tilePrefabs.Count; i++) { var candidateGO = tilePrefabs[i]; var candidate = candidateGO?.GetComponent(); if (candidate == null) continue; if (candidate.difficultyLevel > currentDifficulty) continue; bool compatible = AreTilesFullyCompatible(prevTile, candidate); if (compatible) compatibleIndices.Add(i); } if (compatibleIndices.Count == 0) { Debug.LogError("No compatible tile found for previous tile and current difficulty. Spawning aborted."); return -1; } List weights = new List(compatibleIndices.Count); foreach (var i in compatibleIndices) { int lastUsed = _tileLastUsed.TryGetValue(i, out var value) ? value : -tilePrefabs.Count; int age = _spawnCounter - lastUsed; float weight = Mathf.Clamp(age, 1, tilePrefabs.Count * 2); weights.Add(weight); } float totalWeight = 0f; foreach (var weight in weights) totalWeight += weight; float randomValue = Random.value * totalWeight; for (int idx = 0; idx < compatibleIndices.Count; idx++) { if (randomValue < weights[idx]) return compatibleIndices[idx]; randomValue -= weights[idx]; } return compatibleIndices[Random.Range(0, compatibleIndices.Count)]; } /// /// Spawn a new tile at the specified Y position /// /// The Y position to spawn at private void SpawnTileAtY(float y) { if (tilePrefabs == null || tilePrefabs.Count == 0) { Debug.LogError("No tile prefabs available for spawning!"); return; } int prefabIndex; // First tile: any tile if (_activeTiles.Count == 0) { prefabIndex = GetWeightedRandomTileIndex(); } else { GameObject prevTileGO = _activeTiles[_activeTiles.Count - 1]; Tile prevTile = prevTileGO != null ? prevTileGO.GetComponent() : null; if (prevTile != null) { // Use weighted compatible selection prefabIndex = GetWeightedCompatibleTileIndex(prevTile); if (prefabIndex == -1) { Debug.LogError("No compatible tile can be spawned after previous tile. Aborting spawn."); return; } } else { prefabIndex = GetWeightedRandomTileIndex(); } } GameObject prefab = tilePrefabs[prefabIndex]; if (prefab == null) { Debug.LogError($"Tile prefab at index {prefabIndex} is null!"); return; } GameObject tile; if (_devSettings != null && _devSettings.TrenchTileUseObjectPooling && _tilePool != null) { tile = _tilePool.GetTile(prefabIndex); if (tile == null) { Debug.LogError("Failed to get tile from pool!"); return; } tile.transform.position = new Vector3(0f, y, TileSpawnZ); tile.transform.rotation = prefab.transform.rotation; tile.transform.SetParent(transform); if (_devSettings != null) { tile.layer = _devSettings.TrenchTileLayer; SetLayerRecursively(tile, _devSettings.TrenchTileLayer); } } else { tile = Instantiate(prefab, new Vector3(0f, y, TileSpawnZ), prefab.transform.rotation, transform); if (_devSettings != null) { tile.layer = _devSettings.TrenchTileLayer; SetLayerRecursively(tile, _devSettings.TrenchTileLayer); } } _activeTiles.Add(tile); _tileLastUsed[prefabIndex] = _spawnCounter++; _currentDepth++; Debug.Log($"[TrenchTileSpawner] Current Depth: {_currentDepth}"); onTileSpawned?.Invoke(tile); // --- FLOATING AREA STATE MANAGEMENT --- Tile spawnedTile = tile.GetComponent(); if (spawnedTile != null) { if (spawnedTile.hasFloatingAreaMiddle || spawnedTile.continuesFloatingAreaMiddle) { isFloatingAreaActive = true; } if (spawnedTile.endsFloatingAreaMiddle) { isFloatingAreaActive = false; } } } /// /// Spawn a specific tile at the specified Y position /// /// The tile prefab to spawn /// The Y position to spawn at private void SpawnSpecificTileAtY(GameObject prefab, float y) { GameObject tile; if (_devSettings != null && _devSettings.TrenchTileUseObjectPooling && _tilePool != null) { int prefabIndex = tilePrefabs.IndexOf(prefab); if (prefabIndex >= 0) { tile = _tilePool.GetTile(prefabIndex); if (tile == null) { Debug.LogError("Failed to get initial tile from pool!"); return; } } else { tile = Instantiate(prefab, new Vector3(0f, y, TileSpawnZ), prefab.transform.rotation, transform); } tile.transform.position = new Vector3(0f, y, TileSpawnZ); tile.transform.rotation = prefab.transform.rotation; tile.transform.SetParent(transform); if (_devSettings != null) { tile.layer = _devSettings.TrenchTileLayer; SetLayerRecursively(tile, _devSettings.TrenchTileLayer); } } else { tile = Instantiate(prefab, new Vector3(0f, y, TileSpawnZ), prefab.transform.rotation, transform); if (_devSettings != null) { tile.layer = _devSettings.TrenchTileLayer; SetLayerRecursively(tile, _devSettings.TrenchTileLayer); } } _activeTiles.Add(tile); _currentDepth++; Debug.Log($"[TrenchTileSpawner] Current Depth: {_currentDepth}"); onTileSpawned?.Invoke(tile); // Optionally update floating area state if needed Tile spawnedTile = tile.GetComponent(); if (spawnedTile != null) { if (spawnedTile.hasFloatingAreaMiddle || spawnedTile.continuesFloatingAreaMiddle) { isFloatingAreaActive = true; } if (spawnedTile.endsFloatingAreaMiddle) { isFloatingAreaActive = false; } } } /// /// Gets a list of allowed tile indices for the current difficulty /// /// List of allowed prefab indices private List GetAllowedTileIndicesForCurrentDifficulty() { var allowedIndices = new List(); int currentDifficulty = CurrentDifficulty; for (int i = 0; i < tilePrefabs.Count; i++) { var tileComponent = tilePrefabs[i]?.GetComponent(); if (tileComponent != null && tileComponent.difficultyLevel <= currentDifficulty) { allowedIndices.Add(i); } } return allowedIndices; } /// /// Gets a weighted random tile index, favoring tiles that haven't been used recently /// /// The selected prefab index private int GetWeightedRandomTileIndex() { var allowedIndices = GetAllowedTileIndicesForCurrentDifficulty(); if (allowedIndices.Count == 0) { Debug.LogError("No allowed tiles for current difficulty!"); return 0; } List weights = new List(allowedIndices.Count); for (int i = 0; i < allowedIndices.Count; i++) { int lastUsed = _tileLastUsed.TryGetValue(allowedIndices[i], out var value) ? value : -tilePrefabs.Count; int age = _spawnCounter - lastUsed; float weight = Mathf.Clamp(age, 1, tilePrefabs.Count * 2); // More unused = higher weight weights.Add(weight); } float totalWeight = 0f; foreach (var weight in weights) { totalWeight += weight; } float randomValue = Random.value * totalWeight; for (int i = 0; i < allowedIndices.Count; i++) { if (randomValue < weights[i]) { return allowedIndices[i]; } randomValue -= weights[i]; } return Random.Range(0, allowedIndices.Count); // fallback } /// /// Gets the height of a tile based on its prefab or renderer bounds /// /// The tile to measure /// The height of the tile private float GetTileHeight(GameObject tile) { if (tile == null) { Debug.LogWarning("Attempted to get height of null tile!"); return DefaultTileHeight; } // Check if this is a known prefab foreach (var prefab in tilePrefabs) { if (prefab == null) continue; // Check if this tile was created from this prefab if (tile.name.StartsWith(prefab.name)) { if (_tileHeights.TryGetValue(prefab, out float height)) { return height; } } } // If not found, calculate it from the renderer Renderer renderer = tile.GetComponentInChildren(); if (renderer != null) { return renderer.bounds.size.y; } // Fallback return DefaultTileHeight; } /// /// Gets the index of the prefab that was used to create this tile /// /// The tile to check /// The index of the prefab or -1 if not found private int GetPrefabIndex(GameObject tile) { if (tile == null || tilePrefabs == null) { return -1; } for (int i = 0; i < tilePrefabs.Count; i++) { if (tilePrefabs[i] == null) continue; if (tile.name.StartsWith(tilePrefabs[i].name)) { return i; } } return -1; } /// /// Called periodically to trim excess pooled tiles /// private void TrimExcessPooledTiles() { if (_tilePool != null) { _tilePool.TrimExcess(); } } #if UNITY_EDITOR private void OnDrawGizmosSelected() { if (!Application.isPlaying) { // Only try to calculate this if _screenBottom hasn't been set by the game UnityEngine.Camera editorCam = UnityEngine.Camera.main; if (editorCam != null) { Vector3 bottom = editorCam.ViewportToWorldPoint(new Vector3(0.5f, 0f, editorCam.nearClipPlane)); _screenBottom = bottom.y; } } // Draw tile bounds for debugging Gizmos.color = Color.cyan; if (_settings != null) { for (int i = 0; i < _settings.InitialTileCount; i++) { float height = DefaultTileHeight; if (tilePrefabs != null && tilePrefabs.Count > 0 && tilePrefabs[0] != null && _tileHeights.TryGetValue(tilePrefabs[0], out float h)) { height = h; } Vector3 center = new Vector3(0f, _screenBottom + i * height, 0f); Gizmos.DrawWireCube(center, new Vector3(10f, height, 1f)); } } } #endif /// /// Set the layer of a GameObject and all its children recursively /// /// The GameObject to set the layer for /// The layer index to set private void SetLayerRecursively(GameObject obj, int layer) { if (obj == null) return; obj.layer = layer; foreach (Transform child in obj.transform) { if (child != null) { SetLayerRecursively(child.gameObject, layer); } } } /// /// Starts the tile destruction coroutine and stores its reference /// private void StartTileDestructionCoroutine() { if (_tileDestructionCoroutine != null) { StopCoroutine(_tileDestructionCoroutine); } _tileDestructionCoroutine = StartCoroutine(TileDestructionCoroutine()); } /// /// Coroutine that checks for tiles to destroy periodically /// private IEnumerator TileDestructionCoroutine() { const float checkInterval = 0.5f; // Check every half second as requested Debug.Log($"[TrenchTileSpawner] Started tile destruction coroutine with interval: {checkInterval}s"); while (enabled && gameObject.activeInHierarchy) { // Check and handle tile destruction if (_activeTiles.Count > 0) { GameObject topTile = _activeTiles[0]; if (topTile == null) { _activeTiles.RemoveAt(0); } else { float tileHeight = GetTileHeight(topTile); bool shouldDestroy; if (_isSurfacing) { // When surfacing, destroy tiles at the bottom shouldDestroy = topTile.transform.position.y + tileHeight / 2 < _screenBottom - _settings.TileSpawnBuffer; } else { // When descending, destroy tiles at the top shouldDestroy = topTile.transform.position.y - tileHeight / 2 > _screenTop + _settings.TileSpawnBuffer; } if (shouldDestroy) { _activeTiles.RemoveAt(0); onTileDestroyed?.Invoke(topTile); if (_devSettings != null && _devSettings.TrenchTileUseObjectPooling && _tilePool != null) { // Find the prefab index for this tile int prefabIndex = GetPrefabIndex(topTile); if (prefabIndex >= 0) { _tilePool.ReturnTile(topTile, prefabIndex); } else { Destroy(topTile); } } else { Destroy(topTile); } } } } // Wait for the next check interval yield return new WaitForSeconds(checkInterval); } } /// /// Starts the tile spawning coroutine and stores its reference /// private void StartTileSpawningCoroutine() { if (_tileSpawningCoroutine != null) { StopCoroutine(_tileSpawningCoroutine); } _tileSpawningCoroutine = StartCoroutine(TileSpawningCoroutine()); } /// /// Coroutine that checks if new tiles need to be spawned periodically /// private IEnumerator TileSpawningCoroutine() { const float checkInterval = 0.2f; // Check every half second as requested Debug.Log($"[TrenchTileSpawner] Started tile spawning coroutine with interval: {checkInterval}s"); while (enabled && gameObject.activeInHierarchy) { // Check if we need to spawn new tiles if (_activeTiles.Count == 0) { // If we have no active tiles and spawning is stopped, trigger the event if (_stopSpawning) { onLastTileLeft.Invoke(); } } else { GameObject bottomTile = _activeTiles[^1]; if (bottomTile == null) { _activeTiles.RemoveAt(_activeTiles.Count - 1); } else { // Get the tile height once to use in all calculations float tileHeight = GetTileHeight(bottomTile); // If we're in stop spawning mode, check if last tile is leaving if (_stopSpawning) { // Check if this is the last tile, and if it's about to leave the screen if (_activeTiles.Count == 1) { bool isLastTileLeaving; if (_isSurfacing) { // When surfacing, check if bottom of tile is above top of screen isLastTileLeaving = bottomTile.transform.position.y - tileHeight / 2 > _screenTop + _settings.TileSpawnBuffer; } else { // When descending, check if top of tile is below bottom of screen isLastTileLeaving = bottomTile.transform.position.y + tileHeight / 2 < _screenBottom - _settings.TileSpawnBuffer; } if (isLastTileLeaving) { onLastTileLeft.Invoke(); } } } else { // Normal spawning mode bool shouldSpawn; float newY; if (_isSurfacing) { // When surfacing, spawn new tiles at the top float topEdge = bottomTile.transform.position.y + tileHeight / 2; shouldSpawn = topEdge < _screenTop + _settings.TileSpawnBuffer; newY = bottomTile.transform.position.y + tileHeight; } else { // When descending, spawn new tiles at the bottom float bottomEdge = bottomTile.transform.position.y - tileHeight / 2; shouldSpawn = bottomEdge > _screenBottom - _settings.TileSpawnBuffer; newY = bottomTile.transform.position.y - tileHeight; } if (shouldSpawn) { SpawnTileAtY(newY); } } } } // Wait for the next check interval yield return new WaitForSeconds(checkInterval); } } /// /// Starts the speed ramping coroutine and stores its reference /// private void StartSpeedRampingCoroutine() { if (_speedRampingCoroutine != null) { StopCoroutine(_speedRampingCoroutine); } _speedRampingCoroutine = StartCoroutine(SpeedRampingRoutine()); } } }