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;
}
}