Forráskód Böngészése

Fight zoom and time-controls

Axel Nordh 4 hete
szülő
commit
ed57e76d2c

+ 148 - 0
ACTIVE_FIGHTS_SYSTEM.md

@@ -0,0 +1,148 @@
+# Active Fights Display System
+
+## Overview
+A complete system for tracking, displaying, and interacting with active fights between runners (AI agents) and monsters in the maze.
+
+## Components Created
+
+### 1. **FightTracker.cs** (Core Manager)
+- **Purpose**: Tracks all active fights in real-time
+- **Key Features**:
+  - `StartFight(AIAgent runner, Monster monster)` - Called when combat begins
+  - `EndFight(AIAgent runner, Monster monster)` - Called when combat ends
+  - `GetDisplayFights()` - Returns sorted list of fights to display
+  - Automatically cleans up old fights after `fightDisplayDuration` (default: 3 seconds after ending)
+  - **Sorting**: Fights sorted by number of participants in room (descending), then by active status
+
+### 2. **ActiveFightsUIPanel.cs** (UI Display)
+- **Purpose**: Displays active fights with zoom functionality
+- **Features**:
+  - Shows runner name vs monster type
+  - Each fight entry has:
+    - Fight description text (e.g., "Alice vs Monster_12345")
+    - Status indicator (red dot = active, orange dot = ended)
+    - Zoom button to center camera on fight location
+  - Panel positioned in top-right corner
+  - Auto-updates every 0.5 seconds
+  - Clean, dark-themed UI that doesn't obstruct gameplay
+
+### 3. **MazeCameraController.cs** (Enhanced with zoom)
+- **New Method**: `ZoomToRoom(MazeRoom room, float zoomPadding = 1.3f, float transitionDuration = 0.5f)`
+- **Features**:
+  - Smooth pan to room center
+  - Smooth zoom with adjustable padding (1.3 = 30% larger than room)
+  - Adjustable transition speed (default: 0.5 seconds)
+  - Works with both orthographic and perspective cameras
+  - Bound checks for camera limits
+
+## Integration Points
+
+### AIAgent.cs
+- Added `fightingMonsters` HashSet to track active fights
+- Modified `CombatUpdate()` to:
+  - Report fight start when monster enters melee range
+  - Report fight end when monster leaves range or dies
+  - Track multiple concurrent fights
+- Modified `Die()` to end all active fights when agent dies
+
+### Monster.cs
+- Added `previousTarget` tracking
+- Modified `AcquireTarget()` to:
+  - Report fight start when acquiring new target
+  - Report fight end when target is lost/killed
+- Modified `Die()` to report fight end
+
+### MazeController.cs
+- Added `InitializeFightTracking()` in Start()
+- Automatically creates FightTracker and ActiveFightsUIPanel if not present
+- Ensures components are initialized before agents/monsters spawn
+
+## Data Structure
+
+### FightData (struct)
+```csharp
+public string runnerName;       // Agent's display name
+public int runnerId;            // Agent's ID
+public string monsterType;      // Monster identifier
+public MazeRoom room;           // Room where fight occurs
+public bool isActive;           // True if fight is ongoing
+public float timeSinceEnd;      // Time since fight ended
+public AIAgent runner;          // Reference to agent
+public Monster monster;         // Reference to monster
+```
+
+## Features
+
+✅ **Real-time Fight Tracking**
+- Fights automatically detected when agents and monsters are in melee range
+
+✅ **Smart Display**
+- Shows only active fights + recent fights (for a few seconds after they end)
+- Fights ranked by number of participants in the room (higher = more intense)
+
+✅ **Interactive Zoom**
+- Click "Zoom" button to smoothly pan and zoom camera to fight location
+- Camera zooms to 30% larger than room size for good visibility
+- Smooth 0.5-second transition
+
+✅ **Visual Feedback**
+- Red indicator dot for active fights
+- Orange indicator dot for recently-ended fights
+- Runner name vs monster type displayed
+- Clear, unobtrusive UI panel
+
+✅ **Automatic Cleanup**
+- Old fights automatically removed from display
+- Dead fighters automatically cleaned up from tracking
+
+## Configuration
+
+### FightTracker Settings
+```csharp
+[SerializeField] private float fightDisplayDuration = 3f; // How long to show ended fights
+```
+
+### Camera Zoom Settings (in MazeCameraController)
+```csharp
+[SerializeField] private float zoomMin = 5f;
+[SerializeField] private float zoomMax = 120f;
+```
+
+### UI Panel Settings (in ActiveFightsUIPanel)
+```csharp
+[SerializeField] private float updateInterval = 0.5f;  // How often to update display
+[SerializeField] private float panelMaxHeight = 400f;  // Maximum panel height
+```
+
+## Usage Example
+
+```csharp
+// FightTracker is a singleton - access it anywhere:
+FightTracker tracker = FightTracker.Instance;
+
+// Get current fights for display
+List<FightData> fights = tracker.GetDisplayFights();
+
+// Zoom to a room
+MazeCameraController camera = FindObjectOfType<MazeCameraController>();
+camera.ZoomToRoom(room, 1.3f, 0.5f); // zoom to room with 30% padding in 0.5s
+```
+
+## Debug Information
+
+The system logs the following to console:
+- When fights start (via AIAgent and Monster combat methods)
+- When fights end (via death or separation)
+- Fight tracking status in FightTracker
+
+Enable debug logging in FightTracker, AIAgent, and Monster if needed for troubleshooting.
+
+## Future Enhancements
+
+Potential additions:
+- Sound effects when fights start/end
+- Different colors/icons for different monster types
+- Fight statistics (damage dealt, healing, etc.)
+- Fight history/replay system
+- Animation transitions for UI panel entries
+- Custom zoom speeds/durations per difficulty level

+ 167 - 3
Assets/Scripts/AIAgent.cs

@@ -68,6 +68,8 @@ public class AIAgent : MonoBehaviour
     private bool isDead = false;
     /// <summary>True while a monster is within melee range – halts path-following.</summary>
     private bool isInCombat = false;
+    /// <summary>Monsters this agent is currently fighting (for fight tracking).</summary>
+    private HashSet<Monster> fightingMonsters = new();
 
     // ----- Room danger / avoidance -----
     /// <summary>Room IDs the agent has decided to avoid (too dangerous to fight through).</summary>
@@ -103,6 +105,10 @@ public class AIAgent : MonoBehaviour
         // Cache room data once – avoids List allocations and LINQ on every Update
         _allRooms = maze.Rooms.ToArray();
         _goalRooms = new HashSet<MazeRoom>(maze.GetRoomsByType(MazeRoom.RoomType.End));
+        if (_goalRooms.Count == 0)
+            Debug.LogWarning($"[Agent] No goal rooms found! Agents will never exit.");
+        else
+            Debug.Log($"[Agent] {_goalRooms.Count} goal room(s) cached for exit detection.");
 
         // Initialize personalization
         agentName = AgentNameGenerator.GenerateRandomName();
