using UnityEngine; using System.Collections.Generic; using System.Linq; /// /// Spawns monsters into the maze based on MazeConfig settings. /// /// Spawning rules /// ───────────── /// • Monster rooms (RoomType.Boss + Normal rooms chosen by MonsterAreaDensity %) /// → 2–4 monsters, difficulty multiplier scales with room distance from start. /// /// • Hallway tiles (walkable tiles not inside any room) /// → sparse single monsters; much lower density, difficulty ≤ 1. /// /// Monster prefab is a red sphere identical in size to a solo agent. /// /// Exposes a static room-threat lookup so AIAgents can query danger before entering. /// public class MonsterSpawner : MonoBehaviour { // ------------------------------------------------------------------ // // Inspector // // ------------------------------------------------------------------ // [Header("Prefab (auto-created if null)")] [SerializeField] private GameObject monsterPrefab; [Header("Room spawn")] [Tooltip("Min monsters in a monster room")] [SerializeField] private int minMonstersPerRoom = 2; [Tooltip("Max monsters in a monster room")] [SerializeField] private int maxMonstersPerRoom = 4; [Tooltip("Extra monsters added per Boss room")] [SerializeField] private int bossRoomBonusMonsters = 3; [Tooltip("Difficulty multiplier range for deepest rooms (1 = weakest, 2 = strongest)")] [SerializeField] private float maxDifficultyMultiplier = 2f; [Header("Hallway spawn")] [Tooltip("Probability that any given hallway tile spawns a monster (0–1)")] [SerializeField][Range(0f, 0.05f)] private float hallwaySpawnChance = 0.005f; [Tooltip("Difficulty multiplier cap for hallway monsters")] [SerializeField] private float hallwayMaxDifficulty = 1f; // ------------------------------------------------------------------ // // Runtime // // ------------------------------------------------------------------ // private MazeData maze; private MazeConfig config; /// /// Room ID → list of living monsters. Agents can query this to assess danger. /// Hallway monsters are stored under key -1. /// private static readonly Dictionary> roomMonsters = new(); // ------------------------------------------------------------------ // // Public entry point – called by MazeController after generation // // ------------------------------------------------------------------ // /// /// Called by MazeController once the maze has been fully generated. /// Destroys any previously spawned monsters and re-spawns for the new maze. /// public void SpawnForMaze(MazeData mazeData, MazeConfig mazeConfig) { // Destroy any monsters left over from a previous maze foreach (var m in FindObjectsByType()) Destroy(m.gameObject); maze = mazeData; config = mazeConfig; if (maze == null) { Debug.LogError("MonsterSpawner: Maze is null!"); return; } if (monsterPrefab == null) monsterPrefab = CreateDefaultMonsterPrefab(); roomMonsters.Clear(); SpawnRoomMonsters(); SpawnHallwayMonsters(); Debug.Log("[MonsterSpawner] Spawning complete."); } // ------------------------------------------------------------------ // // Public query API // // ------------------------------------------------------------------ // /// /// Total threat in a room (sum of living-monster health). /// Returns 0 for rooms with no monsters or already cleared. /// public static int GetRoomThreat(int roomId) { if (!roomMonsters.TryGetValue(roomId, out var list)) return 0; return list.Where(m => m != null && !m.IsDead).Sum(m => m.ThreatValue); } /// How many living monsters are in a room. public static int GetLivingMonsterCount(int roomId) { if (!roomMonsters.TryGetValue(roomId, out var list)) return 0; return list.Count(m => m != null && !m.IsDead); } // ------------------------------------------------------------------ // // Spawning // // ------------------------------------------------------------------ // private void SpawnRoomMonsters() { if (!config.UseMonsterAreas) return; var allRooms = maze.Rooms; if (allRooms.Count == 0) return; // Build a "depth" estimate: BFS distance from start rooms. var depthMap = BuildRoomDepthMap(); float maxDepth = depthMap.Values.Count > 0 ? depthMap.Values.Max() : 1f; // Rooms eligible for monsters var eligibleRooms = allRooms.Where(r => r.Type != MazeRoom.RoomType.Safe && r.Type != MazeRoom.RoomType.End && !r.IsStart ).ToList(); // Filter down by MonsterAreaDensity % int targetCount = Mathf.RoundToInt(eligibleRooms.Count * (config.MonsterAreaDensity / 100f)); Shuffle(eligibleRooms); var chosenRooms = eligibleRooms.Take(targetCount).ToList(); // Always include all Boss rooms in the chosen set foreach (var boss in allRooms.Where(r => r.Type == MazeRoom.RoomType.Boss)) if (!chosenRooms.Contains(boss)) chosenRooms.Add(boss); foreach (var room in chosenRooms) { int count = Random.Range(minMonstersPerRoom, maxMonstersPerRoom + 1); if (room.Type == MazeRoom.RoomType.Boss) count += bossRoomBonusMonsters; // Difficulty: rooms farther from start are harder float depth = depthMap.TryGetValue(room.Id, out float d) ? d : 0f; float t = maxDepth > 0 ? depth / maxDepth : 0f; float difficulty = Mathf.Lerp(1f, maxDifficultyMultiplier, t); SpawnMonstersInRoom(room, count, difficulty); } } private void SpawnHallwayMonsters() { if (!config.UseMonsterAreas) return; // Iterate walkable tiles that are NOT inside any room for (int x = 0; x < maze.Width; x++) { for (int y = 0; y < maze.Height; y++) { if (!maze.IsWalkable(x, y)) continue; if (maze.GetRoomAtTile(x, y) != null) continue; // skip room tiles if (Random.value > hallwaySpawnChance) continue; float difficulty = Random.Range(1f, hallwayMaxDifficulty); SpawnSingleMonster(null, new Vector3(x + 0.5f, 1f, y + 0.5f), difficulty); } } } private void SpawnMonstersInRoom(MazeRoom room, int count, float difficulty) { for (int i = 0; i < count; i++) { Vector2Int tile = room.GetRandomPoint(); Vector3 worldPos = new Vector3(tile.x + 0.5f, 1f, tile.y + 0.5f); SpawnSingleMonster(room, worldPos, difficulty); } } private void SpawnSingleMonster(MazeRoom room, Vector3 worldPos, float difficulty) { GameObject go = Instantiate(monsterPrefab, worldPos, Quaternion.Euler(90, 0, 0)); var monster = go.GetComponent(); if (monster == null) monster = go.AddComponent(); // Use a placeholder room for hallway monsters so Init doesn't crash MazeRoom spawnRoom = room ?? new MazeRoom(-1, 0, 0, 0, 0); monster.Init(spawnRoom, maze, difficulty); // Track in lookup int key = room?.Id ?? -1; if (!roomMonsters.ContainsKey(key)) roomMonsters[key] = new List(); roomMonsters[key].Add(monster); // Remove from list on death monster.OnMonsterKilled += m => { roomMonsters[key]?.Remove(m); }; } // ------------------------------------------------------------------ // // Helpers // // ------------------------------------------------------------------ // /// BFS from all start-rooms to compute depth per room. private Dictionary BuildRoomDepthMap() { var depth = new Dictionary(); var queue = new Queue(); foreach (var room in maze.Rooms.Where(r => r.IsStart)) { depth[room.Id] = 0f; queue.Enqueue(room.Id); } // Build adjacency via room connectivity (rooms that share hallway tiles) var adj = BuildRoomAdjacency(); while (queue.Count > 0) { int id = queue.Dequeue(); if (!adj.TryGetValue(id, out var neighbours)) continue; foreach (int nId in neighbours) { if (!depth.ContainsKey(nId)) { depth[nId] = depth[id] + 1f; queue.Enqueue(nId); } } } return depth; } private Dictionary> BuildRoomAdjacency() { var adj = new Dictionary>(); foreach (var room in maze.Rooms) adj[room.Id] = new List(); // For each walkable non-room tile, check if it borders two different rooms for (int x = 0; x < maze.Width; x++) { for (int y = 0; y < maze.Height; y++) { if (!maze.IsWalkable(x, y)) continue; MazeRoom r1 = maze.GetRoomAtTile(x, y); if (r1 == null) continue; Vector2Int[] dirs = { new(x + 1, y), new(x - 1, y), new(x, y + 1), new(x, y - 1) }; foreach (var d in dirs) { if (!maze.IsInBounds(d.x, d.y) || !maze.IsWalkable(d.x, d.y)) continue; MazeRoom r2 = maze.GetRoomAtTile(d.x, d.y); if (r2 == null || r2.Id == r1.Id) continue; if (!adj[r1.Id].Contains(r2.Id)) adj[r1.Id].Add(r2.Id); if (!adj[r2.Id].Contains(r1.Id)) adj[r2.Id].Add(r1.Id); } } } return adj; } private static void Shuffle(List list) { for (int i = list.Count - 1; i > 0; i--) { int j = Random.Range(0, i + 1); (list[i], list[j]) = (list[j], list[i]); } } // ------------------------------------------------------------------ // // Prefab factory // // ------------------------------------------------------------------ // private GameObject CreateDefaultMonsterPrefab() { var go = new GameObject("MonsterPrefab"); go.transform.localScale = Vector3.one * 0.5f; go.AddComponent().radius = 1f; var mf = go.AddComponent(); mf.mesh = Resources.GetBuiltinResource("Sphere.fbx"); var mr = go.AddComponent(); var mat = new Material(Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard")); mat.color = Color.red; mr.material = mat; go.AddComponent(); return go; } }