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