@@ -177,13 +183,27 @@ public class AIAgent : MonoBehaviour
 
     void Update()
     {
-        if (maze == null || isDead) return;
+        if (maze == null) return;
+
+        // Dead bodies persist forever - just stop updating logic
+        if (isDead)
+        {
+            return;
+        }
+
+        // Agent has reached the exit - stop all movement
+        if (hasReachedGoal)
+        {
+            return;
+        }
 
         // Followers defer all movement to the group leader
         if (isGroupFollower)
         {
             if (currentGroup != null && currentGroup.Leader != null)
                 transform.position = currentGroup.Leader.transform.position;
+            // Followers still check for exit rooms
+            UpdateCurrentRoom();
             // Followers still fight monsters in range
             CombatUpdate();
             return;
@@ -233,6 +253,7 @@ public class AIAgent : MonoBehaviour
     /// <summary>
     /// Updates the current room based on position.
     /// Only queries the maze when the agent has moved to a different tile.
+    /// Also checks if we've reached the goal room immediately.
     /// </summary>
     private void UpdateCurrentRoom()
     {
@@ -245,6 +266,30 @@ public class AIAgent : MonoBehaviour
 
         if (room != null)
         {
+            // CHECK FOR GOAL ROOM FIRST - every tile, regardless of room change
+            if (!hasReachedGoal && _goalRooms.Contains(room))
+            {
+                hasReachedGoal = true;
+                currentPath.Clear();
+                currentPathIndex = 0;
+                commitedToExit = false;
+
+                // If this agent is in a group, mark all members as having reached the goal
+                if (currentGroup != null)
+                {
+                    foreach (var member in currentGroup.Members)
+                    {
+                        if (member != null && !member.HasReachedGoal)
+                        {
+                            member.hasReachedGoal = true;
+                        }
+                    }
+                }
+
+                Debug.Log($"[Agent {agentId}] {agentName} reached the exit!");
+                return; // Stop immediately
+            }
+
             if (currentRoom.x != room.Id)
             {
                 currentRoom = new Vector2Int(room.Id, 0);
@@ -254,7 +299,6 @@ public class AIAgent : MonoBehaviour
                 recentRooms.Enqueue(room.Id);
                 if (recentRooms.Count > RECENT_ROOMS_BUFFER_SIZE)
                     recentRooms.Dequeue();
-
             }
         }
     }
@@ -1106,6 +1150,31 @@ public class AIAgent : MonoBehaviour
         if (isDead) return;
         isDead = true;
         currentPath.Clear();
+
+        // End all fights this agent is in
+        foreach (var monster in fightingMonsters)
+        {
+            if (monster != null)
+                FightTracker.Instance.EndFight(this, monster);
+        }
+        fightingMonsters.Clear();
+
+        // Change visual to show dead (grayscale/dark with white X)
+        var renderer = GetComponent<Renderer>();
+        if (renderer != null)
+        {
+            var material = new Material(renderer.material);
+            material.color = new Color(0.4f, 0.4f, 0.4f, 0.9f); // Dark gray, opaque
+            renderer.material = material;
+        }
+
+        // Add X marker BEFORE disabling (must happen while enabled)
+        AddDeadMarker();
+
+        // Disable pathfinding AFTER adding marker
+        if (pathRenderer != null)
+            pathRenderer.enabled = false;
+
         Debug.Log($"[Agent {agentId}] {agentName} has died!");
 
         // Notify the manager so death stats can be tracked
@@ -1114,8 +1183,71 @@ public class AIAgent : MonoBehaviour
 
         // Leave group cleanly
         currentGroup?.RemoveMember(this);
+    }
 
-        Destroy(gameObject, 0.1f);
+    /// <summary>
+    /// Adds a white X marker to indicate the body is dead.
+    /// Uses a child GameObject so it doesn't conflict with the path LineRenderer.
+    /// </summary>
+    private void AddDeadMarker()
+    {
+        try
+        {
+            if (gameObject == null)
+                return;
+
+            // Use a dedicated child GameObject so it doesn't clash with pathRenderer
+            var markerGO = new GameObject("DeadMarker");
+            markerGO.transform.SetParent(transform, worldPositionStays: false);
+            markerGO.transform.localPosition = Vector3.zero;
+
+            var lineRenderer = markerGO.AddComponent<LineRenderer>();
+            if (lineRenderer == null)
+            {
+                Debug.LogWarning("Failed to add LineRenderer component");
+                return;
+            }
+
+            // Set material first before configuring the renderer
+            Shader lineShader = Shader.Find("Unlit/Color");
+            if (lineShader == null)
+                lineShader = Shader.Find("Sprites/Default");
+            if (lineShader == null)
+                lineShader = Shader.Find("Standard");
+            if (lineShader != null)
+            {
+                lineRenderer.material = new Material(lineShader);
+            }
+            else
+            {
+                Debug.LogWarning("No suitable shader found for dead marker");
+                return;
+            }
+
+            // Now configure the line
+            lineRenderer.positionCount = 4;
+            lineRenderer.useWorldSpace = true;
+
+            float offset = 0.35f;
+            // Lift the X marker above the body so the top-down camera can see it
+            Vector3 markerBase = transform.position + Vector3.up * 0.6f;
+
+            // Draw a white X in XZ plane (visible from top-down camera)
+            lineRenderer.SetPosition(0, markerBase + Vector3.left * offset + Vector3.forward * offset);
+            lineRenderer.SetPosition(1, markerBase + Vector3.right * offset + Vector3.back * offset);
+
+            lineRenderer.SetPosition(2, markerBase + Vector3.right * offset + Vector3.forward * offset);
+            lineRenderer.SetPosition(3, markerBase + Vector3.left * offset + Vector3.back * offset);
+
+            lineRenderer.startWidth = 0.15f;
+            lineRenderer.endWidth = 0.15f;
+            lineRenderer.startColor = Color.white;
+            lineRenderer.endColor = Color.white;
+        }
+        catch (System.Exception ex)
+        {
+            Debug.LogWarning($"Failed to add dead marker: {ex.Message}\n{ex.StackTrace}");
+        }
     }
 
     /// <summary>
@@ -1142,6 +1274,38 @@ public class AIAgent : MonoBehaviour
         // Update combat lock – halts FollowPath while a monster is adjacent
         isInCombat = nearestMonster != null;
 
+        // Track fight start
+        if (nearestMonster != null && !fightingMonsters.Contains(nearestMonster))
+        {
+            fightingMonsters.Add(nearestMonster);
+            FightTracker.Instance.StartFight(this, nearestMonster);
+        }
+
+        // Track fight end for monsters no longer in range
+        var monstersToRemove = new List<Monster>();
+        foreach (var monster in fightingMonsters)
+        {
+            if (monster == null || monster.IsDead)
+            {
+                monstersToRemove.Add(monster);
+            }
+            else
+            {
+                float dist = (monster.transform.position - transform.position).sqrMagnitude;
+                if (dist > weapon.MeleeRange * weapon.MeleeRange)
+                {
+                    monstersToRemove.Add(monster);
+                }
+            }
+        }
+
+        foreach (var monster in monstersToRemove)
+        {
+            fightingMonsters.Remove(monster);
+            if (monster != null)
+                FightTracker.Instance.EndFight(this, monster);
+        }
+
         if (nearestMonster == null) return;
 
         // Attack on cooldown

+ 214 - 0
Assets/Scripts/ActiveFightsUIPanel.cs

@@ -0,0 +1,214 @@
+using UnityEngine;
+using UnityEngine.UIElements;
+using System.Collections.Generic;
+using System.Linq;
+
+/// <summary>
+/// Displays active fights in the UI with zoom buttons
+/// Shows runner vs monster fights, sorted by fight intensity (participants in room)
+/// </summary>
+public class ActiveFightsUIPanel : MonoBehaviour
+{
+    [Header("Settings")]
+    [SerializeField] private float updateInterval = 0.5f;
+
+    private VisualElement root;
+    private VisualElement fightsList;
+    private Dictionary<string, VisualElement> fightEntries = new();
+    private float lastUpdateTime = 0f;
+
+    void Start()
+    {
+        InitializeUI();
+    }
+
+    private void InitializeUI()
+    {
+        // Find or create UIDocument
+        var uiDocument = FindAnyObjectByType<UIDocument>();
+        if (uiDocument == null)
+        {
+            var docGO = new GameObject("UIDocument");
+            uiDocument = docGO.AddComponent<UIDocument>();
+        }
+
+        root = uiDocument.rootVisualElement;
+
+        // Create the fights panel container
+        var panelContainer = new VisualElement();
+        panelContainer.name = "ActiveFightsPanel";
+        panelContainer.style.position = Position.Absolute;
+        panelContainer.style.left = 20;  // Left side instead of right
+        panelContainer.style.top = 20;
+        panelContainer.style.width = 350;
+        // Limit height so the list scrolls instead of growing off-screen
+        panelContainer.style.maxHeight = new StyleLength(new Length(85, LengthUnit.Percent));
+        panelContainer.style.backgroundColor = new Color(0.1f, 0.1f, 0.1f, 0.8f);
+        panelContainer.style.borderBottomLeftRadius = 5;
+        panelContainer.style.borderBottomRightRadius = 5;
+        panelContainer.style.borderTopLeftRadius = 5;
+        panelContainer.style.borderTopRightRadius = 5;
+        panelContainer.style.paddingBottom = 10;
+        panelContainer.style.paddingTop = 10;
+        panelContainer.style.paddingLeft = 10;
+        panelContainer.style.paddingRight = 10;
+        panelContainer.style.flexDirection = FlexDirection.Column;
+
+        // Title
+        var title = new Label("Active Fights");
+        title.style.fontSize = 16;
+        title.style.color = Color.white;
+        title.style.marginBottom = 10;
+        title.style.unityFontStyleAndWeight = FontStyle.Bold;
+        title.style.flexShrink = 0;
+        panelContainer.Add(title);
+
+        // Scrollable container so the list doesn't overflow the screen
+        var scrollView = new ScrollView(ScrollViewMode.Vertical);
+        scrollView.name = "FightsScrollView";
+        scrollView.style.flexGrow = 1;
+        scrollView.style.flexShrink = 1;
+        scrollView.horizontalScrollerVisibility = ScrollerVisibility.Hidden;
+        scrollView.verticalScrollerVisibility = ScrollerVisibility.Auto;
+
+        // Fights list container (lives inside the ScrollView)
+        fightsList = new VisualElement();
+        fightsList.name = "FightsList";
+        fightsList.style.flexDirection = FlexDirection.Column;
+        scrollView.Add(fightsList);
+
+        panelContainer.Add(scrollView);
+
+        root.Add(panelContainer);
+    }
+
+    void Update()
+    {
+        if (FightTracker.Instance == null) return;
+        if (fightsList == null) return;
+        if (Time.time - lastUpdateTime < updateInterval) return;
+        lastUpdateTime = Time.time;
+
+        RefreshFightsList();
+    }
+
+    /// <summary>
+    /// Updates the list of displayed fights
+    /// </summary>
+    private void RefreshFightsList()
+    {
+        var fights = FightTracker.Instance.GetDisplayFights();
+        var currentFightIds = new HashSet<string>();
+
+        // Update or create entries for current fights
+        foreach (var fight in fights)
+        {
+            string fightId = $"{fight.runnerId}_{fight.monster.GetHashCode()}";
+            currentFightIds.Add(fightId);
+
+            if (fightEntries.ContainsKey(fightId))
+            {
+                // Update existing entry
+                UpdateFightEntry(fightEntries[fightId], fight);
+            }
+            else
+            {
+                // Create new entry
+                var entryElement = CreateFightEntry(fight, fightId);
+                fightEntries[fightId] = entryElement;
+                fightsList.Add(entryElement);
+            }
+        }
+
+        // Remove entries for fights that are no longer active
+        var entriesToRemove = fightEntries.Keys.Where(id => !currentFightIds.Contains(id)).ToList();
+        foreach (var id in entriesToRemove)
+        {
+            if (fightEntries[id] != null)
+            {
+                fightEntries[id].RemoveFromHierarchy();
+            }
+            fightEntries.Remove(id);
+        }
+    }
+
+    /// <summary>
+    /// Creates a single fight entry UI element
+    /// </summary>
+    private VisualElement CreateFightEntry(FightData fight, string fightId)
+    {
+        var entryContainer = new VisualElement();
+        entryContainer.name = $"FightEntry_{fightId}";
+        entryContainer.style.flexDirection = FlexDirection.Row;
+        entryContainer.style.backgroundColor = new Color(0.2f, 0.2f, 0.2f, 0.9f);
+        entryContainer.style.borderBottomLeftRadius = 3;
+        entryContainer.style.borderBottomRightRadius = 3;
+        entryContainer.style.borderTopLeftRadius = 3;
+        entryContainer.style.borderTopRightRadius = 3;
+        entryContainer.style.paddingBottom = 5;
+        entryContainer.style.paddingTop = 5;
+        entryContainer.style.paddingLeft = 8;
+        entryContainer.style.paddingRight = 8;
+        entryContainer.style.marginBottom = 5;
+        entryContainer.style.alignItems = Align.Center;
+
+        // Status indicator
+        var statusDot = new VisualElement();
+        statusDot.style.width = 12;
+        statusDot.style.height = 12;
+        statusDot.style.borderBottomLeftRadius = 6;
+        statusDot.style.borderBottomRightRadius = 6;
+        statusDot.style.borderTopLeftRadius = 6;
+        statusDot.style.borderTopRightRadius = 6;
+        statusDot.style.backgroundColor = fight.isActive ? Color.red : new Color(1f, 0.5f, 0f);
+        statusDot.style.marginRight = 8;
+        entryContainer.Add(statusDot);
+
+        // Fight text
+        var fightText = new Label(fight.GetDisplayText());
+        fightText.style.color = Color.white;
+        fightText.style.fontSize = 12;
+        fightText.style.flexGrow = 1;
+        entryContainer.Add(fightText);
+
+        // Zoom button
+        var zoomButton = new Button(() => ZoomToFight(fight));
+        zoomButton.text = "Zoom";
+        zoomButton.style.width = 60;
+        zoomButton.style.height = 25;
+        zoomButton.style.backgroundColor = new Color(0.2f, 0.5f, 0.9f, 0.9f);
+        zoomButton.style.color = Color.white;
+        zoomButton.style.fontSize = 11;
+        zoomButton.style.marginLeft = 8;
+        entryContainer.Add(zoomButton);
+
+        return entryContainer;
+    }
+
+    /// <summary>
+    /// Updates an existing fight entry
+    /// </summary>
+    private void UpdateFightEntry(VisualElement entryElement, FightData fight)
+    {
+        if (entryElement == null) return;
+
+        // Update status dot color
+        var statusDot = entryElement.Q<VisualElement>(null, "unity-content-container")?.Children().FirstOrDefault() as VisualElement;
+        if (statusDot != null)
+        {
+            statusDot.style.backgroundColor = fight.isActive ? Color.red : new Color(1f, 0.5f, 0f);
+        }
+    }
+
+    /// <summary>
+    /// Zooms the camera to the room where the fight is happening
+    /// </summary>
+    private void ZoomToFight(FightData fight)
+    {
+        var cameraController = FindAnyObjectByType<MazeCameraController>();
+        if (cameraController != null)
+        {
+            cameraController.ZoomToRoom(fight.room, 1.3f, 0.5f);
+        }
+    }
+}

+ 2 - 0
Assets/Scripts/ActiveFightsUIPanel.cs.meta

@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: f7ebbec473a7bee41bee3547dfab28a7

+ 18 - 0
Assets/Scripts/ExitRoomTrigger.cs

@@ -0,0 +1,18 @@
+using UnityEngine;
+
+/// <summary>
+/// Trigger volume placed in exit rooms to detect when agents reach the goal.
+/// When an agent enters the trigger, it calls StopAtGoal() to stop movement and mark as exited.
+/// </summary>
+public class ExitRoomTrigger : MonoBehaviour
+{
+    private void OnTriggerEnter(Collider other)
+    {
+        // Check if the entering object is an agent
+        AIAgent agent = other.GetComponent<AIAgent>();
+        if (agent != null && !agent.HasReachedGoal)
+        {
+            agent.StopAtGoal();
+        }
+    }
+}

+ 2 - 0
Assets/Scripts/ExitRoomTrigger.cs.meta

@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 0b5dce8477db73a439067ccafdc3b0c8

+ 190 - 0
Assets/Scripts/FightTracker.cs

@@ -0,0 +1,190 @@
+using UnityEngine;
+using System.Collections.Generic;
+using System.Linq;
+
+/// <summary>
+/// Tracks active fights between runners and monsters
+/// Provides fight data for UI display and camera zoom operations
+/// </summary>
+public class FightTracker : MonoBehaviour
+{
+    [SerializeField] private float fightDisplayDuration = 3f; // How long to display fights after they end
+
+    // Internal fight tracking structure
+    private class ActiveFight
+    {
+        public AIAgent runner;
+        public Monster monster;
+        public MazeRoom room;
+        public float startTime;
+        public float endTime = float.MaxValue; // While fighting, this is MaxValue
+        public bool isActive => endTime == float.MaxValue;
+    }
+
+    private List<ActiveFight> activeFights = new();
+    private Dictionary<(AIAgent, Monster), ActiveFight> fightLookup = new();
+
+    private static FightTracker instance;
+    public static FightTracker Instance
+    {
+        get
+        {
+            if (instance == null)
+            {
+                var go = new GameObject("FightTracker");
+                instance = go.AddComponent<FightTracker>();
+            }
+            return instance;
+        }
+    }
+
+    /// <summary>
+    /// Called when a runner starts fighting a monster
+    /// </summary>
+    public void StartFight(AIAgent runner, Monster monster)
+    {
+        if (runner == null || monster == null) return;
+        if (runner.IsDead || monster.IsDead) return;
+
+        var key = (runner, monster);
+        if (fightLookup.ContainsKey(key)) return; // Already tracking this fight
+
+        var fight = new ActiveFight
+        {
+            runner = runner,
+            monster = monster,
+            room = monster.HomeRoom,
+            startTime = Time.time
+        };
+
+        activeFights.Add(fight);
+        fightLookup[key] = fight;
+    }
+
+    /// <summary>
+    /// Called when a fight ends (runner or monster dies, or they separate)
+    /// </summary>
+    public void EndFight(AIAgent runner, Monster monster)
+    {
+        if (runner == null || monster == null) return;
+
+        var key = (runner, monster);
+        if (fightLookup.TryGetValue(key, out var fight))
+        {
+            fight.endTime = Time.time;
+            fightLookup.Remove(key);
+        }
+    }
+
+    /// <summary>
+    /// Gets all fights that should currently be displayed (active + recent)
+    /// </summary>
+    public List<FightData> GetDisplayFights()
+    {
+        var displayFights = new List<FightData>();
+
+        // Clean up old completed fights and null entries
+        activeFights.RemoveAll(f =>
+        {
+            // Remove if either fighter was destroyed
+            if (f.runner == null || f.monster == null)
+                return true;
+
+            float timeSinceEnd = Time.time - f.endTime;
+            return !f.isActive && timeSinceEnd > fightDisplayDuration;
+        });
+
+        // Convert to display format and sort
+        foreach (var fight in activeFights)
+        {
+            // Skip if either fighter is null (shouldn't happen due to RemoveAll, but safe)
+            if (fight.runner == null || fight.monster == null)
+                continue;
+
+            displayFights.Add(new FightData
+            {
+                runnerName = fight.runner.AgentName,
+                runnerId = fight.runner.AgentId,
+                monsterType = $"Monster_{fight.monster.GetHashCode()}",
+                room = fight.room,
+                isActive = fight.isActive,
+                timeSinceEnd = fight.isActive ? 0f : Time.time - fight.endTime,
+                runner = fight.runner,
+                monster = fight.monster
+            });
+        }
+
+        // Sort by number of participants in same room (descending), then by active status
+        displayFights = displayFights
+            .OrderByDescending(f => CountFightsInRoom(f.room))
+            .ThenByDescending(f => f.isActive)
+            .ToList();
+
+        return displayFights;
+    }
+
+    /// <summary>
+    /// Count how many fights are happening in a specific room
+    /// </summary>
+    private int CountFightsInRoom(MazeRoom room)
+    {
+        return activeFights.Count(f => f.room.Id == room.Id);
+    }
+
+    /// <summary>
+    /// Gets total number of fighters in a room (runners + monsters)
+    /// </summary>
+    public int GetFighterCountInRoom(MazeRoom room)
+    {
+        var fightersInRoom = activeFights.Where(f => f.room.Id == room.Id).ToList();
+
+        var uniqueRunners = new HashSet<AIAgent>(fightersInRoom.Select(f => f.runner));
+        var uniqueMonsters = new HashSet<Monster>(fightersInRoom.Select(f => f.monster));
+
+        return uniqueRunners.Count + uniqueMonsters.Count;
+    }
+
+    void Awake()
+    {
+        if (instance == null)
+        {
+            instance = this;
+            DontDestroyOnLoad(gameObject);
+        }
+        else if (instance != this)
+        {
+            Destroy(gameObject);
+        }
+    }
+
+    void Update()
+    {
+        // Periodically clean up dead fighters
+        activeFights.RemoveAll(f => f.runner == null || f.runner.IsDead || f.monster == null || f.monster.IsDead);
+    }
+}
+
+/// <summary>
+/// Display-friendly fight data
+/// </summary>
+public struct FightData
+{
+    public string runnerName;
+    public int runnerId;
+    public string monsterType;
+    public MazeRoom room;
+    public bool isActive;
+    public float timeSinceEnd;
+    public AIAgent runner;
+    public Monster monster;
+
+    public string GetDisplayText()
+    {
+        return $"{runnerName} vs {monsterType}";
+    }
+
+    public string GetRoomInfo()
+    {
+        return $"Room {room.Id}";
+    }
+}

+ 2 - 0
Assets/Scripts/FightTracker.cs.meta

@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 0caab9ba9f356af4bbd561fa1e0df139

+ 115 - 5
Assets/Scripts/MazeCameraController.cs

@@ -27,6 +27,13 @@ public class MazeCameraController : MonoBehaviour
     private Vector3 lastMousePosition;
     private bool isDragging;
 
+    // Smooth zoom and pan to location
+    private bool isSmoothlyCentering = false;
+    private Vector3 smoothTargetPosition;
+    private float smoothTargetZoom;
+    private float smoothCenteringSpeed = 5f;
+    private float smoothZoomSpeed = 3f;
+
     private void Awake()
     {
         cameraComponent = GetComponent<Camera>();
@@ -35,6 +42,11 @@ public class MazeCameraController : MonoBehaviour
 
     private void Update()
     {
+        if (isSmoothlyCentering)
+        {
+            UpdateSmoothCentering();
+        }
+
         HandleKeyboardMovement();
         HandleMouseDrag();
         HandleZoom();
@@ -71,19 +83,23 @@ public class MazeCameraController : MonoBehaviour
 
         if (horizontal != 0f || vertical != 0f)
         {
+            isSmoothlyCentering = false; // Interrupt smooth zoom on manual movement
             Vector3 right = transform.right;
             Vector3 forward = Vector3.Cross(transform.right, Vector3.up).normalized;
             Vector3 worldMove = (right * horizontal + forward * vertical).normalized;
-            targetPosition += worldMove * moveSpeed * Time.deltaTime;
+            // Use unscaledDeltaTime so camera movement is unaffected by simulation speed/pause
+            targetPosition += worldMove * moveSpeed * Time.unscaledDeltaTime;
         }
 
         if (keyboard.qKey.isPressed)
         {
-            Zoom(-zoomSpeed * Time.deltaTime);
+            isSmoothlyCentering = false; // Interrupt smooth zoom on manual zoom
+            Zoom(-zoomSpeed * Time.unscaledDeltaTime);
         }
         else if (keyboard.eKey.isPressed)
         {
-            Zoom(zoomSpeed * Time.deltaTime);
+            isSmoothlyCentering = false; // Interrupt smooth zoom on manual zoom
+            Zoom(zoomSpeed * Time.unscaledDeltaTime);
         }
     }
 
@@ -97,6 +113,7 @@ public class MazeCameraController : MonoBehaviour
 
         if (mouse.rightButton.wasPressedThisFrame)
         {
+            isSmoothlyCentering = false; // Interrupt smooth zoom on manual drag
             isDragging = true;
             Vector2 position = mouse.position.ReadValue();
             lastMousePosition = new Vector3(position.x, position.y, 0f);
@@ -128,7 +145,8 @@ public class MazeCameraController : MonoBehaviour
             float scroll = mouse.scroll.ReadValue().y;
             if (scroll != 0f)
             {
-                Zoom(-scroll * zoomSpeed * Time.deltaTime);
+                isSmoothlyCentering = false; // Interrupt smooth zoom on manual zoom
+                Zoom(-scroll * zoomSpeed * Time.unscaledDeltaTime);
             }
         }
     }
@@ -172,4 +190,96 @@ public class MazeCameraController : MonoBehaviour
 
         transform.position = newPosition;
     }
