| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297 |
- using UnityEngine;
- using System.Collections.Generic;
- using System.Linq;
- /// <summary>
- /// 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.
- /// </summary>
- 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;
- /// <summary>
- /// Room ID → list of living monsters. Agents can query this to assess danger.
- /// Hallway monsters are stored under key -1.
- /// </summary>
- private static readonly Dictionary<int, List<Monster>> roomMonsters = new();
- // ------------------------------------------------------------------ //
- // Public entry point – called by MazeController after generation //
- // ------------------------------------------------------------------ //
- /// <summary>
- /// Called by MazeController once the maze has been fully generated.
- /// Destroys any previously spawned monsters and re-spawns for the new maze.
- /// </summary>
- public void SpawnForMaze(MazeData mazeData, MazeConfig mazeConfig)
- {
- // Destroy any monsters left over from a previous maze
- foreach (var m in FindObjectsByType<Monster>())
- 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 //
- // ------------------------------------------------------------------ //
- /// <summary>
- /// Total threat in a room (sum of living-monster health).
- /// Returns 0 for rooms with no monsters or already cleared.
- /// </summary>
- 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);
- }
- /// <summary>How many living monsters are in a room.</summary>
- 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<Monster>();
- if (monster == null) monster = go.AddComponent<Monster>();
- // 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<Monster>();
- roomMonsters[key].Add(monster);
- // Remove from list on death
- monster.OnMonsterKilled += m => { roomMonsters[key]?.Remove(m); };
- }
- // ------------------------------------------------------------------ //
- // Helpers //
- // ------------------------------------------------------------------ //
- /// <summary>BFS from all start-rooms to compute depth per room.</summary>
- private Dictionary<int, float> BuildRoomDepthMap()
- {
- var depth = new Dictionary<int, float>();
- var queue = new Queue<int>();
- 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<int, List<int>> BuildRoomAdjacency()
- {
- var adj = new Dictionary<int, List<int>>();
- foreach (var room in maze.Rooms)
- adj[room.Id] = new List<int>();
- // 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<T>(List<T> 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<SphereCollider>().radius = 1f;
- var mf = go.AddComponent<MeshFilter>();
- mf.mesh = Resources.GetBuiltinResource<Mesh>("Sphere.fbx");
- var mr = go.AddComponent<MeshRenderer>();
- var mat = new Material(Shader.Find("Universal Render Pipeline/Lit")
- ?? Shader.Find("Standard"));
- mat.color = Color.red;
- mr.material = mat;
- go.AddComponent<Monster>();
- return go;
- }
- }
|