using System.Collections; using System.Collections.Generic; using UnityEngine; namespace Hallucinate.GameSetup.Maze { /// /// Responsible for the visual representation of the maze. /// Handles spawning, pooling, and animations with safety checks. /// // public class MazeRenderer : MonoBehaviour // { // [SerializeField] private MazeVisualProfile visualProfile; // // private readonly Dictionary _spawnedCells = new Dictionary(); // private Transform _container; // // public void Initialize(MazeGrid grid, Transform container) // { // _container = container; // grid.OnCellChanged += HandleCellChanged; // // // Initial render // for (int z = 0; z < grid.Depth; z++) // { // for (int x = 0; x < grid.Width; x++) // { // UpdateCellVisual(x, z, grid.GetCell(x, z), false); // } // } // } // // public void Clear() // { // StopAllCoroutines(); // // foreach (var cell in _spawnedCells.Values) // { // if (cell != null) Destroy(cell); // } // _spawnedCells.Clear(); // } // // private void HandleCellChanged(int x, int z, MazeCellType type) // { // UpdateCellVisual(x, z, type, true); // } // // private void UpdateCellVisual(int x, int z, MazeCellType type, bool animate) // { // Vector2Int pos = new Vector2Int(x, z); // // if (_spawnedCells.TryGetValue(pos, out GameObject oldObj)) // { // Destroy(oldObj); // _spawnedCells.Remove(pos); // } // // GameObject prefab = visualProfile.GetPrefab(type); // if (prefab == null) return; // // // Ensure scale is always positive to avoid BoxCollider issues // float safeScale = Mathf.Max(0.001f, visualProfile.scale); // Vector3 worldPos = new Vector3(x * safeScale, 0, z * safeScale); // // GameObject newObj = Instantiate(prefab, worldPos, Quaternion.identity, _container); // newObj.transform.localScale = Vector3.one * safeScale; // _spawnedCells[pos] = newObj; // // if (animate && visualProfile.animationDuration > 0) // { // StartCoroutine(AnimateCell(newObj.transform)); // } // } // // private IEnumerator AnimateCell(Transform target) // { // if (target == null) yield break; // // float duration = Mathf.Max(0.01f, visualProfile.animationDuration); // float elapsed = 0; // Vector3 finalScale = target.localScale; // target.localScale = Vector3.one * 0.001f; // Use tiny positive instead of zero // // while (elapsed < duration) // { // if (target == null) yield break; // // elapsed += Time.deltaTime; // float t = Mathf.Clamp01(elapsed / duration); // float s = Mathf.Sin(t * Mathf.PI * 0.5f); // // // Ensure s is never negative // target.localScale = finalScale * Mathf.Max(0.001f, s); // yield return null; // } // // if (target != null) // { // target.localScale = finalScale; // } // } // } public class MazeRenderer : MonoBehaviour { [SerializeField] private MazeVisualProfile visualProfile; private readonly Dictionary _spawnedCells = new Dictionary(); private Transform _container; private MazeGrid _currentGrid; public void Initialize(MazeGrid grid, Transform container) { _currentGrid = grid; _container = container; grid.OnCellChanged += HandleCellChanged; // Initial render for (int z = 0; z < grid.Depth; z++) { for (int x = 0; x < grid.Width; x++) { UpdateCellVisual(x, z, grid.GetCell(x, z), false); } } } public void Clear() { StopAllCoroutines(); foreach (var cell in _spawnedCells.Values) { if (cell != null) Destroy(cell); } _spawnedCells.Clear(); _currentGrid = null; } private void HandleCellChanged(int x, int z, MazeCellType type) { UpdateCellVisual(x, z, type, true); UpdateNeighborVisual(x + 1, z); UpdateNeighborVisual(x - 1, z); UpdateNeighborVisual(x, z + 1); UpdateNeighborVisual(x, z - 1); } private void UpdateNeighborVisual(int x, int z) { if (_currentGrid != null && _currentGrid.IsInBounds(x, z)) { if (IsPath(x, z)) { MazeCellType type = _currentGrid.GetCell(x, z); UpdateCellVisual(x, z, type, false); } } } private void UpdateCellVisual(int x, int z, MazeCellType type, bool animate) { Vector2Int pos = new Vector2Int(x, z); if (_spawnedCells.TryGetValue(pos, out GameObject oldObj)) { Destroy(oldObj); _spawnedCells.Remove(pos); } GameObject prefab = null; Quaternion rotation = Quaternion.identity; if (type == MazeCellType.Corridor || type == MazeCellType.Processing) { (prefab, rotation) = GetCorridorPrefabAndRotation(x, z); } else { prefab = visualProfile.GetPrefab(type); } if (prefab == null) return; float safeScale = Mathf.Max(0.001f, visualProfile.scale); float modelScaleMultiplier = 0.25f; Vector3 localPos = new Vector3(x * safeScale, 0, z * safeScale); // GameObject newObj = Instantiate(prefab, worldPos, rotation, _container); GameObject newObj = Instantiate(prefab, _container); newObj.transform.localPosition = localPos; newObj.transform.localRotation = rotation; newObj.transform.localScale = Vector3.one * safeScale * modelScaleMultiplier; _spawnedCells[pos] = newObj; if (animate && visualProfile.animationDuration > 0) { StartCoroutine(AnimateCell(newObj.transform)); } } // ================================================================================= // THUẬT TOÁN BITMASK AUTO-TILING // ================================================================================= private (GameObject, Quaternion) GetCorridorPrefabAndRotation(int x, int z) { bool top = IsPath(x, z + 1); bool right = IsPath(x + 1, z); bool bottom = IsPath(x, z - 1); bool left = IsPath(x - 1, z); int mask = 0; if (top) mask += 1; if (right) mask += 2; if (bottom) mask += 4; if (left) mask += 8; GameObject prefabToSpawn = null; float yRotation = 0f; switch (mask) { case 1: prefabToSpawn = visualProfile.corridorDeadEnd; yRotation = 180f; break; case 2: prefabToSpawn = visualProfile.corridorDeadEnd; yRotation = 270f; break; case 4: prefabToSpawn = visualProfile.corridorDeadEnd; yRotation = 0f; break; case 8: prefabToSpawn = visualProfile.corridorDeadEnd; yRotation = 90f; break; case 5: prefabToSpawn = visualProfile.corridorStraight; yRotation = 0f; break; case 10: prefabToSpawn = visualProfile.corridorStraight; yRotation = 90f; break; case 3: prefabToSpawn = visualProfile.corridorCorner; yRotation = 0f; break; case 6: prefabToSpawn = visualProfile.corridorCorner; yRotation = 90f; break; case 12: prefabToSpawn = visualProfile.corridorCorner; yRotation = 180f; break; case 9: prefabToSpawn = visualProfile.corridorCorner; yRotation = 270f; break; case 11: prefabToSpawn = visualProfile.corridorTJunction; yRotation = 180f; break; case 7: prefabToSpawn = visualProfile.corridorTJunction; yRotation = 270f; break; case 14: prefabToSpawn = visualProfile.corridorTJunction; yRotation = 0f; break; case 13: prefabToSpawn = visualProfile.corridorTJunction; yRotation = 90f; break; case 15: prefabToSpawn = visualProfile.corridorCross; yRotation = 0f; break; default: prefabToSpawn = visualProfile.corridorDeadEnd; yRotation = 0f; break; } // --- CỘNG THÊM OFFSET (Đã xóa bỏ phần code thừa bị lặp lại) --- float finalRotation = yRotation; if (prefabToSpawn == visualProfile.corridorTJunction) finalRotation += visualProfile.tJunctionOffset; if (prefabToSpawn == visualProfile.corridorDeadEnd) finalRotation += visualProfile.deadEndOffset; if (prefabToSpawn == visualProfile.corridorCorner) finalRotation += visualProfile.cornerOffset; if (prefabToSpawn == null) prefabToSpawn = visualProfile.corridorPrefab; return (prefabToSpawn, Quaternion.Euler(0, finalRotation, 0)); } private bool IsPath(int x, int z) { if (_currentGrid == null || !_currentGrid.IsInBounds(x, z)) return false; MazeCellType type = _currentGrid.GetCell(x, z); return type == MazeCellType.Corridor || type == MazeCellType.Processing || type == MazeCellType.Start || type == MazeCellType.End || type == MazeCellType.Path; } // ================================================================================= // ANIMATION // ================================================================================= private IEnumerator AnimateCell(Transform target) { if (target == null) yield break; float duration = Mathf.Max(0.01f, visualProfile.animationDuration); float elapsed = 0; Vector3 finalScale = target.localScale; target.localScale = Vector3.one * 0.001f; while (elapsed < duration) { if (target == null) yield break; elapsed += Time.deltaTime; float t = Mathf.Clamp01(elapsed / duration); float s = Mathf.Sin(t * Mathf.PI * 0.5f); target.localScale = finalScale * Mathf.Max(0.001f, s); yield return null; } if (target != null) { target.localScale = finalScale; } } } }