-}
+
+    /// <summary>
+    /// Smoothly zooms and pans the camera to a specific room
+    /// </summary>
+    /// <param name="room">The room to zoom to</param>
+    /// <param name="zoomPadding">Multiplier for room size (1.0 = exactly room size, 1.3 = 30% larger)</param>
+    /// <param name="transitionDuration">Time to complete the zoom/pan in seconds</param>
+    public void ZoomToRoom(MazeRoom room, float zoomPadding = 1.3f, float transitionDuration = 0.5f)
+    {
+        if (room == null) return;
+
+        // Stop any previous zoom to prevent stacking
+        isSmoothlyCentering = false;
+
+        // Calculate room center in world space (XZ plane only - don't change camera height Y)
+        Vector2Int roomCenter = room.GetCenter();
+        Vector3 roomCenterXZ = new Vector3(roomCenter.x + 0.5f, targetPosition.y, roomCenter.y + 0.5f); // Keep Y unchanged
+
+        // Calculate required zoom level to fit room with padding
+        float roomWidth = room.Width;
+        float roomHeight = room.Height;
+        float maxRoomDim = Mathf.Max(roomWidth, roomHeight) * zoomPadding;
+
+        // Adjust for camera aspect ratio
+        float requiredZoom = maxRoomDim / (2f * Mathf.Tan(cameraComponent.fieldOfView * Mathf.Deg2Rad / 2f));
+        if (cameraComponent.orthographic)
+        {
+            requiredZoom = maxRoomDim * 0.5f;
+        }
+
+        // Set smooth transition targets
+        smoothTargetPosition = roomCenterXZ; // Pan to room center on XZ plane only
+        smoothTargetZoom = Mathf.Clamp(requiredZoom, zoomMin, zoomMax);
+        smoothCenteringSpeed = 1f / transitionDuration;
+        isSmoothlyCentering = true;
+    }
+
+    /// <summary>
+    /// Updates the smooth camera transition toward target position and zoom
+    /// </summary>
+    private void UpdateSmoothCentering()
+    {
+        // Smooth pan to target position - use unscaledDeltaTime so it works while paused/slow
+        float posDist = Vector3.Distance(targetPosition, smoothTargetPosition);
+        if (posDist > 0.01f)
+        {
+            targetPosition = Vector3.Lerp(targetPosition, smoothTargetPosition, smoothCenteringSpeed * Time.unscaledDeltaTime);
+        }
+        else
+        {
+            targetPosition = smoothTargetPosition; // Snap to target
+        }
+
+        // Smooth zoom to target
+        float currentZoom = cameraComponent.orthographic ? cameraComponent.orthographicSize : cameraComponent.fieldOfView;
+        float zoomDist = Mathf.Abs(currentZoom - smoothTargetZoom);
+
+        if (zoomDist > 0.01f)
+        {
+            if (cameraComponent.orthographic)
+            {
+                cameraComponent.orthographicSize = Mathf.Lerp(cameraComponent.orthographicSize, smoothTargetZoom, smoothZoomSpeed * Time.unscaledDeltaTime);
+            }
+            else
+            {
+                cameraComponent.fieldOfView = Mathf.Lerp(cameraComponent.fieldOfView, smoothTargetZoom, smoothZoomSpeed * Time.unscaledDeltaTime);
+            }
+        }
+        else
+        {
+            // Snap to target zoom
+            if (cameraComponent.orthographic)
+            {
+                cameraComponent.orthographicSize = smoothTargetZoom;
+            }
+            else
+            {
+                cameraComponent.fieldOfView = smoothTargetZoom;
+            }
+        }
+
+        // Check if we've reached both targets
+        posDist = Vector3.Distance(targetPosition, smoothTargetPosition);
+        currentZoom = cameraComponent.orthographic ? cameraComponent.orthographicSize : cameraComponent.fieldOfView;
+        zoomDist = Mathf.Abs(currentZoom - smoothTargetZoom);
+
+        if (posDist < 0.01f && zoomDist < 0.01f)
+        {
+            isSmoothlyCentering = false;
+        }
+    }
+
+}

