BattleArenaView.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using UnityEngine.SceneManagement;
  5. namespace MazeGen
  6. {
  7. /// <summary>
  8. /// Plays a fight out on a grid in its own additively-loaded scene, then throws that scene
  9. /// away and hands the camera back to the maze.
  10. ///
  11. /// Why this can't corrupt the maze: the fight's outcome is already fully decided before
  12. /// playback starts (see <see cref="CombatResolver"/>), so this class is pure animation over a
  13. /// finished <see cref="BattleResult"/> - it never touches a character sheet, a room, or an
  14. /// agent. Health bars run off a private display copy of each combatant's health. And because
  15. /// everything it creates lives in a separate scene which is unloaded on exit, there's nothing
  16. /// left behind to restore. The maze's own GameObjects are never disabled or re-created (which
  17. /// would kill the spawn coroutines), only hidden by suspending the maze camera; when the fight
  18. /// ends every agent is exactly where it was, and only the fight's result has changed.
  19. ///
  20. /// Runs on real Unity time, because the maze simulation is frozen via SimulationClock (not
  21. /// Time.timeScale) precisely so that this playback can still animate.
  22. /// </summary>
  23. [DisallowMultipleComponent]
  24. public class BattleArenaView : MonoBehaviour
  25. {
  26. [Header("Arena")]
  27. [Tooltip("Grid dimensions (columns x rows) of the battle floor.")]
  28. [SerializeField] private Vector2Int gridSize = new Vector2Int(11, 7);
  29. [SerializeField] private float cellSize = 1f;
  30. [Tooltip("Where in world space the arena scene is built. Far from the maze so the arena camera can't see maze geometry.")]
  31. [SerializeField] private Vector2 arenaWorldOrigin = new Vector2(0f, 10000f);
  32. [Header("Playback")]
  33. [Tooltip("Base tempo: seconds per combat event at 1x speed.")]
  34. [SerializeField, Min(0.02f)] private float secondsPerEvent = 0.4f;
  35. [SerializeField, Range(0.25f, 8f)] private float playbackSpeed = 1f;
  36. [Tooltip("Seconds the final result stays on screen before auto-returning to the maze (0 = wait for the button).")]
  37. [SerializeField, Min(0f)] private float autoReturnSeconds = 4f;
  38. /// <summary>Per-combatant playback state. DisplayHealth is a copy - never the real sheet.</summary>
  39. private class Slot
  40. {
  41. public Combatant Combatant;
  42. public Transform Transform;
  43. public SpriteRenderer Sprite;
  44. public Vector3 Home;
  45. public Vector3 CurrentGridPos; // Current position on the grid
  46. public float DisplayHealth;
  47. public bool Felled;
  48. }
  49. private struct Floater
  50. {
  51. public Vector3 WorldPosition;
  52. public string Text;
  53. public Color Color;
  54. public float BornAt;
  55. }
  56. private readonly Dictionary<int, Slot> _slots = new Dictionary<int, Slot>();
  57. private readonly List<Floater> _floaters = new List<Floater>();
  58. private readonly List<string> _feed = new List<string>();
  59. private readonly List<Camera> _suspendedCameras = new List<Camera>();
  60. private Scene _scene;
  61. private bool _sceneLoaded;
  62. private GameObject _root;
  63. private Camera _camera;
  64. private PendingFight _fight;
  65. private BattleResult _result;
  66. private int _round;
  67. private bool _playbackFinished;
  68. private bool _skipRequested;
  69. private bool _returnRequested;
  70. private Vector2 _feedScroll;
  71. public bool IsBuilt => _sceneLoaded;
  72. /// <summary>Seconds per animation "beat" at the current playback speed.</summary>
  73. private float Beat => Mathf.Max(0.02f, secondsPerEvent) / Mathf.Max(0.25f, playbackSpeed);
  74. /// <summary>
  75. /// Builds the arena, replays the fight, waits for the observer, and tears everything back
  76. /// down. Yield on this from a coroutine; when it completes the maze is on screen again.
  77. /// </summary>
  78. public IEnumerator Play(PendingFight fight, IReadOnlyList<Combatant> combatants)
  79. {
  80. _fight = fight;
  81. _result = fight.Result;
  82. _skipRequested = false;
  83. _returnRequested = false;
  84. _playbackFinished = false;
  85. _round = 0;
  86. _feed.Clear();
  87. _floaters.Clear();
  88. Build(combatants);
  89. yield return Replay();
  90. ApplyFinalState();
  91. _playbackFinished = true;
  92. yield return WaitForObserver();
  93. Teardown();
  94. }
  95. // ---------------------------------------------------------------- build / teardown
  96. private void Build(IReadOnlyList<Combatant> combatants)
  97. {
  98. // A per-fight scene name: CreateScene refuses to make a second scene with a name
  99. // that's already loaded, and a previous arena may still be unloading.
  100. _scene = SceneManager.CreateScene($"CombatArena_{_fight.Id}_{Time.frameCount}");
  101. _sceneLoaded = true;
  102. _root = new GameObject("ArenaRoot");
  103. SceneManager.MoveGameObjectToScene(_root, _scene);
  104. _root.transform.position = arenaWorldOrigin;
  105. BuildFloor();
  106. BuildCamera();
  107. PlaceCombatants(combatants);
  108. }
  109. private void BuildFloor()
  110. {
  111. var floor = new GameObject("Floor");
  112. floor.transform.SetParent(_root.transform, false);
  113. for (int x = 0; x < gridSize.x; x++)
  114. {
  115. for (int y = 0; y < gridSize.y; y++)
  116. {
  117. var tile = new GameObject($"Cell_{x}_{y}");
  118. tile.transform.SetParent(floor.transform, false);
  119. tile.transform.position = CellToWorld(x, y);
  120. tile.transform.localScale = Vector3.one * (cellSize * 0.96f);
  121. var sr = tile.AddComponent<SpriteRenderer>();
  122. sr.sprite = RoomView.SharedSquareSprite;
  123. sr.color = (x + y) % 2 == 0 ? new Color(0.16f, 0.17f, 0.2f) : new Color(0.21f, 0.22f, 0.26f);
  124. sr.sortingOrder = 0;
  125. }
  126. }
  127. }
  128. private void BuildCamera()
  129. {
  130. var cameraGO = new GameObject("ArenaCamera");
  131. cameraGO.transform.SetParent(_root.transform, false);
  132. cameraGO.transform.position = new Vector3(arenaWorldOrigin.x, arenaWorldOrigin.y, -10f);
  133. _camera = cameraGO.AddComponent<Camera>();
  134. _camera.orthographic = true;
  135. _camera.backgroundColor = new Color(0.05f, 0.05f, 0.07f);
  136. _camera.clearFlags = CameraClearFlags.SolidColor;
  137. _camera.depth = 100f; // draw over anything else that somehow stays enabled
  138. float aspect = Mathf.Max(_camera.aspect, 0.01f);
  139. float halfHeight = gridSize.y * cellSize * 0.5f;
  140. float halfWidth = gridSize.x * cellSize * 0.5f;
  141. _camera.orthographicSize = Mathf.Max(halfHeight, halfWidth / aspect) * 1.35f;
  142. // Hide the maze by suspending its cameras rather than deactivating maze objects -
  143. // SetActive(false) on the maze root would permanently kill its spawn coroutines.
  144. _suspendedCameras.Clear();
  145. foreach (var cam in Camera.allCameras)
  146. {
  147. if (cam == _camera) continue;
  148. _suspendedCameras.Add(cam);
  149. cam.enabled = false;
  150. }
  151. }
  152. /// <summary>
  153. /// Place combatants at their starting grid positions. Uses StartGridPosition, not the
  154. /// live GridPosition - by the time playback starts, CombatResolver has already resolved
  155. /// the whole fight synchronously, so GridPosition holds the *final* position, not where
  156. /// the combatant actually started.
  157. /// </summary>
  158. private void PlaceCombatants(IReadOnlyList<Combatant> combatants)
  159. {
  160. _slots.Clear();
  161. foreach (var combatant in combatants)
  162. {
  163. Vector3 worldPos = CellToWorld(combatant.StartGridPosition.x, combatant.StartGridPosition.y);
  164. _slots[combatant.Id] = CreateSlot(combatant, worldPos);
  165. }
  166. }
  167. private Slot CreateSlot(Combatant combatant, Vector3 position)
  168. {
  169. var go = new GameObject($"{(combatant.IsAgent ? "Agent" : "Monster")}_{combatant.Name}");
  170. go.transform.SetParent(_root.transform, false);
  171. go.transform.position = position;
  172. // Scale based on size: Large > Medium > Small
  173. float sizeScale = combatant.Size switch
  174. {
  175. MonsterSize.Small => 0.4f, // Smaller creatures
  176. MonsterSize.Medium => 0.58f, // Standard size (was the default for agents)
  177. MonsterSize.Large => 1.2f, // Large creatures take up more space
  178. _ => 0.58f
  179. };
  180. go.transform.localScale = Vector3.one * (cellSize * sizeScale);
  181. var sr = go.AddComponent<SpriteRenderer>();
  182. sr.sprite = RoomView.SharedSquareSprite;
  183. sr.color = combatant.Color;
  184. sr.sortingOrder = 2;
  185. return new Slot
  186. {
  187. Combatant = combatant,
  188. Transform = go.transform,
  189. Sprite = sr,
  190. Home = position,
  191. CurrentGridPos = position, // Initialize to the actual starting position from the grid
  192. DisplayHealth = combatant.StartHealth
  193. };
  194. }
  195. private void Teardown()
  196. {
  197. foreach (var cam in _suspendedCameras)
  198. if (cam != null) cam.enabled = true;
  199. _suspendedCameras.Clear();
  200. _slots.Clear();
  201. _floaters.Clear();
  202. _camera = null;
  203. _root = null;
  204. if (_sceneLoaded && _scene.IsValid() && _scene.isLoaded)
  205. SceneManager.UnloadSceneAsync(_scene);
  206. _sceneLoaded = false;
  207. _fight = null;
  208. _result = null;
  209. }
  210. // ---------------------------------------------------------------- playback
  211. private IEnumerator Replay()
  212. {
  213. foreach (var e in _result.Events)
  214. {
  215. if (_skipRequested || _returnRequested) break;
  216. Feed(e.Text);
  217. switch (e.Kind)
  218. {
  219. case CombatEventKind.MovementStart:
  220. // Movement is handled in MovementComplete, this is just flavor text
  221. yield return Hold(0.2f);
  222. break;
  223. case CombatEventKind.MovementComplete:
  224. yield return Movement(e);
  225. break;
  226. case CombatEventKind.Hit:
  227. case CombatEventKind.Miss:
  228. yield return Strike(e);
  229. break;
  230. case CombatEventKind.Death:
  231. yield return Fell(e.TargetId);
  232. break;
  233. case CombatEventKind.OutOfRange:
  234. case CombatEventKind.TooFarForMelee:
  235. case CombatEventKind.TargetUnreachable:
  236. // Visual feedback for failure: brief shake or pulse
  237. yield return RangeFailure(e.ActorId);
  238. break;
  239. case CombatEventKind.RoundStart:
  240. _round = e.Round;
  241. yield return Hold(0.6f);
  242. break;
  243. case CombatEventKind.HealthPotionUsed:
  244. yield return HealPotion(e);
  245. break;
  246. default:
  247. yield return Hold(0.3f);
  248. break;
  249. }
  250. }
  251. }
  252. private IEnumerator Strike(CombatEvent e)
  253. {
  254. if (!_slots.TryGetValue(e.ActorId, out Slot actor) || !_slots.TryGetValue(e.TargetId, out Slot target))
  255. {
  256. yield return Hold(0.5f);
  257. yield break;
  258. }
  259. // Use actual grid positions from the event
  260. Vector3 actorPos = actor.CurrentGridPos;
  261. Vector3 targetPos = target.CurrentGridPos;
  262. // Lunge toward target (halfway)
  263. Vector3 lunge = Vector3.Lerp(actorPos, targetPos, 0.4f);
  264. yield return Glide(actor, actorPos, lunge, 0.35f);
  265. if (e.Kind == CombatEventKind.Hit)
  266. {
  267. target.DisplayHealth = Mathf.Max(0f, target.DisplayHealth - e.Amount);
  268. AddFloater(target, $"-{e.Amount:0.#}", new Color(1f, 0.45f, 0.4f));
  269. yield return Flash(target, new Color(1f, 0.35f, 0.35f));
  270. }
  271. else
  272. {
  273. AddFloater(target, "miss", new Color(0.85f, 0.85f, 0.85f));
  274. yield return Flash(target, Color.white);
  275. }
  276. // Return to grid position
  277. yield return Glide(actor, lunge, actorPos, 0.25f);
  278. }
  279. /// <summary>
  280. /// Animate a combatant moving across the grid to a new position.
  281. /// </summary>
  282. private IEnumerator Movement(CombatEvent e)
  283. {
  284. if (!_slots.TryGetValue(e.ActorId, out Slot actor))
  285. {
  286. yield return Hold(0.3f);
  287. yield break;
  288. }
  289. // Convert grid positions to world positions
  290. Vector3 fromWorld = CellToWorld(e.SourcePosition.x, e.SourcePosition.y);
  291. Vector3 toWorld = CellToWorld(e.TargetPosition.x, e.TargetPosition.y);
  292. // Update the slot's current grid position
  293. actor.CurrentGridPos = toWorld;
  294. // Animate movement
  295. yield return Glide(actor, fromWorld, toWorld, 0.6f);
  296. }
  297. /// <summary>
  298. /// Visual feedback when an attack fails due to range.
  299. /// Brief shake/pulse effect.
  300. /// </summary>
  301. private IEnumerator RangeFailure(int combatantId)
  302. {
  303. if (!_slots.TryGetValue(combatantId, out Slot actor))
  304. {
  305. yield return Hold(0.3f);
  306. yield break;
  307. }
  308. Vector3 startPos = actor.Transform.position;
  309. Color startColor = actor.Sprite.color;
  310. // Brief shake
  311. for (int i = 0; i < 3; i++)
  312. {
  313. actor.Transform.position = startPos + Vector3.right * (cellSize * 0.15f);
  314. yield return Hold(0.15f);
  315. actor.Transform.position = startPos - Vector3.right * (cellSize * 0.15f);
  316. yield return Hold(0.15f);
  317. }
  318. actor.Transform.position = startPos;
  319. actor.Sprite.color = startColor;
  320. yield return Hold(0.2f);
  321. }
  322. /// <summary>
  323. /// Animate a combatant using a health potion.
  324. /// </summary>
  325. private IEnumerator HealPotion(CombatEvent e)
  326. {
  327. if (!_slots.TryGetValue(e.ActorId, out Slot actor))
  328. {
  329. yield return Hold(0.4f);
  330. yield break;
  331. }
  332. // Update displayed health
  333. actor.DisplayHealth = Mathf.Min(actor.Combatant.Sheet.Stats.MaxHealth,
  334. actor.DisplayHealth + e.Amount);
  335. // Green flash for healing
  336. Color original = actor.Sprite.color;
  337. actor.Sprite.color = new Color(0.5f, 1f, 0.5f);
  338. // Green floating text
  339. AddFloater(actor, $"+{e.Amount:0.#}", new Color(0.5f, 1f, 0.5f));
  340. yield return Hold(0.35f);
  341. actor.Sprite.color = original;
  342. yield return Hold(0.25f);
  343. }
  344. private IEnumerator Fell(int combatantId)
  345. {
  346. if (!_slots.TryGetValue(combatantId, out Slot slot)) yield break;
  347. slot.Felled = true;
  348. slot.DisplayHealth = 0f;
  349. Vector3 startScale = slot.Transform.localScale;
  350. Color startColor = slot.Sprite.color;
  351. float duration = Beat * 0.9f;
  352. for (float t = 0f; t < duration; t += Time.deltaTime)
  353. {
  354. if (_returnRequested) break;
  355. float k = Mathf.Clamp01(t / duration);
  356. slot.Transform.localScale = Vector3.Lerp(startScale, startScale * 0.35f, k);
  357. slot.Sprite.color = new Color(startColor.r * 0.4f, startColor.g * 0.4f, startColor.b * 0.4f, 1f - 0.65f * k);
  358. slot.Transform.rotation = Quaternion.Euler(0f, 0f, 90f * k);
  359. yield return null;
  360. }
  361. slot.Transform.localScale = startScale * 0.35f;
  362. slot.Sprite.color = new Color(startColor.r * 0.4f, startColor.g * 0.4f, startColor.b * 0.4f, 0.35f);
  363. slot.Transform.rotation = Quaternion.Euler(0f, 0f, 90f);
  364. }
  365. private IEnumerator Glide(Slot slot, Vector3 from, Vector3 to, float beats)
  366. {
  367. float duration = Beat * beats;
  368. for (float t = 0f; t < duration; t += Time.deltaTime)
  369. {
  370. if (_skipRequested || _returnRequested) break;
  371. slot.Transform.position = Vector3.Lerp(from, to, Mathf.Clamp01(t / duration));
  372. yield return null;
  373. }
  374. slot.Transform.position = to;
  375. }
  376. private IEnumerator Flash(Slot slot, Color flashColor)
  377. {
  378. Color original = slot.Sprite.color;
  379. slot.Sprite.color = flashColor;
  380. yield return Hold(0.35f);
  381. if (!slot.Felled) slot.Sprite.color = original;
  382. }
  383. private IEnumerator Hold(float beats)
  384. {
  385. float duration = Beat * beats;
  386. for (float t = 0f; t < duration; t += Time.deltaTime)
  387. {
  388. if (_skipRequested || _returnRequested) yield break;
  389. yield return null;
  390. }
  391. }
  392. /// <summary>
  393. /// Snaps every combatant to the true final state of the decided battle, so a skipped or
  394. /// interrupted playback still ends on an accurate picture.
  395. /// </summary>
  396. private void ApplyFinalState()
  397. {
  398. foreach (var slot in _slots.Values)
  399. {
  400. slot.DisplayHealth = slot.Combatant.Sheet.Stats.CurrentHealth;
  401. // Use final grid position from the combatant
  402. slot.Transform.position = CellToWorld(slot.Combatant.GridPosition.x, slot.Combatant.GridPosition.y);
  403. if (!slot.Combatant.IsAlive && !slot.Felled)
  404. {
  405. slot.Felled = true;
  406. Color c = slot.Sprite.color;
  407. slot.Sprite.color = new Color(c.r * 0.4f, c.g * 0.4f, c.b * 0.4f, 0.35f);
  408. slot.Transform.localScale *= 0.35f;
  409. slot.Transform.rotation = Quaternion.Euler(0f, 0f, 90f);
  410. }
  411. else if (!slot.Felled)
  412. {
  413. slot.Sprite.color = slot.Combatant.Color;
  414. }
  415. }
  416. if (_skipRequested)
  417. Feed("(skipped to the end)");
  418. Feed(_result.Summary);
  419. }
  420. private IEnumerator WaitForObserver()
  421. {
  422. float waited = 0f;
  423. while (!_returnRequested)
  424. {
  425. if (autoReturnSeconds > 0f && waited >= autoReturnSeconds) break;
  426. waited += Time.deltaTime;
  427. yield return null;
  428. }
  429. }
  430. // ---------------------------------------------------------------- helpers & HUD
  431. private Vector3 CellToWorld(int x, int y)
  432. {
  433. float ox = (x - (gridSize.x - 1) * 0.5f) * cellSize;
  434. float oy = (y - (gridSize.y - 1) * 0.5f) * cellSize;
  435. return new Vector3(arenaWorldOrigin.x + ox, arenaWorldOrigin.y + oy, 0f);
  436. }
  437. private void AddFloater(Slot slot, string text, Color color)
  438. {
  439. _floaters.Add(new Floater
  440. {
  441. WorldPosition = slot.Transform.position + Vector3.up * (cellSize * 0.5f),
  442. Text = text,
  443. Color = color,
  444. BornAt = Time.time
  445. });
  446. }
  447. private void Feed(string line)
  448. {
  449. if (string.IsNullOrEmpty(line)) return;
  450. _feed.Add(line);
  451. if (_feed.Count > 200) _feed.RemoveAt(0);
  452. _feedScroll.y = float.MaxValue; // keep pinned to the newest line
  453. }
  454. private void OnGUI()
  455. {
  456. if (!_sceneLoaded || _camera == null || _fight == null) return;
  457. DrawNameplates();
  458. DrawFloaters();
  459. DrawPanel();
  460. }
  461. private void DrawNameplates()
  462. {
  463. var style = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontSize = 11 };
  464. foreach (var slot in _slots.Values)
  465. {
  466. Vector3 screen = _camera.WorldToScreenPoint(slot.Transform.position);
  467. if (screen.z < 0f) continue;
  468. float max = slot.Combatant.Sheet.Stats.MaxHealth;
  469. float fraction = Mathf.Clamp01(max > 0f ? slot.DisplayHealth / max : 0f);
  470. var rect = new Rect(screen.x - 55f, Screen.height - screen.y - 44f, 110f, 30f);
  471. GUI.color = slot.Felled ? new Color(0.6f, 0.6f, 0.6f) : Color.white;
  472. GUI.Label(rect, $"{slot.Combatant.Name}\n{slot.DisplayHealth:0}/{max:0}", style);
  473. GUI.color = Color.white;
  474. // Health bar under the nameplate.
  475. var barBack = new Rect(screen.x - 26f, Screen.height - screen.y - 16f, 52f, 5f);
  476. GUI.color = new Color(0f, 0f, 0f, 0.6f);
  477. GUI.DrawTexture(barBack, Texture2D.whiteTexture);
  478. GUI.color = slot.Combatant.IsAgent ? new Color(0.4f, 0.9f, 0.5f) : new Color(0.9f, 0.4f, 0.4f);
  479. GUI.DrawTexture(new Rect(barBack.x, barBack.y, barBack.width * fraction, barBack.height), Texture2D.whiteTexture);
  480. GUI.color = Color.white;
  481. }
  482. }
  483. private void DrawFloaters()
  484. {
  485. const float lifetime = 1.1f;
  486. var style = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontStyle = FontStyle.Bold };
  487. for (int i = _floaters.Count - 1; i >= 0; i--)
  488. {
  489. Floater floater = _floaters[i];
  490. float age = Time.time - floater.BornAt;
  491. if (age > lifetime)
  492. {
  493. _floaters.RemoveAt(i);
  494. continue;
  495. }
  496. Vector3 screen = _camera.WorldToScreenPoint(floater.WorldPosition);
  497. if (screen.z < 0f) continue;
  498. GUI.color = new Color(floater.Color.r, floater.Color.g, floater.Color.b, 1f - age / lifetime);
  499. GUI.Label(new Rect(screen.x - 40f, Screen.height - screen.y - 30f - age * 25f, 80f, 20f), floater.Text, style);
  500. GUI.color = Color.white;
  501. }
  502. }
  503. private void DrawPanel()
  504. {
  505. const float width = 380f;
  506. GUILayout.BeginArea(new Rect(Screen.width - width - 10f, 10f, width, Screen.height - 20f), GUI.skin.box);
  507. GUILayout.Label($"FIGHT #{_fight.Id} - room {_fight.Room.Id}");
  508. GUILayout.Label(_playbackFinished
  509. ? _result.Summary
  510. : $"Round {_round} / max - {_fight.Agents.Count} agent(s) vs {_result.Combatants.Count - _fight.Agents.Count} monster(s)");
  511. GUILayout.BeginHorizontal();
  512. GUILayout.Label("Speed", GUILayout.Width(45f));
  513. playbackSpeed = GUILayout.HorizontalSlider(playbackSpeed, 0.25f, 8f);
  514. GUILayout.Label($"{playbackSpeed:0.0}x", GUILayout.Width(40f));
  515. GUILayout.EndHorizontal();
  516. if (!_playbackFinished)
  517. {
  518. if (GUILayout.Button("Skip to end")) _skipRequested = true;
  519. }
  520. else if (GUILayout.Button("Return to the maze"))
  521. {
  522. _returnRequested = true;
  523. }
  524. GUILayout.Space(6f);
  525. GUILayout.Label("Blow by blow:");
  526. _feedScroll = GUILayout.BeginScrollView(_feedScroll);
  527. foreach (var line in _feed)
  528. GUILayout.Label(line);
  529. GUILayout.EndScrollView();
  530. GUILayout.EndArea();
  531. }
  532. }
  533. }