|
|
@@ -0,0 +1,525 @@
|
|
|
+using System.Collections;
|
|
|
+using System.Collections.Generic;
|
|
|
+using UnityEngine;
|
|
|
+using UnityEngine.SceneManagement;
|
|
|
+
|
|
|
+namespace MazeGen
|
|
|
+{
|
|
|
+ /// <summary>
|
|
|
+ /// Plays a fight out on a grid in its own additively-loaded scene, then throws that scene
|
|
|
+ /// away and hands the camera back to the maze.
|
|
|
+ ///
|
|
|
+ /// Why this can't corrupt the maze: the fight's outcome is already fully decided before
|
|
|
+ /// playback starts (see <see cref="CombatResolver"/>), so this class is pure animation over a
|
|
|
+ /// finished <see cref="BattleResult"/> - it never touches a character sheet, a room, or an
|
|
|
+ /// agent. Health bars run off a private display copy of each combatant's health. And because
|
|
|
+ /// everything it creates lives in a separate scene which is unloaded on exit, there's nothing
|
|
|
+ /// left behind to restore. The maze's own GameObjects are never disabled or re-created (which
|
|
|
+ /// would kill the spawn coroutines), only hidden by suspending the maze camera; when the fight
|
|
|
+ /// ends every agent is exactly where it was, and only the fight's result has changed.
|
|
|
+ ///
|
|
|
+ /// Runs on real Unity time, because the maze simulation is frozen via SimulationClock (not
|
|
|
+ /// Time.timeScale) precisely so that this playback can still animate.
|
|
|
+ /// </summary>
|
|
|
+ [DisallowMultipleComponent]
|
|
|
+ public class BattleArenaView : MonoBehaviour
|
|
|
+ {
|
|
|
+ [Header("Arena")]
|
|
|
+ [Tooltip("Grid dimensions (columns x rows) of the battle floor.")]
|
|
|
+ [SerializeField] private Vector2Int gridSize = new Vector2Int(11, 7);
|
|
|
+ [SerializeField] private float cellSize = 1f;
|
|
|
+ [Tooltip("Where in world space the arena scene is built. Far from the maze so the arena camera can't see maze geometry.")]
|
|
|
+ [SerializeField] private Vector2 arenaWorldOrigin = new Vector2(0f, 10000f);
|
|
|
+
|
|
|
+ [Header("Playback")]
|
|
|
+ [Tooltip("Base tempo: seconds per combat event at 1x speed.")]
|
|
|
+ [SerializeField, Min(0.02f)] private float secondsPerEvent = 0.4f;
|
|
|
+ [SerializeField, Range(0.25f, 8f)] private float playbackSpeed = 1f;
|
|
|
+ [Tooltip("Seconds the final result stays on screen before auto-returning to the maze (0 = wait for the button).")]
|
|
|
+ [SerializeField, Min(0f)] private float autoReturnSeconds = 4f;
|
|
|
+
|
|
|
+ /// <summary>Per-combatant playback state. DisplayHealth is a copy - never the real sheet.</summary>
|
|
|
+ private class Slot
|
|
|
+ {
|
|
|
+ public Combatant Combatant;
|
|
|
+ public Transform Transform;
|
|
|
+ public SpriteRenderer Sprite;
|
|
|
+ public Vector3 Home;
|
|
|
+ public float DisplayHealth;
|
|
|
+ public bool Felled;
|
|
|
+ }
|
|
|
+
|
|
|
+ private struct Floater
|
|
|
+ {
|
|
|
+ public Vector3 WorldPosition;
|
|
|
+ public string Text;
|
|
|
+ public Color Color;
|
|
|
+ public float BornAt;
|
|
|
+ }
|
|
|
+
|
|
|
+ private readonly Dictionary<int, Slot> _slots = new Dictionary<int, Slot>();
|
|
|
+ private readonly List<Floater> _floaters = new List<Floater>();
|
|
|
+ private readonly List<string> _feed = new List<string>();
|
|
|
+ private readonly List<Camera> _suspendedCameras = new List<Camera>();
|
|
|
+
|
|
|
+ private Scene _scene;
|
|
|
+ private bool _sceneLoaded;
|
|
|
+ private GameObject _root;
|
|
|
+ private Camera _camera;
|
|
|
+ private PendingFight _fight;
|
|
|
+ private BattleResult _result;
|
|
|
+ private int _round;
|
|
|
+ private bool _playbackFinished;
|
|
|
+ private bool _skipRequested;
|
|
|
+ private bool _returnRequested;
|
|
|
+ private Vector2 _feedScroll;
|
|
|
+
|
|
|
+ public bool IsBuilt => _sceneLoaded;
|
|
|
+
|
|
|
+ /// <summary>Seconds per animation "beat" at the current playback speed.</summary>
|
|
|
+ private float Beat => Mathf.Max(0.02f, secondsPerEvent) / Mathf.Max(0.25f, playbackSpeed);
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Builds the arena, replays the fight, waits for the observer, and tears everything back
|
|
|
+ /// down. Yield on this from a coroutine; when it completes the maze is on screen again.
|
|
|
+ /// </summary>
|
|
|
+ public IEnumerator Play(PendingFight fight, IReadOnlyList<Combatant> combatants)
|
|
|
+ {
|
|
|
+ _fight = fight;
|
|
|
+ _result = fight.Result;
|
|
|
+ _skipRequested = false;
|
|
|
+ _returnRequested = false;
|
|
|
+ _playbackFinished = false;
|
|
|
+ _round = 0;
|
|
|
+ _feed.Clear();
|
|
|
+ _floaters.Clear();
|
|
|
+
|
|
|
+ Build(combatants);
|
|
|
+
|
|
|
+ yield return Replay();
|
|
|
+
|
|
|
+ ApplyFinalState();
|
|
|
+ _playbackFinished = true;
|
|
|
+
|
|
|
+ yield return WaitForObserver();
|
|
|
+
|
|
|
+ Teardown();
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------------------------------------------------------------- build / teardown
|
|
|
+
|
|
|
+ private void Build(IReadOnlyList<Combatant> combatants)
|
|
|
+ {
|
|
|
+ // A per-fight scene name: CreateScene refuses to make a second scene with a name
|
|
|
+ // that's already loaded, and a previous arena may still be unloading.
|
|
|
+ _scene = SceneManager.CreateScene($"CombatArena_{_fight.Id}_{Time.frameCount}");
|
|
|
+ _sceneLoaded = true;
|
|
|
+
|
|
|
+ _root = new GameObject("ArenaRoot");
|
|
|
+ SceneManager.MoveGameObjectToScene(_root, _scene);
|
|
|
+ _root.transform.position = arenaWorldOrigin;
|
|
|
+
|
|
|
+ BuildFloor();
|
|
|
+ BuildCamera();
|
|
|
+ PlaceCombatants(combatants);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void BuildFloor()
|
|
|
+ {
|
|
|
+ var floor = new GameObject("Floor");
|
|
|
+ floor.transform.SetParent(_root.transform, false);
|
|
|
+
|
|
|
+ for (int x = 0; x < gridSize.x; x++)
|
|
|
+ {
|
|
|
+ for (int y = 0; y < gridSize.y; y++)
|
|
|
+ {
|
|
|
+ var tile = new GameObject($"Cell_{x}_{y}");
|
|
|
+ tile.transform.SetParent(floor.transform, false);
|
|
|
+ tile.transform.position = CellToWorld(x, y);
|
|
|
+ tile.transform.localScale = Vector3.one * (cellSize * 0.96f);
|
|
|
+
|
|
|
+ var sr = tile.AddComponent<SpriteRenderer>();
|
|
|
+ sr.sprite = RoomView.SharedSquareSprite;
|
|
|
+ sr.color = (x + y) % 2 == 0 ? new Color(0.16f, 0.17f, 0.2f) : new Color(0.21f, 0.22f, 0.26f);
|
|
|
+ sr.sortingOrder = 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void BuildCamera()
|
|
|
+ {
|
|
|
+ var cameraGO = new GameObject("ArenaCamera");
|
|
|
+ cameraGO.transform.SetParent(_root.transform, false);
|
|
|
+ cameraGO.transform.position = new Vector3(arenaWorldOrigin.x, arenaWorldOrigin.y, -10f);
|
|
|
+
|
|
|
+ _camera = cameraGO.AddComponent<Camera>();
|
|
|
+ _camera.orthographic = true;
|
|
|
+ _camera.backgroundColor = new Color(0.05f, 0.05f, 0.07f);
|
|
|
+ _camera.clearFlags = CameraClearFlags.SolidColor;
|
|
|
+ _camera.depth = 100f; // draw over anything else that somehow stays enabled
|
|
|
+
|
|
|
+ float aspect = Mathf.Max(_camera.aspect, 0.01f);
|
|
|
+ float halfHeight = gridSize.y * cellSize * 0.5f;
|
|
|
+ float halfWidth = gridSize.x * cellSize * 0.5f;
|
|
|
+ _camera.orthographicSize = Mathf.Max(halfHeight, halfWidth / aspect) * 1.35f;
|
|
|
+
|
|
|
+ // Hide the maze by suspending its cameras rather than deactivating maze objects -
|
|
|
+ // SetActive(false) on the maze root would permanently kill its spawn coroutines.
|
|
|
+ _suspendedCameras.Clear();
|
|
|
+ foreach (var cam in Camera.allCameras)
|
|
|
+ {
|
|
|
+ if (cam == _camera) continue;
|
|
|
+ _suspendedCameras.Add(cam);
|
|
|
+ cam.enabled = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>Agents line up on the left of the grid, monsters on the right, facing off.</summary>
|
|
|
+ private void PlaceCombatants(IReadOnlyList<Combatant> combatants)
|
|
|
+ {
|
|
|
+ _slots.Clear();
|
|
|
+
|
|
|
+ var agents = new List<Combatant>();
|
|
|
+ var monsters = new List<Combatant>();
|
|
|
+ foreach (var c in combatants)
|
|
|
+ (c.Team == Combatant.TeamAgents ? agents : monsters).Add(c);
|
|
|
+
|
|
|
+ LayoutTeam(agents, true);
|
|
|
+ LayoutTeam(monsters, false);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void LayoutTeam(List<Combatant> team, bool leftSide)
|
|
|
+ {
|
|
|
+ int rows = Mathf.Max(1, gridSize.y);
|
|
|
+
|
|
|
+ for (int i = 0; i < team.Count; i++)
|
|
|
+ {
|
|
|
+ int column = i / rows;
|
|
|
+ int indexInColumn = i % rows;
|
|
|
+ int countInColumn = Mathf.Min(rows, team.Count - column * rows);
|
|
|
+
|
|
|
+ int x = leftSide ? Mathf.Min(column, gridSize.x - 1) : Mathf.Max(0, gridSize.x - 1 - column);
|
|
|
+ int y = (rows - countInColumn) / 2 + indexInColumn;
|
|
|
+
|
|
|
+ _slots[team[i].Id] = CreateSlot(team[i], CellToWorld(x, y));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private Slot CreateSlot(Combatant combatant, Vector3 position)
|
|
|
+ {
|
|
|
+ var go = new GameObject($"{(combatant.IsAgent ? "Agent" : "Monster")}_{combatant.Name}");
|
|
|
+ go.transform.SetParent(_root.transform, false);
|
|
|
+ go.transform.position = position;
|
|
|
+ go.transform.localScale = Vector3.one * (cellSize * (combatant.IsAgent ? 0.58f : 0.5f));
|
|
|
+
|
|
|
+ var sr = go.AddComponent<SpriteRenderer>();
|
|
|
+ sr.sprite = RoomView.SharedSquareSprite;
|
|
|
+ sr.color = combatant.Color;
|
|
|
+ sr.sortingOrder = 2;
|
|
|
+
|
|
|
+ return new Slot
|
|
|
+ {
|
|
|
+ Combatant = combatant,
|
|
|
+ Transform = go.transform,
|
|
|
+ Sprite = sr,
|
|
|
+ Home = position,
|
|
|
+ DisplayHealth = combatant.StartHealth
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private void Teardown()
|
|
|
+ {
|
|
|
+ foreach (var cam in _suspendedCameras)
|
|
|
+ if (cam != null) cam.enabled = true;
|
|
|
+ _suspendedCameras.Clear();
|
|
|
+
|
|
|
+ _slots.Clear();
|
|
|
+ _floaters.Clear();
|
|
|
+ _camera = null;
|
|
|
+ _root = null;
|
|
|
+
|
|
|
+ if (_sceneLoaded && _scene.IsValid() && _scene.isLoaded)
|
|
|
+ SceneManager.UnloadSceneAsync(_scene);
|
|
|
+
|
|
|
+ _sceneLoaded = false;
|
|
|
+ _fight = null;
|
|
|
+ _result = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------------------------------------------------------------- playback
|
|
|
+
|
|
|
+ private IEnumerator Replay()
|
|
|
+ {
|
|
|
+ foreach (var e in _result.Events)
|
|
|
+ {
|
|
|
+ if (_skipRequested || _returnRequested) break;
|
|
|
+
|
|
|
+ Feed(e.Text);
|
|
|
+
|
|
|
+ switch (e.Kind)
|
|
|
+ {
|
|
|
+ case CombatEventKind.Hit:
|
|
|
+ case CombatEventKind.Miss:
|
|
|
+ yield return Strike(e);
|
|
|
+ break;
|
|
|
+ case CombatEventKind.Death:
|
|
|
+ yield return Fell(e.TargetId);
|
|
|
+ break;
|
|
|
+ case CombatEventKind.RoundStart:
|
|
|
+ _round = e.Round;
|
|
|
+ yield return Hold(0.6f);
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ yield return Hold(1.2f);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private IEnumerator Strike(CombatEvent e)
|
|
|
+ {
|
|
|
+ if (!_slots.TryGetValue(e.ActorId, out Slot actor) || !_slots.TryGetValue(e.TargetId, out Slot target))
|
|
|
+ {
|
|
|
+ yield return Hold(0.5f);
|
|
|
+ yield break;
|
|
|
+ }
|
|
|
+
|
|
|
+ Vector3 lunge = Vector3.Lerp(actor.Home, target.Home, 0.5f);
|
|
|
+ yield return Glide(actor, actor.Home, lunge, 0.45f);
|
|
|
+
|
|
|
+ if (e.Kind == CombatEventKind.Hit)
|
|
|
+ {
|
|
|
+ target.DisplayHealth = Mathf.Max(0f, target.DisplayHealth - e.Amount);
|
|
|
+ AddFloater(target, $"-{e.Amount:0.#}", new Color(1f, 0.45f, 0.4f));
|
|
|
+ yield return Flash(target, new Color(1f, 0.35f, 0.35f));
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ AddFloater(target, "miss", new Color(0.85f, 0.85f, 0.85f));
|
|
|
+ yield return Flash(target, Color.white);
|
|
|
+ }
|
|
|
+
|
|
|
+ yield return Glide(actor, lunge, actor.Home, 0.3f);
|
|
|
+ }
|
|
|
+
|
|
|
+ private IEnumerator Fell(int combatantId)
|
|
|
+ {
|
|
|
+ if (!_slots.TryGetValue(combatantId, out Slot slot)) yield break;
|
|
|
+
|
|
|
+ slot.Felled = true;
|
|
|
+ slot.DisplayHealth = 0f;
|
|
|
+
|
|
|
+ Vector3 startScale = slot.Transform.localScale;
|
|
|
+ Color startColor = slot.Sprite.color;
|
|
|
+ float duration = Beat * 0.9f;
|
|
|
+
|
|
|
+ for (float t = 0f; t < duration; t += Time.deltaTime)
|
|
|
+ {
|
|
|
+ if (_returnRequested) break;
|
|
|
+ float k = Mathf.Clamp01(t / duration);
|
|
|
+ slot.Transform.localScale = Vector3.Lerp(startScale, startScale * 0.35f, k);
|
|
|
+ slot.Sprite.color = new Color(startColor.r * 0.4f, startColor.g * 0.4f, startColor.b * 0.4f, 1f - 0.65f * k);
|
|
|
+ slot.Transform.rotation = Quaternion.Euler(0f, 0f, 90f * k);
|
|
|
+ yield return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ slot.Transform.localScale = startScale * 0.35f;
|
|
|
+ slot.Sprite.color = new Color(startColor.r * 0.4f, startColor.g * 0.4f, startColor.b * 0.4f, 0.35f);
|
|
|
+ slot.Transform.rotation = Quaternion.Euler(0f, 0f, 90f);
|
|
|
+ }
|
|
|
+
|
|
|
+ private IEnumerator Glide(Slot slot, Vector3 from, Vector3 to, float beats)
|
|
|
+ {
|
|
|
+ float duration = Beat * beats;
|
|
|
+ for (float t = 0f; t < duration; t += Time.deltaTime)
|
|
|
+ {
|
|
|
+ if (_skipRequested || _returnRequested) break;
|
|
|
+ slot.Transform.position = Vector3.Lerp(from, to, Mathf.Clamp01(t / duration));
|
|
|
+ yield return null;
|
|
|
+ }
|
|
|
+ slot.Transform.position = to;
|
|
|
+ }
|
|
|
+
|
|
|
+ private IEnumerator Flash(Slot slot, Color flashColor)
|
|
|
+ {
|
|
|
+ Color original = slot.Sprite.color;
|
|
|
+ slot.Sprite.color = flashColor;
|
|
|
+ yield return Hold(0.35f);
|
|
|
+ if (!slot.Felled) slot.Sprite.color = original;
|
|
|
+ }
|
|
|
+
|
|
|
+ private IEnumerator Hold(float beats)
|
|
|
+ {
|
|
|
+ float duration = Beat * beats;
|
|
|
+ for (float t = 0f; t < duration; t += Time.deltaTime)
|
|
|
+ {
|
|
|
+ if (_skipRequested || _returnRequested) yield break;
|
|
|
+ yield return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Snaps every combatant to the true final state of the decided battle, so a skipped or
|
|
|
+ /// interrupted playback still ends on an accurate picture.
|
|
|
+ /// </summary>
|
|
|
+ private void ApplyFinalState()
|
|
|
+ {
|
|
|
+ foreach (var slot in _slots.Values)
|
|
|
+ {
|
|
|
+ slot.DisplayHealth = slot.Combatant.Sheet.Stats.CurrentHealth;
|
|
|
+ slot.Transform.position = slot.Home;
|
|
|
+
|
|
|
+ if (!slot.Combatant.IsAlive && !slot.Felled)
|
|
|
+ {
|
|
|
+ slot.Felled = true;
|
|
|
+ Color c = slot.Sprite.color;
|
|
|
+ slot.Sprite.color = new Color(c.r * 0.4f, c.g * 0.4f, c.b * 0.4f, 0.35f);
|
|
|
+ slot.Transform.localScale *= 0.35f;
|
|
|
+ slot.Transform.rotation = Quaternion.Euler(0f, 0f, 90f);
|
|
|
+ }
|
|
|
+ else if (!slot.Felled)
|
|
|
+ {
|
|
|
+ slot.Sprite.color = slot.Combatant.Color;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (_skipRequested)
|
|
|
+ Feed("(skipped to the end)");
|
|
|
+ Feed(_result.Summary);
|
|
|
+ }
|
|
|
+
|
|
|
+ private IEnumerator WaitForObserver()
|
|
|
+ {
|
|
|
+ float waited = 0f;
|
|
|
+ while (!_returnRequested)
|
|
|
+ {
|
|
|
+ if (autoReturnSeconds > 0f && waited >= autoReturnSeconds) break;
|
|
|
+ waited += Time.deltaTime;
|
|
|
+ yield return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------------------------------------------------------------- helpers & HUD
|
|
|
+
|
|
|
+ private Vector3 CellToWorld(int x, int y)
|
|
|
+ {
|
|
|
+ float ox = (x - (gridSize.x - 1) * 0.5f) * cellSize;
|
|
|
+ float oy = (y - (gridSize.y - 1) * 0.5f) * cellSize;
|
|
|
+ return new Vector3(arenaWorldOrigin.x + ox, arenaWorldOrigin.y + oy, 0f);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void AddFloater(Slot slot, string text, Color color)
|
|
|
+ {
|
|
|
+ _floaters.Add(new Floater
|
|
|
+ {
|
|
|
+ WorldPosition = slot.Home + Vector3.up * (cellSize * 0.5f),
|
|
|
+ Text = text,
|
|
|
+ Color = color,
|
|
|
+ BornAt = Time.time
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private void Feed(string line)
|
|
|
+ {
|
|
|
+ if (string.IsNullOrEmpty(line)) return;
|
|
|
+ _feed.Add(line);
|
|
|
+ if (_feed.Count > 200) _feed.RemoveAt(0);
|
|
|
+ _feedScroll.y = float.MaxValue; // keep pinned to the newest line
|
|
|
+ }
|
|
|
+
|
|
|
+ private void OnGUI()
|
|
|
+ {
|
|
|
+ if (!_sceneLoaded || _camera == null || _fight == null) return;
|
|
|
+
|
|
|
+ DrawNameplates();
|
|
|
+ DrawFloaters();
|
|
|
+ DrawPanel();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void DrawNameplates()
|
|
|
+ {
|
|
|
+ var style = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontSize = 11 };
|
|
|
+
|
|
|
+ foreach (var slot in _slots.Values)
|
|
|
+ {
|
|
|
+ Vector3 screen = _camera.WorldToScreenPoint(slot.Home);
|
|
|
+ if (screen.z < 0f) continue;
|
|
|
+
|
|
|
+ float max = slot.Combatant.MaxHealth;
|
|
|
+ float fraction = Mathf.Clamp01(max > 0f ? slot.DisplayHealth / max : 0f);
|
|
|
+ var rect = new Rect(screen.x - 55f, Screen.height - screen.y - 44f, 110f, 30f);
|
|
|
+
|
|
|
+ GUI.color = slot.Felled ? new Color(0.6f, 0.6f, 0.6f) : Color.white;
|
|
|
+ GUI.Label(rect, $"{slot.Combatant.Name}\n{slot.DisplayHealth:0}/{max:0}", style);
|
|
|
+ GUI.color = Color.white;
|
|
|
+
|
|
|
+ // Health bar under the nameplate.
|
|
|
+ var barBack = new Rect(screen.x - 26f, Screen.height - screen.y - 16f, 52f, 5f);
|
|
|
+ GUI.color = new Color(0f, 0f, 0f, 0.6f);
|
|
|
+ GUI.DrawTexture(barBack, Texture2D.whiteTexture);
|
|
|
+ GUI.color = slot.Combatant.IsAgent ? new Color(0.4f, 0.9f, 0.5f) : new Color(0.9f, 0.4f, 0.4f);
|
|
|
+ GUI.DrawTexture(new Rect(barBack.x, barBack.y, barBack.width * fraction, barBack.height), Texture2D.whiteTexture);
|
|
|
+ GUI.color = Color.white;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void DrawFloaters()
|
|
|
+ {
|
|
|
+ const float lifetime = 1.1f;
|
|
|
+ var style = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontStyle = FontStyle.Bold };
|
|
|
+
|
|
|
+ for (int i = _floaters.Count - 1; i >= 0; i--)
|
|
|
+ {
|
|
|
+ Floater floater = _floaters[i];
|
|
|
+ float age = Time.time - floater.BornAt;
|
|
|
+ if (age > lifetime)
|
|
|
+ {
|
|
|
+ _floaters.RemoveAt(i);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ Vector3 screen = _camera.WorldToScreenPoint(floater.WorldPosition);
|
|
|
+ if (screen.z < 0f) continue;
|
|
|
+
|
|
|
+ GUI.color = new Color(floater.Color.r, floater.Color.g, floater.Color.b, 1f - age / lifetime);
|
|
|
+ GUI.Label(new Rect(screen.x - 40f, Screen.height - screen.y - 30f - age * 25f, 80f, 20f), floater.Text, style);
|
|
|
+ GUI.color = Color.white;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void DrawPanel()
|
|
|
+ {
|
|
|
+ const float width = 380f;
|
|
|
+ GUILayout.BeginArea(new Rect(Screen.width - width - 10f, 10f, width, Screen.height - 20f), GUI.skin.box);
|
|
|
+
|
|
|
+ GUILayout.Label($"FIGHT #{_fight.Id} - room {_fight.Room.Id}");
|
|
|
+ GUILayout.Label(_playbackFinished
|
|
|
+ ? _result.Summary
|
|
|
+ : $"Round {_round} / max - {_fight.Agents.Count} agent(s) vs {_result.Combatants.Count - _fight.Agents.Count} monster(s)");
|
|
|
+
|
|
|
+ GUILayout.BeginHorizontal();
|
|
|
+ GUILayout.Label("Speed", GUILayout.Width(45f));
|
|
|
+ playbackSpeed = GUILayout.HorizontalSlider(playbackSpeed, 0.25f, 8f);
|
|
|
+ GUILayout.Label($"{playbackSpeed:0.0}x", GUILayout.Width(40f));
|
|
|
+ GUILayout.EndHorizontal();
|
|
|
+
|
|
|
+ if (!_playbackFinished)
|
|
|
+ {
|
|
|
+ if (GUILayout.Button("Skip to end")) _skipRequested = true;
|
|
|
+ }
|
|
|
+ else if (GUILayout.Button("Return to the maze"))
|
|
|
+ {
|
|
|
+ _returnRequested = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ GUILayout.Space(6f);
|
|
|
+ GUILayout.Label("Blow by blow:");
|
|
|
+ _feedScroll = GUILayout.BeginScrollView(_feedScroll);
|
|
|
+ foreach (var line in _feed)
|
|
|
+ GUILayout.Label(line);
|
|
|
+ GUILayout.EndScrollView();
|
|
|
+
|
|
|
+ GUILayout.EndArea();
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|