+ 44 - 0
Assets/Scripts/MazeController.cs

@@ -34,6 +34,9 @@ public class MazeController : MonoBehaviour
             meshMazeRenderer = GetComponent<MeshMazeRenderer>() ?? FindAnyObjectByType<MeshMazeRenderer>();
         }
 
+        // Setup Fight Tracker and UI
+        InitializeFightTracking();
+
         // Setup AI agent manager
         if (spawnAIAgents)
         {
@@ -60,6 +63,47 @@ public class MazeController : MonoBehaviour
         }
     }
 
+    /// <summary>
+    /// Initializes the fight tracking system and UI
+    /// </summary>
+    private void InitializeFightTracking()
+    {
+        // Ensure FightTracker exists
+        var fightTracker = FindAnyObjectByType<FightTracker>();
+        if (fightTracker == null)
+        {
+            var trackerGO = new GameObject("FightTracker");
+            trackerGO.transform.parent = transform;
+            trackerGO.AddComponent<FightTracker>();
+        }
+
+        // Ensure ActiveFightsUIPanel exists
+        var fightsPanel = FindAnyObjectByType<ActiveFightsUIPanel>();
+        if (fightsPanel == null)
+        {
+            var panelGO = new GameObject("ActiveFightsUIPanel");
+            var canvas = FindAnyObjectByType<Canvas>();
+            if (canvas != null)
+            {
+                panelGO.transform.SetParent(canvas.transform);
+            }
+            else
+            {
+                panelGO.transform.parent = transform;
+            }
+            panelGO.AddComponent<ActiveFightsUIPanel>();
+        }
+
+        // Ensure SimulationControlsUIPanel exists (pause / 0.5x / 1x / 2x buttons)
+        var simControls = FindAnyObjectByType<SimulationControlsUIPanel>();
+        if (simControls == null)
+        {
+            var controlsGO = new GameObject("SimulationControlsUIPanel");
+            controlsGO.transform.parent = transform;
+            controlsGO.AddComponent<SimulationControlsUIPanel>();
+        }
+    }
+
     /// <summary>
     /// Generates a new maze using the current configuration
     /// </summary>

