MazeSceneBuilder.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using UnityEngine;
  5. namespace MazeGen
  6. {
  7. /// <summary>
  8. /// Scene entry point: drop this on one empty GameObject in the scene and press Play.
  9. /// Generates the configured start rooms, then spawns and drives a configurable number
  10. /// of autonomous agents that explore the maze on their own (see AutonomousAgentBrain).
  11. /// The level ends when enough agents reach an exit room or a time limit is hit.
  12. ///
  13. /// Also owns the observation HUD:
  14. /// - simulation controls (pause/speed - the sim runs on SimulationClock, not Time.timeScale)
  15. /// - the agent list (click to have the camera follow one)
  16. /// - "Agent thoughts and info" for the followed agent, fed by its AgentThoughtLog
  17. /// - live fights with their join-window countdown and watch/simulate controls
  18. /// - the global combat log
  19. /// The HUD hides itself entirely while a fight is being watched, since BattleArenaView
  20. /// draws its own.
  21. /// </summary>
  22. [RequireComponent(typeof(MazeGenerator))]
  23. [RequireComponent(typeof(MonsterSpawner))]
  24. [RequireComponent(typeof(CombatDirector))]
  25. [RequireComponent(typeof(BattleArenaView))]
  26. public class MazeSceneBuilder : MonoBehaviour
  27. {
  28. [Header("Agents")]
  29. [SerializeField, Min(1)] private int agentCount = 1;
  30. [Tooltip("Each agent's spawn is delayed by a random amount in this range (seconds), so they enter the maze staggered over time instead of all at once.")]
  31. [SerializeField] private Vector2 spawnStaggerRange = new Vector2(0f, 3f);
  32. [Tooltip("Each agent's FIRST decision is delayed by a random amount in this range (seconds); every decision after that uses the steady decisionInterval.")]
  33. [SerializeField] private Vector2 initialDecisionDelayRange = new Vector2(0.5f, 2f);
  34. [Tooltip("Steady-state time (seconds) between an agent's decisions once it gets going.")]
  35. [SerializeField] private float decisionInterval = 1.5f;
  36. [Tooltip("Chance an agent explores a brand-new exit even when a 'follow' (known but personally-unvisited) option is also available.")]
  37. [SerializeField, Range(0f, 1f)] private float exploreChance = 0.85f;
  38. [Tooltip("Perceived challenge rating an agent will take on alone. Above this it avoids the room, or tries to sneak past instead of fighting.")]
  39. [SerializeField] private float retreatThreshold = 1.5f;
  40. [Header("Win conditions (at least one must be > 0)")]
  41. [SerializeField] private int targetFinishedAgentCount = 1;
  42. [SerializeField] private float timeLimitSeconds = 0f;
  43. private class AgentEntry
  44. {
  45. public MazeAgentController Controller;
  46. public PathVisualizer Path;
  47. public Color Color;
  48. }
  49. private MazeGenerator _generator;
  50. private MonsterSpawner _monsterSpawner;
  51. private CombatDirector _director;
  52. private MazeCameraController _cameraController;
  53. private readonly Dictionary<int, ExitMarker> _exitMarkers = new Dictionary<int, ExitMarker>();
  54. private readonly List<AgentEntry> _agents = new List<AgentEntry>();
  55. private AgentEntry _followedAgent;
  56. private bool _levelEnded;
  57. private float _elapsedTime;
  58. private Vector2 _hudScroll;
  59. private Vector2 _thoughtScroll;
  60. private Vector2 _combatLogScroll;
  61. private Vector2 _fightScroll;
  62. private void Awake()
  63. {
  64. // Static state, and the editor can skip domain reload between Play sessions - clear
  65. // it so a stale pause hold from the last run can't freeze this one on startup.
  66. SimulationClock.Reset();
  67. _generator = GetComponent<MazeGenerator>();
  68. _generator.RoomGenerated += OnRoomGenerated;
  69. _generator.ExitAdded += OnExitAdded;
  70. _monsterSpawner = GetComponent<MonsterSpawner>();
  71. // RequireComponent only auto-adds when the script is attached in the editor, so a
  72. // scene saved before these existed would come up without them - add them if missing
  73. // rather than null-referencing through the whole HUD.
  74. _director = GetComponent<CombatDirector>() ?? gameObject.AddComponent<CombatDirector>();
  75. _director.FightResolved += OnFightResolved;
  76. var cam = Camera.main;
  77. if (cam != null)
  78. {
  79. _cameraController = cam.GetComponent<MazeCameraController>();
  80. if (_cameraController == null)
  81. _cameraController = cam.gameObject.AddComponent<MazeCameraController>();
  82. }
  83. if (targetFinishedAgentCount <= 0 && timeLimitSeconds <= 0f)
  84. {
  85. Debug.LogWarning("[MazeGen] No end condition configured (targetFinishedAgentCount and timeLimitSeconds are both 0) - defaulting targetFinishedAgentCount to agentCount so the level can still end.");
  86. targetFinishedAgentCount = agentCount;
  87. }
  88. }
  89. private void Start()
  90. {
  91. var settings = _generator.Settings;
  92. int startRoomCount = Mathf.Max(1, settings.startRoomCount);
  93. var startRooms = new List<Room>();
  94. for (int i = 0; i < startRoomCount; i++)
  95. {
  96. Vector2 pos = startRoomCount > 1 ? PickRandomStartPosition(settings, startRooms) : Vector2.zero;
  97. startRooms.Add(_generator.GenerateStartRoom(pos));
  98. }
  99. _cameraController?.FrameBounds(ComputeMazeBounds());
  100. // Assign each agent to a start room, then spawn per-room with independent stagger
  101. var assignments = AssignAgentsToStartRooms(agentCount, startRooms.Count);
  102. for (int r = 0; r < startRooms.Count; r++)
  103. StartCoroutine(SpawnAgentsForRoomRoutine(startRooms[r], assignments[r]));
  104. }
  105. /// <summary>
  106. /// Picks a uniformly random point within a disk of radius `startRoomSpread` (instead
  107. /// of evenly around its edge), retrying a few times to avoid landing on top of a
  108. /// start room already placed this call.
  109. /// </summary>
  110. private static Vector2 PickRandomStartPosition(MazeGenerationSettings settings, List<Room> placed)
  111. {
  112. float spread = Mathf.Max(settings.startRoomSpread, 0.01f);
  113. float minDistance = settings.startRoomRadius * 2f;
  114. for (int attempt = 0; attempt < 30; attempt++)
  115. {
  116. Vector2 candidate = Random.insideUnitCircle * spread;
  117. bool overlapsPlaced = false;
  118. foreach (var room in placed)
  119. {
  120. if (Vector2.Distance(candidate, room.Position) < minDistance)
  121. {
  122. overlapsPlaced = true;
  123. break;
  124. }
  125. }
  126. if (!overlapsPlaced) return candidate;
  127. }
  128. return Random.insideUnitCircle * spread; // give up avoiding overlap, use whatever we last rolled
  129. }
  130. /// <summary>
  131. /// Randomly assigns each agent index (0..agentCount-1) to one of the start rooms, decided
  132. /// upfront before any staggering/spawning begins, so the same statistical distribution as
  133. /// today's "pick a random room per spawn" applies, but grouped so each room's agents can be
  134. /// staggered independently and concurrently with other rooms.
  135. /// </summary>
  136. private static List<int>[] AssignAgentsToStartRooms(int agentCount, int roomCount)
  137. {
  138. var result = new List<int>[roomCount];
  139. for (int r = 0; r < roomCount; r++)
  140. result[r] = new List<int>();
  141. for (int i = 0; i < agentCount; i++)
  142. result[Random.Range(0, roomCount)].Add(i);
  143. return result;
  144. }
  145. /// <summary>
  146. /// One independent instance of this runs per start room, all started concurrently from
  147. /// Start(). Staggers only the agents assigned to THIS room by spawnStaggerRange, so agents
  148. /// in different start rooms can spawn at the same wall-clock time while agents sharing a
  149. /// room are still staggered relative to each other, same as before.
  150. /// </summary>
  151. private IEnumerator SpawnAgentsForRoomRoutine(Room startRoom, List<int> agentIndices)
  152. {
  153. foreach (int index in agentIndices)
  154. {
  155. float delay = Random.Range(spawnStaggerRange.x, spawnStaggerRange.y);
  156. if (delay > 0f) yield return new WaitForSeconds(delay);
  157. SpawnAgent(startRoom, index);
  158. }
  159. }
  160. private void SpawnAgent(Room startRoom, int index)
  161. {
  162. var agentGO = new GameObject($"Agent_{index}");
  163. agentGO.transform.SetParent(transform, false);
  164. var controller = agentGO.AddComponent<MazeAgentController>();
  165. controller.Initialize(_generator);
  166. // Golden-ratio hue spread gives visually distinct colors even for many agents.
  167. Color color = Color.HSVToRGB((index * 0.618034f) % 1f, 0.65f, 1f);
  168. controller.Spawn(startRoom, color);
  169. var pathGO = new GameObject("Path");
  170. pathGO.transform.SetParent(agentGO.transform, false);
  171. pathGO.AddComponent<LineRenderer>();
  172. var path = pathGO.AddComponent<PathVisualizer>();
  173. path.AppendPoint(startRoom.Position);
  174. pathGO.SetActive(false); // only the followed agent's trail is shown
  175. var brain = agentGO.AddComponent<AutonomousAgentBrain>();
  176. float initialDelay = Random.Range(initialDecisionDelayRange.x, initialDecisionDelayRange.y);
  177. brain.Initialize(controller, _generator, _director, initialDelay, decisionInterval, exploreChance, retreatThreshold, () => _levelEnded);
  178. // The director needs the roster to know who else is standing in a room when a fight
  179. // breaks out there, and which color to draw each agent with in the arena.
  180. _director.RegisterAgent(controller, color);
  181. controller.Agent.RoomEntered += room => path.AppendPoint(room.Position);
  182. controller.Agent.RoomEntered += _ => RefreshMarkerVisuals();
  183. controller.Agent.RoomEntered += _ => CheckFinishCondition();
  184. controller.Agent.ReachedExitRoom += room => Debug.Log($"[MazeGen] {controller.Agent.DisplayName} reached the exit room (id {room.Id})!");
  185. _agents.Add(new AgentEntry { Controller = controller, Path = path, Color = color });
  186. }
  187. private void OnRoomGenerated(Room room)
  188. {
  189. var roomGO = new GameObject($"Room_{room.Id}_{room.Kind}");
  190. roomGO.transform.SetParent(transform, false);
  191. roomGO.AddComponent<RoomView>().Initialize(room);
  192. foreach (var exit in room.Exits)
  193. SpawnExitMarker(room, exit);
  194. }
  195. private void OnExitAdded(Room room, Exit exit)
  196. {
  197. SpawnExitMarker(room, exit);
  198. RefreshMarkerVisuals();
  199. }
  200. private void SpawnExitMarker(Room room, Exit exit)
  201. {
  202. var markerGO = new GameObject($"Exit_{exit.Id}");
  203. markerGO.transform.SetParent(transform, false);
  204. var marker = markerGO.AddComponent<ExitMarker>();
  205. marker.Initialize(room, exit);
  206. _exitMarkers[exit.Id] = marker;
  207. }
  208. private void RefreshMarkerVisuals()
  209. {
  210. Room currentRoom = _followedAgent?.Controller.Agent?.CurrentRoom;
  211. MazeAgent agent = _followedAgent?.Controller.Agent;
  212. foreach (var marker in _exitMarkers.Values)
  213. marker.UpdateVisual(currentRoom, agent);
  214. }
  215. /// <summary>A fight is over: drop the views of anything that died in it and recheck the level.</summary>
  216. private void OnFightResolved(PendingFight fight)
  217. {
  218. _monsterSpawner?.RefreshRoom(fight.Room);
  219. RefreshMarkerVisuals();
  220. CheckFinishCondition();
  221. }
  222. private void Update()
  223. {
  224. if (MazeInput.WasPauseTogglePressed())
  225. SimulationClock.UserPaused = !SimulationClock.UserPaused;
  226. if (_levelEnded) return;
  227. // The level clock is simulation time, so pausing to watch a fight doesn't burn it.
  228. _elapsedTime += SimulationClock.DeltaTime;
  229. if (timeLimitSeconds > 0f && _elapsedTime >= timeLimitSeconds)
  230. EndLevel($"Time limit of {timeLimitSeconds:0.0}s reached.");
  231. }
  232. private void CheckFinishCondition()
  233. {
  234. if (_levelEnded) return;
  235. int finished = _agents.Count(a => a.Controller.Agent.IsFinished);
  236. if (targetFinishedAgentCount > 0 && finished >= targetFinishedAgentCount)
  237. {
  238. EndLevel($"{finished} agent(s) reached the exit.");
  239. return;
  240. }
  241. if (_agents.Count > 0 && _agents.Count >= agentCount && _agents.All(a => a.Controller.Agent.IsDead || a.Controller.Agent.IsFinished))
  242. EndLevel($"Every agent is dead or finished ({finished} reached the exit).");
  243. }
  244. private void EndLevel(string reason)
  245. {
  246. _levelEnded = true;
  247. Debug.Log($"[MazeGen] LEVEL COMPLETE - {reason}");
  248. }
  249. private void FollowAgent(AgentEntry entry)
  250. {
  251. if (_followedAgent == entry) return;
  252. StopFollowing();
  253. _followedAgent = entry;
  254. entry.Controller.SetFollowed(true);
  255. entry.Path.gameObject.SetActive(true);
  256. entry.Controller.Agent.RoomEntered += OnFollowedAgentMoved;
  257. _cameraController?.FocusOn(entry.Controller.Agent.CurrentRoom.Position);
  258. RefreshMarkerVisuals();
  259. }
  260. private void StopFollowing()
  261. {
  262. if (_followedAgent == null) return;
  263. _followedAgent.Controller.SetFollowed(false);
  264. _followedAgent.Path.gameObject.SetActive(false);
  265. _followedAgent.Controller.Agent.RoomEntered -= OnFollowedAgentMoved;
  266. _followedAgent = null;
  267. RefreshMarkerVisuals();
  268. }
  269. private void OnFollowedAgentMoved(Room room) => _cameraController?.FocusOn(room.Position);
  270. private Bounds ComputeMazeBounds()
  271. {
  272. var rooms = _generator.Graph.Rooms.Values;
  273. if (rooms.Count == 0) return new Bounds(Vector3.zero, Vector3.one * 8f);
  274. var bounds = new Bounds(rooms.First().Position, Vector3.zero);
  275. foreach (var room in rooms)
  276. bounds.Encapsulate(new Bounds(room.Position, Vector3.one * (room.Radius * 2f)));
  277. return bounds;
  278. }
  279. // ---------------------------------------------------------------- HUD
  280. private void OnGUI()
  281. {
  282. // The arena draws its own HUD and owns the screen while a fight is being watched.
  283. if (_director != null && _director.IsWatching) return;
  284. DrawStatusPanel();
  285. DrawFightPanel();
  286. DrawFollowedAgentPanel();
  287. }
  288. private void DrawStatusPanel()
  289. {
  290. GUILayout.BeginArea(new Rect(10, 10, 280, Screen.height - 20), GUI.skin.box);
  291. string timeLabel = timeLimitSeconds > 0f ? $"{_elapsedTime:0.0}s / {timeLimitSeconds:0.0}s" : $"{_elapsedTime:0.0}s";
  292. GUILayout.Label($"Elapsed: {timeLabel}");
  293. int finishedCount = _agents.Count(a => a.Controller.Agent.IsFinished);
  294. string finishLabel = targetFinishedAgentCount > 0 ? $"{finishedCount} / {targetFinishedAgentCount}" : $"{finishedCount}";
  295. GUILayout.Label($"Finished: {finishLabel} Dead: {_agents.Count(a => a.Controller.Agent.IsDead)}");
  296. if (_levelEnded)
  297. GUILayout.Label("LEVEL COMPLETE", GUI.skin.box);
  298. GUILayout.Space(4);
  299. DrawSimulationControls();
  300. GUILayout.Space(4);
  301. if (GUILayout.Button("Zoom to fit maze"))
  302. _cameraController?.FrameBounds(ComputeMazeBounds());
  303. if (_followedAgent != null && GUILayout.Button("Stop following"))
  304. StopFollowing();
  305. GUILayout.Space(6);
  306. GUILayout.Label("Agents (click to follow):");
  307. _hudScroll = GUILayout.BeginScrollView(_hudScroll);
  308. foreach (var entry in _agents)
  309. {
  310. MazeAgent agent = entry.Controller.Agent;
  311. bool isFollowed = entry == _followedAgent;
  312. GUI.color = agent.IsDead ? new Color(0.55f, 0.55f, 0.55f) : entry.Color;
  313. if (GUILayout.Button($"{(isFollowed ? "> " : "")}{agent.DisplayName} - {DescribeStatus(agent)}"))
  314. FollowAgent(entry);
  315. }
  316. GUI.color = Color.white;
  317. GUILayout.EndScrollView();
  318. GUILayout.EndArea();
  319. }
  320. private void DrawSimulationControls()
  321. {
  322. bool paused = SimulationClock.IsPaused;
  323. GUILayout.Label(paused ? $"PAUSED ({SimulationClock.HoldSummary ?? "manual"})" : $"Running at {SimulationClock.TimeScale:0.0}x", GUI.skin.box);
  324. GUILayout.BeginHorizontal();
  325. if (GUILayout.Button(SimulationClock.UserPaused ? "Resume (space)" : "Pause (space)"))
  326. SimulationClock.UserPaused = !SimulationClock.UserPaused;
  327. if (GUILayout.Button("0.5x")) SimulationClock.TimeScale = 0.5f;
  328. if (GUILayout.Button("1x")) SimulationClock.TimeScale = 1f;
  329. if (GUILayout.Button("4x")) SimulationClock.TimeScale = 4f;
  330. GUILayout.EndHorizontal();
  331. _director.PauseOnFightDeclared = GUILayout.Toggle(_director.PauseOnFightDeclared, " Pause when a fight is declared");
  332. GUILayout.BeginHorizontal();
  333. GUILayout.Label("On join window end:", GUILayout.Width(120f));
  334. bool watch = _director.TimeoutResolution == CombatViewMode.Watch;
  335. if (GUILayout.Button(watch ? "Watch fight" : "Log only"))
  336. _director.TimeoutResolution = watch ? CombatViewMode.Simulate : CombatViewMode.Watch;
  337. GUILayout.EndHorizontal();
  338. }
  339. /// <summary>
  340. /// Live fights: the join-window countdown plus the controls to watch or fast-resolve.
  341. /// Drawn centre-top so a paused fight is impossible to miss.
  342. /// </summary>
  343. private void DrawFightPanel()
  344. {
  345. var fights = _director.ActiveFights;
  346. if (fights.Count == 0) return;
  347. float width = Mathf.Min(460f, Mathf.Max(280f, Screen.width - 720f));
  348. GUILayout.BeginArea(new Rect(300f, 10f, width, Screen.height - 20f), GUI.skin.box);
  349. GUILayout.Label($"FIGHTS BREAKING OUT ({fights.Count})", GUI.skin.box);
  350. // The "resume" button stays outside the scroll view so it's always reachable even
  351. // when there are more fights than fit on screen at once.
  352. _fightScroll = GUILayout.BeginScrollView(_fightScroll);
  353. // Snapshot: resuming from this panel can resolve/cancel a fight, mutating the live list.
  354. foreach (var fight in fights.ToArray())
  355. {
  356. GUILayout.BeginVertical(GUI.skin.box);
  357. GUILayout.Label($"#{fight.Id} in room {fight.Room.Id} - {fight.Agents.Count} agent(s) vs {fight.Encounter.AliveMonsterCount} monster(s)");
  358. GUILayout.Label($"Starts in {Mathf.Max(0f, fight.JoinTimeRemaining):0.0}s - {(SimulationClock.IsPaused ? "frozen; resume to let the timer run out" : "others can still join")}");
  359. GUILayout.Label($"In: {string.Join(", ", fight.Agents.Select(a => a.DisplayName))}");
  360. // These only set a preference for when the join window naturally expires - they
  361. // never skip the timer, so late arrivals always get their full chance to join.
  362. CombatViewMode preferred = fight.PreferredResolution ?? _director.TimeoutResolution;
  363. GUILayout.Label($"When it starts: {(preferred == CombatViewMode.Watch ? "watch it" : "log only")}");
  364. GUILayout.BeginHorizontal();
  365. GUI.enabled = preferred != CombatViewMode.Watch;
  366. if (GUILayout.Button("Watch it"))
  367. _director.PreferWatch(fight);
  368. GUI.enabled = preferred != CombatViewMode.Simulate;
  369. if (GUILayout.Button("Log only"))
  370. _director.PreferSimulate(fight);
  371. GUI.enabled = true;
  372. if (GUILayout.Button("Go to"))
  373. _cameraController?.FocusOn(fight.Room.Position);
  374. GUILayout.EndHorizontal();
  375. GUILayout.EndVertical();
  376. }
  377. GUILayout.EndScrollView();
  378. if (SimulationClock.IsPaused && GUILayout.Button("Resume - let the join window run"))
  379. {
  380. _director.ResumeAllMusters();
  381. SimulationClock.UserPaused = false;
  382. }
  383. GUILayout.EndArea();
  384. }
  385. /// <summary>
  386. /// "Agent thoughts and info" for the followed agent: its vitals plus its decision
  387. /// narration, newest first, and the shared combat log underneath.
  388. /// </summary>
  389. private void DrawFollowedAgentPanel()
  390. {
  391. float width = 400f;
  392. float x = Screen.width - width - 10f;
  393. float height = Screen.height - 20f;
  394. GUILayout.BeginArea(new Rect(x, 10f, width, height), GUI.skin.box);
  395. if (_followedAgent == null)
  396. {
  397. GUILayout.Label("AGENT THOUGHTS AND INFO", GUI.skin.box);
  398. GUILayout.Label("Click an agent on the left to follow it and read what it's thinking.");
  399. }
  400. else
  401. {
  402. MazeAgent agent = _followedAgent.Controller.Agent;
  403. CharacterSheet sheet = agent.CharacterSheet;
  404. GUI.color = _followedAgent.Color;
  405. GUILayout.Label($"{agent.DisplayName.ToUpperInvariant()} - {DescribeStatus(agent)}", GUI.skin.box);
  406. GUI.color = Color.white;
  407. GUILayout.Label($"Room {agent.CurrentRoom?.Id.ToString() ?? "?"} New rooms seen: {agent.NewRoomsVisited} Path: {agent.PathHistory.Count} hop(s)");
  408. GUILayout.Label($"HP {sheet.Stats.CurrentHealth:0.#}/{sheet.Stats.MaxHealth:0.#} ATK {sheet.GetEffectiveAttack():0.#} DEF {sheet.GetEffectiveDefense():0.#} SPD {sheet.GetEffectiveSpeed():0.#}");
  409. GUILayout.Label($"Weapon: {sheet.EquippedWeapon?.ItemName ?? "none"} Armor: {sheet.EquippedArmor?.ItemName ?? "none"}");
  410. Room room = agent.CurrentRoom;
  411. int hostiles = room?.Encounter != null ? room.Encounter.AliveMonsterCount : 0;
  412. GUILayout.Label(hostiles > 0
  413. ? $"This room: {hostiles} living hostile(s), threat {room.Encounter.GetPerceivedChallengeRating(sheet):0.00}"
  414. : "This room: clear");
  415. GUILayout.Space(4);
  416. GUILayout.Label("Thoughts (newest first):");
  417. _thoughtScroll = GUILayout.BeginScrollView(_thoughtScroll, GUILayout.Height(Mathf.Max(120f, height * 0.42f)));
  418. var thoughts = agent.Thoughts.Entries;
  419. for (int i = thoughts.Count - 1; i >= 0; i--)
  420. {
  421. Thought thought = thoughts[i];
  422. GUI.color = AgentThoughtLog.ColorFor(thought.Kind);
  423. GUILayout.Label($"[{thought.Time:0.0}s] {thought.Text}");
  424. }
  425. GUI.color = Color.white;
  426. GUILayout.EndScrollView();
  427. }
  428. GUILayout.Space(4);
  429. GUILayout.Label("Combat log (all fights):");
  430. _combatLogScroll = GUILayout.BeginScrollView(_combatLogScroll);
  431. var log = _director.CombatLog;
  432. for (int i = log.Count - 1; i >= 0; i--)
  433. GUILayout.Label(log[i]);
  434. GUILayout.EndScrollView();
  435. GUILayout.EndArea();
  436. }
  437. private string DescribeStatus(MazeAgent agent)
  438. {
  439. if (agent.IsDead) return "dead";
  440. if (agent.IsFinished) return "reached exit";
  441. if (agent.IsEngaged) return "fighting";
  442. return $"exploring ({agent.CharacterSheet.Stats.CurrentHealth:0}hp)";
  443. }
  444. }
  445. }