using System.Collections.Generic;
using UnityEngine;
namespace MazeGen
{
///
/// Spawns visual monster GameObjects when rooms with encounters are generated.
/// Listens to MazeGenerator's RoomGenerated event and creates MonsterView children
/// in each room that has an encounter.
///
/// Also keeps the views honest after a fight: removes the views of
/// monsters that died, so a cleared room actually looks cleared.
///
public class MonsterSpawner : MonoBehaviour
{
[SerializeField] private MazeGenerator mazeGenerator;
// Container transform to organize spawned monsters in the hierarchy
private Transform _monstersContainer;
// Views per room id, so a resolved fight can clean up just that room's dead.
private readonly Dictionary> _viewsByRoom = new Dictionary>();
// Subscribing in Awake rather than Start: MazeSceneBuilder generates the start rooms in
// its own Start, and component Start order on a shared GameObject isn't something to rely
// on - subscribing a frame-phase earlier guarantees no room generation is missed.
private void Awake()
{
if (mazeGenerator == null)
mazeGenerator = GetComponent();
if (mazeGenerator == null)
{
Debug.LogError("[MonsterSpawner] Could not find MazeGenerator!");
return;
}
// Create a container for all spawned monsters
_monstersContainer = new GameObject("SpawnedMonsters").transform;
_monstersContainer.SetParent(transform);
_monstersContainer.localPosition = Vector3.zero;
// Subscribe to room generation event
mazeGenerator.RoomGenerated += OnRoomGenerated;
}
private void OnDestroy()
{
if (mazeGenerator != null)
mazeGenerator.RoomGenerated -= OnRoomGenerated;
}
///
/// Called whenever a new room is generated. If it has an encounter,
/// spawn visual monsters in that room.
///
private void OnRoomGenerated(Room room)
{
if (room.Encounter == null || room.Encounter.Monsters.Count == 0)
return;
SpawnMonstersInRoom(room);
}
///
/// Drops the views of any monster in this room that is now dead. Wire this to
/// CombatDirector.FightResolved so the map reflects the fight's outcome.
///
public void RefreshRoom(Room room)
{
if (room == null || !_viewsByRoom.TryGetValue(room.Id, out var views)) return;
for (int i = views.Count - 1; i >= 0; i--)
{
MonsterView view = views[i];
if (view == null)
{
views.RemoveAt(i);
continue;
}
if (view.MonsterData != null && view.MonsterData.IsDefeated)
{
views.RemoveAt(i);
view.OnDefeated();
}
}
if (views.Count == 0)
_viewsByRoom.Remove(room.Id);
}
///
/// Creates visual MonsterView GameObjects scattered around the room.
///
private void SpawnMonstersInRoom(Room room)
{
var encounter = room.Encounter;
int monsterCount = encounter.Monsters.Count;
// Create a parent for this room's monsters
var roomMonstersParent = new GameObject($"Room{room.Id}_Monsters").transform;
roomMonstersParent.SetParent(_monstersContainer);
roomMonstersParent.localPosition = Vector3.zero;
var views = new List(monsterCount);
_viewsByRoom[room.Id] = views;
// Scatter monsters around the room in a circle
float angleStep = 360f / monsterCount;
float scatterRadius = room.Radius * 0.5f; // Spawn within the room
for (int i = 0; i < monsterCount; i++)
{
var monsterData = encounter.Monsters[i];
float angle = i * angleStep * Mathf.Deg2Rad;
Vector2 offset = new Vector2(
Mathf.Cos(angle) * scatterRadius,
Mathf.Sin(angle) * scatterRadius
);
Vector2 spawnPos = room.Position + offset;
views.Add(SpawnMonster(monsterData, spawnPos, roomMonstersParent));
}
}
///
/// Instantiates a single MonsterView at the given position.
///
private MonsterView SpawnMonster(MonsterEncounter.MonsterInstance monsterData, Vector2 position, Transform parent)
{
var monsterGO = new GameObject(monsterData.Template.MonsterName);
monsterGO.transform.SetParent(parent);
monsterGO.transform.position = position;
var monsterView = monsterGO.AddComponent();
monsterView.Initialize(monsterData, position);
// Optional: add a collider for future click-detection or physics
var collider = monsterGO.AddComponent();
collider.radius = 0.15f;
return monsterView;
}
}
}