+ 3 - 7
Assets/Scripts/MazeData.cs

@@ -150,16 +150,12 @@ public class MazeData
     }
 
     /// <summary>
-    /// Gets all rooms of a specific type. Result is cached after the first call.
+    /// Gets all rooms of a specific type. Always queries fresh since room types
+    /// can be changed after AddRoom() (e.g. exit rooms assigned later by generator).
     /// </summary>
     public List<MazeRoom> GetRoomsByType(MazeRoom.RoomType type)
     {
-        if (!_roomsByType.TryGetValue(type, out var list))
-        {
-            list = Rooms.Where(r => r.Type == type).ToList();
-            _roomsByType[type] = list;
-        }
-        return list;
+        return Rooms.Where(r => r.Type == type).ToList();
     }
 
     /// <summary>

+ 102 - 2
Assets/Scripts/Monster.cs

@@ -37,6 +37,7 @@ public class Monster : MonoBehaviour
     private MazeData maze;
 
     private AIAgent target;             // Current attack target
+    private AIAgent previousTarget;      // Track previous target to detect combat start/end
     private float lastAttackTime;
     private float lastWanderTime;
     private Vector3 wanderTarget;
@@ -94,7 +95,13 @@ public class Monster : MonoBehaviour
 
     void Update()
     {
-        if (isDead || maze == null) return;
+        if (maze == null) return;
+
+        // Dead bodies persist forever - just stop updating logic
+        if (isDead)
+        {
+            return;
+        }
 
         AcquireTarget();
 
@@ -113,6 +120,9 @@ public class Monster : MonoBehaviour
         // Drop dead targets
         if (target != null && (target == null || target.IsDead || target.HasReachedGoal))
         {
+            // Report fight end to tracker
+            if (target != null)
+                FightTracker.Instance.EndFight(target, this);
             target = null;
         }
 
@@ -140,7 +150,14 @@ public class Monster : MonoBehaviour
             }
         }
 
+        // Report fight start if target changed
+        if (best != null && best != previousTarget)
+        {
+            FightTracker.Instance.StartFight(best, this);
+        }
+
         target = best;
+        previousTarget = best;
     }
 
     // ------------------------------------------------------------------ //
@@ -243,9 +260,92 @@ public class Monster : MonoBehaviour
         if (isDead) return;
         isDead = true;
         currentHealth = 0;
