using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using Core.Domain; using UnityEngine; using Random = UnityEngine.Random; namespace Infrastructure.Unity { public class LevelGenerator : MonoBehaviour { [Header("Prefabs")] [SerializeField] private TileViewAdapter tilePrefab; [SerializeField] private ParticleSystem tileBreakVfxPrefab; [SerializeField] private JumpPadAdapter jumpPadPrefab; [SerializeField] private TeleporterAdapter teleporterPrefab; [Header("Settings")] [SerializeField] private int gridSizeX = 10; [SerializeField] private int gridSizeY = 10; [SerializeField] private int floorsCount = 3; [SerializeField] private float floorHeightDistance = 15f; [SerializeField] private float decayTime = 0.5f; [SerializeField] private float fallingTime = 2.0f; [SerializeField] private float minimumDistanceBetweenTeleporters = 3f; public float FloorHeightDistance => floorHeightDistance; public int FloorsCount => floorsCount; public int GridSizeX => gridSizeX; public int GridSizeY => gridSizeY; private TilePool _tilePool; public IEnumerator GenerateAsync(SoundManager soundManager, List allTiles, Dictionary tileViews, CameraController camera, RumbleManager rumble, Action onComplete) { _tilePool = new TilePool(tilePrefab, transform); var stopwatch = new Stopwatch(); stopwatch.Start(); yield return GenerateFloorAsync(0, MapPatterns.GenerateSquare(gridSizeX, gridSizeY), soundManager, allTiles, tileViews, camera, rumble, stopwatch); yield return GenerateFloorAsync(1, MapPatterns.GenerateDonut(gridSizeX, Mathf.FloorToInt(gridSizeX / 3f)), soundManager, allTiles, tileViews, camera, rumble, stopwatch); yield return GenerateFloorAsync(2, MapPatterns.GenerateCircle(gridSizeX), soundManager, allTiles, tileViews, camera, rumble, stopwatch); stopwatch?.Stop(); onComplete?.Invoke(); } private IEnumerator GenerateFloorAsync(int floorIndex, List coordinates, SoundManager soundManager, List allTiles, Dictionary tileViews, CameraController camera, RumbleManager rumble, Stopwatch stopwatch) { var yOffset = -(floorIndex * floorHeightDistance); var xOffset = gridSizeX / 2f; var zOffset = gridSizeY / 2f; const long frameBudgetMs = 5; foreach (var coord in coordinates) { var pos = new Vector3(coord.x - xOffset, yOffset, coord.y - zOffset); CreateTile(pos, $"{floorIndex}_{coord.x}_{coord.y}", floorIndex, soundManager, allTiles, tileViews, camera, rumble); if (stopwatch.ElapsedMilliseconds > frameBudgetMs) { stopwatch.Reset(); stopwatch.Start(); yield return null; // Wait for next frame } } if (floorIndex > 0 && jumpPadPrefab) { var validSpot = coordinates[Random.Range(0, coordinates.Count)]; var padPos = new Vector3(validSpot.x - xOffset, yOffset + 0.6f, validSpot.y - zOffset); Instantiate(jumpPadPrefab, padPos, Quaternion.identity, transform); } if (floorIndex > 0 && teleporterPrefab && coordinates.Count > 5) { // Spawn a pair of teleporters var indexA = Random.Range(0, coordinates.Count); int indexB; do { indexB = Random.Range(0, coordinates.Count); } while (indexB == indexA); var spotA = coordinates[indexA]; var spotB = coordinates[indexB]; var posA = new Vector3(spotA.x - xOffset, yOffset + 0.6f, spotA.y - zOffset); var posB = new Vector3(spotB.x - xOffset, yOffset + 0.6f, spotB.y - zOffset); // check if positions are not too close if (Vector3.Distance(posA, posB) < minimumDistanceBetweenTeleporters) { // find a new position for teleporter B for (var i = 0; i < coordinates.Count; i++) { if (i == indexA) continue; var newSpotB = coordinates[i]; var newPosB = new Vector3(newSpotB.x - xOffset, yOffset + 0.6f, newSpotB.y - zOffset); if (Vector3.Distance(posA, newPosB) >= minimumDistanceBetweenTeleporters) { posB = newPosB; break; } } } var telA = Instantiate(teleporterPrefab, posA, Quaternion.identity, transform); var telB = Instantiate(teleporterPrefab, posB, Quaternion.identity, transform); telA.Initialize(telB.transform); telB.Initialize(telA.transform); System.Action onTeleport = () => { camera?.Shake(0.2f, 0.2f); rumble?.PulseMedium(); }; telA.OnTeleport += onTeleport; telB.OnTeleport += onTeleport; } } private void CreateTile(Vector3 position, string id, int floorIndex, SoundManager soundManager, List allTiles, Dictionary tileViews, CameraController camera, RumbleManager rumble) { var go = _tilePool.Get(); go.transform.position = position; go.transform.rotation = Quaternion.identity; go.transform.localScale = Vector3.one * 0.95f; // 15% chance to be Fragile var isFragile = Random.value < 0.15f; var type = isFragile ? TileType.Fragile : TileType.Normal; var tileLogic = new Tile(id, floorIndex, decayTime, fallingTime, type); go.Initialize(tileLogic, (t) => _tilePool.Return(t)); tileLogic.OnStateChanged += (t) => { if (t.CurrentState == TileState.Warning) { soundManager?.PlayTileWarning(position); } else if (t.CurrentState == TileState.Falling) { soundManager?.PlayTileBreak(position); if (t.Type == TileType.Fragile) { camera?.Shake(0.1f, 0.05f); rumble?.PulseLight(); } if (tileBreakVfxPrefab) { var vfx = Instantiate(tileBreakVfxPrefab, position, Quaternion.identity); var main = vfx.main; var currentColor = go.MeshRenderer.material.color; main.startColor = currentColor; Destroy(vfx.gameObject, 2f); } } }; allTiles.Add(tileLogic); tileViews.Add(id, go); } } }