+
+        // Report fight end
+        if (target != null)
+            FightTracker.Instance.EndFight(target, this);
+
+        // Change visual to show dead (gray/dark sphere with red X)
+        var renderer = GetComponent<Renderer>();
+        if (renderer != null)
+        {
+            var material = new Material(renderer.material);
+            material.color = new Color(0.3f, 0.3f, 0.3f, 0.9f); // Dark gray, opaque
+            renderer.material = material;
+        }
+
+        // Add bright red X marker BEFORE disabling (must happen while enabled)
+        AddDeadMarker();
+
+        // Disable movement and collider AFTER adding marker
+        enabled = false;
+
         Debug.Log($"[Monster] in room {homeRoom?.Id} died.");
         OnMonsterKilled?.Invoke(this);
-        Destroy(gameObject);
+    }
+
+    /// <summary>
+    /// Adds a bright red X marker to indicate the body is dead.
+    /// Uses a child GameObject for cleanliness.
+    /// </summary>
+    private void AddDeadMarker()
+    {
+        try
+        {
+            if (gameObject == null)
+                return;
+
+            var markerGO = new GameObject("DeadMarker");
+            markerGO.transform.SetParent(transform, worldPositionStays: false);
+            markerGO.transform.localPosition = Vector3.zero;
+
+            var lineRenderer = markerGO.AddComponent<LineRenderer>();
+            if (lineRenderer == null)
+            {
+                Debug.LogWarning("Failed to add LineRenderer component");
+                return;
+            }
+
+            // Set material first before configuring the renderer
+            Shader lineShader = Shader.Find("Unlit/Color");
+            if (lineShader == null)
+                lineShader = Shader.Find("Sprites/Default");
+            if (lineShader == null)
+                lineShader = Shader.Find("Standard");
+            if (lineShader != null)
+            {
+                lineRenderer.material = new Material(lineShader);
+            }
+            else
+            {
+                Debug.LogWarning("No suitable shader found for dead marker");
+                return;
+            }
+
+            // Now configure the line
+            lineRenderer.positionCount = 4;
+            lineRenderer.useWorldSpace = true;
+
+            float offset = 0.35f;
+            // Lift the X marker above the body so the top-down camera can see it
+            Vector3 markerBase = transform.position + Vector3.up * 0.6f;
+
+            // Draw a bright red X in XZ plane (visible from top-down camera)
+            lineRenderer.SetPosition(0, markerBase + Vector3.left * offset + Vector3.forward * offset);
+            lineRenderer.SetPosition(1, markerBase + Vector3.right * offset + Vector3.back * offset);
+
+            lineRenderer.SetPosition(2, markerBase + Vector3.right * offset + Vector3.forward * offset);
+            lineRenderer.SetPosition(3, markerBase + Vector3.left * offset + Vector3.back * offset);
+
+            lineRenderer.startWidth = 0.15f;
+            lineRenderer.endWidth = 0.15f;
+            lineRenderer.startColor = new Color(1f, 0f, 0f, 1f); // Bright red
+            lineRenderer.endColor = new Color(1f, 0f, 0f, 1f);
+        }
+        catch (System.Exception ex)
+        {
+            Debug.LogWarning($"Failed to add dead marker: {ex.Message}\n{ex.StackTrace}");
+        }
     }
 
     // ------------------------------------------------------------------ //

+ 160 - 0
Assets/Scripts/SimulationControlsUIPanel.cs

@@ -0,0 +1,160 @@
+using UnityEngine;
+using UnityEngine.UIElements;
+
+/// <summary>
+/// Adds a small UI Toolkit control panel for adjusting Time.timeScale at runtime.
+/// Buttons: Pause (0x), 0.5x, 1x (normal), 2x.
+/// Camera movement is independent of Time.timeScale (uses unscaledDeltaTime),
+/// so the simulation can be paused/slowed without affecting navigation.
+/// </summary>
+public class SimulationControlsUIPanel : MonoBehaviour
+{
+    [Header("Speed Options")]
+    [SerializeField] private float pausedScale = 0f;
+    [SerializeField] private float halfScale = 0.5f;
+    [SerializeField] private float normalScale = 1f;
+    [SerializeField] private float doubleScale = 2f;
+
+    private VisualElement root;
+    private Button pauseButton;
+    private Button halfButton;
+    private Button normalButton;
+    private Button doubleButton;
+    private Label statusLabel;
+
+    private float lastSpeedBeforePause = 1f;
+
+    void Start()
+    {
+        InitializeUI();
+        ApplySpeed(normalScale);
+    }
+
+    void OnDestroy()
+    {
+        // Always restore normal speed when leaving the scene/play mode
+        Time.timeScale = 1f;
+    }
+
+    private void InitializeUI()
+    {
+        var uiDocument = FindAnyObjectByType<UIDocument>();
+        if (uiDocument == null)
+        {
+            var docGO = new GameObject("UIDocument");
+            uiDocument = docGO.AddComponent<UIDocument>();
+        }
+
+        root = uiDocument.rootVisualElement;
+
+        // Container - bottom-center
+        var panel = new VisualElement();
+        panel.name = "SimulationControlsPanel";
+        panel.style.position = Position.Absolute;
+        panel.style.bottom = 20;
+        panel.style.left = new StyleLength(new Length(50, LengthUnit.Percent));
+        panel.style.translate = new StyleTranslate(new Translate(new Length(-50, LengthUnit.Percent), 0));
+        panel.style.flexDirection = FlexDirection.Row;
+        panel.style.alignItems = Align.Center;
+        panel.style.backgroundColor = new Color(0.1f, 0.1f, 0.1f, 0.85f);
+        panel.style.borderBottomLeftRadius = 6;
+        panel.style.borderBottomRightRadius = 6;
+        panel.style.borderTopLeftRadius = 6;
+        panel.style.borderTopRightRadius = 6;
+        panel.style.paddingBottom = 6;
+        panel.style.paddingTop = 6;
+        panel.style.paddingLeft = 10;
+        panel.style.paddingRight = 10;
+
+        var title = new Label("Speed:");
+        title.style.color = Color.white;
+        title.style.fontSize = 13;
+        title.style.unityFontStyleAndWeight = FontStyle.Bold;
+        title.style.marginRight = 10;
+        panel.Add(title);
+
+        pauseButton = MakeButton("Pause", () => ApplySpeed(pausedScale));
+        halfButton = MakeButton("0.5x", () => ApplySpeed(halfScale));
+        normalButton = MakeButton("1x", () => ApplySpeed(normalScale));
+        doubleButton = MakeButton("2x", () => ApplySpeed(doubleScale));
+
+        panel.Add(pauseButton);
+        panel.Add(halfButton);
+        panel.Add(normalButton);
+        panel.Add(doubleButton);
+
+        statusLabel = new Label("1x");
+        statusLabel.style.color = new Color(0.7f, 0.9f, 1f);
+        statusLabel.style.fontSize = 12;
+        statusLabel.style.marginLeft = 10;
+        statusLabel.style.minWidth = 50;
+        statusLabel.style.unityTextAlign = TextAnchor.MiddleLeft;
+        panel.Add(statusLabel);
+
+        root.Add(panel);
+    }
+
+    private Button MakeButton(string text, System.Action onClick)
+    {
+        var btn = new Button(onClick) { text = text };
+        btn.style.width = 60;
+        btn.style.height = 28;
+        btn.style.marginLeft = 4;
+        btn.style.marginRight = 4;
+        btn.style.fontSize = 12;
+        btn.style.color = Color.white;
+        btn.style.backgroundColor = new Color(0.25f, 0.25f, 0.28f, 1f);
+        btn.style.borderBottomLeftRadius = 4;
+        btn.style.borderBottomRightRadius = 4;
+        btn.style.borderTopLeftRadius = 4;
+        btn.style.borderTopRightRadius = 4;
+        return btn;
+    }
+
+    private void ApplySpeed(float scale)
+    {
+        if (scale > 0f)
+            lastSpeedBeforePause = scale;
+
+        Time.timeScale = scale;
+        UpdateButtonHighlight(scale);
+        if (statusLabel != null)
+            statusLabel.text = scale <= 0f ? "PAUSED" : $"{scale}x";
+    }
+
+    private void UpdateButtonHighlight(float scale)
+    {
+        SetActive(pauseButton, Mathf.Approximately(scale, pausedScale));
+        SetActive(halfButton, Mathf.Approximately(scale, halfScale));
+        SetActive(normalButton, Mathf.Approximately(scale, normalScale));
+        SetActive(doubleButton, Mathf.Approximately(scale, doubleScale));
+    }
+
+    private void SetActive(Button btn, bool active)
+    {
+        if (btn == null) return;
+        btn.style.backgroundColor = active
+            ? new Color(0.2f, 0.6f, 0.9f, 1f)   // highlighted
+            : new Color(0.25f, 0.25f, 0.28f, 1f);
+        btn.style.unityFontStyleAndWeight = active ? FontStyle.Bold : FontStyle.Normal;
+    }
+
+    void Update()
+    {
+        // Keyboard shortcuts: Space = pause/resume, 1/2/3/4 = speed presets
+        var kb = UnityEngine.InputSystem.Keyboard.current;
+        if (kb == null) return;
+
+        if (kb.spaceKey.wasPressedThisFrame)
+        {
+            if (Time.timeScale > 0f)
+                ApplySpeed(0f);
+            else
+                ApplySpeed(lastSpeedBeforePause);
+        }
+        else if (kb.digit1Key.wasPressedThisFrame) ApplySpeed(pausedScale);
+        else if (kb.digit2Key.wasPressedThisFrame) ApplySpeed(halfScale);
+        else if (kb.digit3Key.wasPressedThisFrame) ApplySpeed(normalScale);
+        else if (kb.digit4Key.wasPressedThisFrame) ApplySpeed(doubleScale);
+    }
+}

+ 2 - 0
Assets/Scripts/SimulationControlsUIPanel.cs.meta

@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 5c3a497a4c950194b9c4fe304e576478

+ 15 - 15
Assets/Settings/Scenes/URP2DSceneTemplate.unity

@@ -220,22 +220,22 @@ MonoBehaviour:
   m_Name: 
   m_EditorClassIdentifier: Assembly-CSharp::MazeController
   mazeConfig:
-    Width: 100
-    Height: 100
+    Width: 500
+    Height: 500
     MinSize: 100
     MaxSize: 10000
-    TargetRoomCount: 200
-    MinRoomWidth: 5
-    MaxRoomWidth: 25
-    MinRoomHeight: 5
-    MaxRoomHeight: 25
-    MinRoomSpacing: 2
-    MinStartPoints: 1
-    MaxStartPoints: 5
-    MinExits: 1
-    MaxExits: 5
-    MinHallwayWidth: 1
-    MaxHallwayWidth: 3
+    TargetRoomCount: 1000
+    MinRoomWidth: 10
+    MaxRoomWidth: 50
+    MinRoomHeight: 10
+    MaxRoomHeight: 50
+    MinRoomSpacing: 4
+    MinStartPoints: 3
+    MaxStartPoints: 10
+    MinExits: 3
+    MaxExits: 10
+    MinHallwayWidth: 2
+    MaxHallwayWidth: 6
     SafeRoomCount: 2
     RestRoomCount: 2
     BossRoomCount: 1
@@ -681,7 +681,7 @@ MonoBehaviour:
   m_Script: {fileID: 11500000, guid: 9a8b93b03e6d9d6459155323011fe978, type: 3}
   m_Name: 
   m_EditorClassIdentifier: Assembly-CSharp::AIAgentManager
-  initialAgentCount: 100
+  initialAgentCount: 1000
   agentCharacterType: Default
   spawnDelay: 0.1
   agentPrefab: {fileID: 0}

+ 78 - 78
UserSettings/Layouts/default-6000.dwlt

@@ -14,10 +14,10 @@ MonoBehaviour:
   m_EditorClassIdentifier: UnityEditor.dll::UnityEditor.ContainerWindow
   m_PixelRect:
     serializedVersion: 2
-    x: 0
-    y: 43
-    width: 3440
-    height: 1349
+    x: -535
+    y: -1037
+    width: 1920
+    height: 989
   m_ShowMode: 4
   m_Title: Game
   m_RootView: {fileID: 5}
@@ -39,10 +39,10 @@ MonoBehaviour:
   m_Children: []
   m_Position:
     serializedVersion: 2
-    x: 1697
+    x: 947
     y: 0
-    width: 1103
-    height: 439
+    width: 616
+    height: 248
   m_MinSize: {x: 102, y: 126}
   m_MaxSize: {x: 4002, y: 4026}
   m_ActualView: {fileID: 23}
@@ -68,13 +68,13 @@ MonoBehaviour:
   m_Position:
     serializedVersion: 2
     x: 0
-    y: 854
-    width: 2800
-    height: 439
+    y: 685
+    width: 1563
+    height: 248
   m_MinSize: {x: 200, y: 56}
   m_MaxSize: {x: 16192, y: 8096}
   vertical: 0
-  controlID: 121
+  controlID: 119
   draggingID: 0
 --- !u!114 &4
 MonoBehaviour:
@@ -91,10 +91,10 @@ MonoBehaviour:
   m_Children: []
   m_Position:
     serializedVersion: 2
-    x: 1522
+    x: 725
     y: 0
-    width: 1278
-    height: 854
+    width: 838
+    height: 685
   m_MinSize: {x: 52, y: 76}
   m_MaxSize: {x: 4002, y: 4026}
   m_ActualView: {fileID: 17}
@@ -123,8 +123,8 @@ MonoBehaviour:
     serializedVersion: 2
     x: 0
     y: 0
-    width: 3440
-    height: 1349
+    width: 1920
+    height: 989
   m_MinSize: {x: 875, y: 300}
   m_MaxSize: {x: 10000, y: 10000}
   m_UseTopView: 1
@@ -148,7 +148,7 @@ MonoBehaviour:
     serializedVersion: 2
     x: 0
     y: 0
-    width: 3440
+    width: 1920
     height: 36
   m_MinSize: {x: 50, y: 50}
   m_MaxSize: {x: 4000, y: 4000}
@@ -169,8 +169,8 @@ MonoBehaviour:
   m_Position:
     serializedVersion: 2
     x: 0
-    y: 1329
-    width: 3440
+    y: 969
+    width: 1920
     height: 20
   m_MinSize: {x: 0, y: 0}
   m_MaxSize: {x: 0, y: 0}
@@ -193,8 +193,8 @@ MonoBehaviour:
     serializedVersion: 2
     x: 0
     y: 36
-    width: 3440
-    height: 1293
+    width: 1920
+    height: 933
   m_MinSize: {x: 400, y: 112}
   m_MaxSize: {x: 32384, y: 16192}
   vertical: 0
@@ -219,8 +219,8 @@ MonoBehaviour:
     serializedVersion: 2
     x: 0
     y: 0
-    width: 2800
-    height: 1293
+    width: 1563
+    height: 933
   m_MinSize: {x: 300, y: 112}
   m_MaxSize: {x: 24288, y: 16192}
   vertical: 1
@@ -246,8 +246,8 @@ MonoBehaviour:
     serializedVersion: 2
     x: 0
     y: 0
-    width: 2800
-    height: 854
+    width: 1563
+    height: 685
   m_MinSize: {x: 300, y: 56}
   m_MaxSize: {x: 24288, y: 8096}
   vertical: 0
@@ -270,8 +270,8 @@ MonoBehaviour:
     serializedVersion: 2
     x: 0
     y: 0
-    width: 620
-    height: 854
+    width: 346
+    height: 685
   m_MinSize: {x: 201, y: 226}
   m_MaxSize: {x: 4001, y: 4026}
   m_ActualView: {fileID: 19}
@@ -294,10 +294,10 @@ MonoBehaviour:
   m_Children: []
   m_Position:
     serializedVersion: 2
-    x: 620
+    x: 346
     y: 0
-    width: 902
-    height: 854
+    width: 379
+    height: 685
   m_MinSize: {x: 202, y: 226}
   m_MaxSize: {x: 4002, y: 4026}
   m_ActualView: {fileID: 20}
@@ -324,8 +324,8 @@ MonoBehaviour:
     serializedVersion: 2
     x: 0
     y: 0
-    width: 1697
-    height: 439
+    width: 947
+    height: 248
   m_MinSize: {x: 231, y: 276}
   m_MaxSize: {x: 10001, y: 10026}
   m_ActualView: {fileID: 22}
@@ -348,10 +348,10 @@ MonoBehaviour:
   m_Children: []
   m_Position:
     serializedVersion: 2
-    x: 2800
+    x: 1563
     y: 0
-    width: 640
-    height: 1293
+    width: 357
+    height: 933
   m_MinSize: {x: 276, y: 76}
   m_MaxSize: {x: 4001, y: 4026}
   m_ActualView: {fileID: 24}
@@ -382,7 +382,7 @@ MonoBehaviour:
     serializedVersion: 2
     x: 0
     y: 0
-    width: 3440
+    width: 1920
     height: 36
   m_SerializedDataModeController:
     m_DataMode: 0
@@ -608,12 +608,12 @@ MonoBehaviour:
       displayed: 1
       id: Play Mode Controls
       index: 0
-      contents: '{"m_Layout":4,"m_Collapsed":false,"m_Folded":false,"m_Floating":false,"m_FloatingSnapOffset":{"x":-1670.0,"y":-36.0},"m_SnapOffsetDelta":{"x":0.0,"y":0.0},"m_FloatingSnapCorner":3,"m_Size":{"x":0.0,"y":0.0},"m_SizeOverridden":false}'
+      contents: '{"m_Layout":4,"m_Collapsed":false,"m_Folded":false,"m_Floating":false,"m_FloatingSnapOffset":{"x":250.0,"y":0.0},"m_SnapOffsetDelta":{"x":0.0,"y":0.0},"m_FloatingSnapCorner":0,"m_Size":{"x":0.0,"y":0.0},"m_SizeOverridden":false}'
       floating: 0
       collapsed: 0
-      snapOffset: {x: -1670, y: -36}
+      snapOffset: {x: 250, y: 0}
       snapOffsetDelta: {x: 0, y: 0}
-      snapCorner: 3
+      snapCorner: 0
       layout: 4
       size: {x: 0, y: 0}
       sizeOverridden: 0
@@ -684,10 +684,10 @@ MonoBehaviour:
     m_TextWithWhitespace: "Game\u200B"
   m_Pos:
     serializedVersion: 2
-    x: 1523
+    x: 726
     y: 24
-    width: 1276
-    height: 828
+    width: 836
+    height: 659
   m_SerializedDataModeController:
     m_DataMode: 0
     m_PreferredDataMode: 0
@@ -707,7 +707,7 @@ MonoBehaviour:
   m_ShowGizmos: 0
   m_TargetDisplay: 0
   m_ClearColor: {r: 0, g: 0, b: 0, a: 0}
-  m_TargetSize: {x: 1276, y: 807}
+  m_TargetSize: {x: 836, y: 638}
   m_TextureFilterMode: 0
   m_TextureHideFlags: 61
   m_RenderIMGUI: 1
@@ -722,10 +722,10 @@ MonoBehaviour:
     m_VRangeLocked: 0
     hZoomLockedByDefault: 0
     vZoomLockedByDefault: 0
-    m_HBaseRangeMin: -638
-    m_HBaseRangeMax: 638
-    m_VBaseRangeMin: -403.5
-    m_VBaseRangeMax: 403.5
+    m_HBaseRangeMin: -418
+    m_HBaseRangeMax: 418
+    m_VBaseRangeMin: -319
+    m_VBaseRangeMax: 319
     m_HAllowExceedBaseRangeMin: 1
     m_HAllowExceedBaseRangeMax: 1
     m_VAllowExceedBaseRangeMin: 1
@@ -743,23 +743,23 @@ MonoBehaviour:
       serializedVersion: 2
       x: 0
       y: 21
-      width: 1276
-      height: 807
+      width: 836
+      height: 638
     m_Scale: {x: 1, y: 1}
-    m_Translation: {x: 638, y: 403.5}
+    m_Translation: {x: 418, y: 319}
     m_MarginLeft: 0
     m_MarginRight: 0
     m_MarginTop: 0
     m_MarginBottom: 0
     m_LastShownAreaInsideMargins:
       serializedVersion: 2
-      x: -638
-      y: -403.5
-      width: 1276
-      height: 807
+      x: -418
+      y: -319
+      width: 836
+      height: 638
     m_MinimalGUI: 1
   m_defaultScale: 1
-  m_LastWindowPixelSize: {x: 1276, y: 828}
+  m_LastWindowPixelSize: {x: 836, y: 659}
   m_ClearInEditMode: 1
   m_NoCameraWarning: 1
   m_LowResolutionForAspectRatios: 01000000000000000000
@@ -831,8 +831,8 @@ MonoBehaviour:
     serializedVersion: 2
     x: 0
     y: 24
-    width: 619
-    height: 828
+    width: 345
+    height: 659
   m_SerializedDataModeController:
     m_DataMode: 0
     m_PreferredDataMode: 0
@@ -901,10 +901,10 @@ MonoBehaviour:
     m_TextWithWhitespace: "Scene\u200B"
   m_Pos:
     serializedVersion: 2
-    x: 621
+    x: 347
     y: 24
-    width: 900
-    height: 828
+    width: 377
+    height: 659
   m_SerializedDataModeController:
     m_DataMode: 0
     m_PreferredDataMode: 0
@@ -1684,8 +1684,8 @@ MonoBehaviour:
     serializedVersion: 2
     x: 0
     y: 24
-    width: 1696
-    height: 413
+    width: 946
+    height: 222
   m_SerializedDataModeController:
     m_DataMode: 0
     m_PreferredDataMode: 0
@@ -1729,16 +1729,16 @@ MonoBehaviour:
   m_FolderTreeState:
     scrollPos: {x: 0, y: 0}
     m_SelectedIDs:
-    - m_Data: 70296
+    - m_Data: 70310
     m_LastClickedID:
-      m_Data: 70296
+      m_Data: 70310
     m_ExpandedIDs:
     - m_Data: 0
-    - m_Data: 65358
-    - m_Data: 69978
-    - m_Data: 69980
+    - m_Data: 65362
     - m_Data: 69982
     - m_Data: 69984
+    - m_Data: 69986
+    - m_Data: 69988
     m_RenameOverlay:
       m_UserAcceptedRename: 0
       m_Name: 
@@ -1772,11 +1772,11 @@ MonoBehaviour:
       m_Data: 0
     m_ExpandedIDs:
     - m_Data: 0
-    - m_Data: 65358
-    - m_Data: 69978
-    - m_Data: 69980
+    - m_Data: 65362
     - m_Data: 69982
     - m_Data: 69984
+    - m_Data: 69986
+    - m_Data: 69988
     m_RenameOverlay:
       m_UserAcceptedRename: 0
       m_Name: 
@@ -1805,9 +1805,9 @@ MonoBehaviour:
       m_ResourceFile: 
   m_ListAreaState:
     m_SelectedInstanceIDs:
-    - m_Data: 69648
+    - m_Data: 53048
     m_LastClickedEntityId:
-      m_Data: 69648
+      m_Data: 53048
     m_HadKeyboardFocusLastEvent: 1
     m_ExpandedInstanceIDs:
     - m_Data: 70016
@@ -1864,10 +1864,10 @@ MonoBehaviour:
     m_TextWithWhitespace: "Console\u200B"
   m_Pos:
     serializedVersion: 2
-    x: 1698
+    x: 948
     y: 24
-    width: 1101
-    height: 413
+    width: 614
+    height: 222
   m_SerializedDataModeController:
     m_DataMode: 0
     m_PreferredDataMode: 0
@@ -1902,10 +1902,10 @@ MonoBehaviour:
     m_TextWithWhitespace: "Inspector\u200B"
   m_Pos:
     serializedVersion: 2
-    x: 2801
+    x: 1564
     y: 24
-    width: 639
-    height: 1267
+    width: 356
+    height: 907
   m_SerializedDataModeController:
     m_DataMode: 0
     m_PreferredDataMode: 0