|
@@ -24,7 +24,8 @@ public class AIAgent : MonoBehaviour
|
|
|
private float actualMovementSpeed; // Per-agent speed variation to reduce synchronization
|
|
private float actualMovementSpeed; // Per-agent speed variation to reduce synchronization
|
|
|
|
|
|
|
|
[Header("Pathfinding")]
|
|
[Header("Pathfinding")]
|
|
|
- [SerializeField] private bool showPath = true;
|
|
|
|
|
|
|
+ [Tooltip("Disable for large agent counts to avoid LineRenderer overhead.")]
|
|
|
|
|
+ [SerializeField] private bool showPath = false;
|
|
|
[SerializeField] private LineRenderer pathRenderer;
|
|
[SerializeField] private LineRenderer pathRenderer;
|
|
|
[SerializeField] private Color pathColor = Color.yellow;
|
|
[SerializeField] private Color pathColor = Color.yellow;
|
|
|
|
|
|
|
@@ -46,6 +47,7 @@ public class AIAgent : MonoBehaviour
|
|
|
private bool commitedToExit = false; // Track if agent has committed to a specific hallway exit
|
|
private bool commitedToExit = false; // Track if agent has committed to a specific hallway exit
|
|
|
private float nextRandomWait = 0f; // Random wait time before exploring new areas
|
|
private float nextRandomWait = 0f; // Random wait time before exploring new areas
|
|
|
private float agentRandomOffset = 0f; // Per-agent random offset to desync movement
|
|
private float agentRandomOffset = 0f; // Per-agent random offset to desync movement
|
|
|
|
|
+ private bool pathRequestPending = false; // True while waiting for async pathfinding result
|
|
|
|
|
|
|
|
// Intelligence-driven disposition (seeded at spawn from Intelligence stat)
|
|
// Intelligence-driven disposition (seeded at spawn from Intelligence stat)
|
|
|
// Will drive group-up logic, risk assessment, and future combat decisions
|
|
// Will drive group-up logic, risk assessment, and future combat decisions
|
|
@@ -57,6 +59,26 @@ public class AIAgent : MonoBehaviour
|
|
|
private AgentGroup currentGroup = null;
|
|
private AgentGroup currentGroup = null;
|
|
|
private bool isGroupFollower = false; // True when this agent defers movement to the leader
|
|
private bool isGroupFollower = false; // True when this agent defers movement to the leader
|
|
|
|
|
|
|
|
|
|
+ // ----- Combat -----
|
|
|
|
|
+ private Weapon weapon;
|
|
|
|
|
+ private float lastAttackTime = 0f;
|
|
|
|
|
+ private const float ATTACK_COOLDOWN = 1.0f;
|
|
|
|
|
+ /// <summary>Agent advantage multiplier on hit rolls (can improve with experience/weapons later).</summary>
|
|
|
|
|
+ private const float AGENT_HIT_ADVANTAGE = 1.3f;
|
|
|
|
|
+ private bool isDead = false;
|
|
|
|
|
+ /// <summary>True while a monster is within melee range – halts path-following.</summary>
|
|
|
|
|
+ private bool isInCombat = false;
|
|
|
|
|
+
|
|
|
|
|
+ // ----- Room danger / avoidance -----
|
|
|
|
|
+ /// <summary>Room IDs the agent has decided to avoid (too dangerous to fight through).</summary>
|
|
|
|
|
+ private readonly HashSet<int> avoidedRooms = new();
|
|
|
|
|
+ /// <summary>Speed threshold: agents above this fraction of max speed may try to run through.</summary>
|
|
|
|
|
+ private const float RUN_THROUGH_SPEED_FRACTION = 0.6f; // Speed stat > 60 enables sprinting
|
|
|
|
|
+
|
|
|
|
|
+ // ----- Cached maze data (set once in Start, never changes) -----
|
|
|
|
|
+ private MazeRoom[] _allRooms; // Array copy is faster to iterate than List
|
|
|
|
|
+ private HashSet<MazeRoom> _goalRooms; // Constant set for O(1) Contains checks
|
|
|
|
|
+
|
|
|
void Start()
|
|
void Start()
|
|
|
{
|
|
{
|
|
|
mazeController = FindAnyObjectByType<MazeController>();
|
|
mazeController = FindAnyObjectByType<MazeController>();
|
|
@@ -78,6 +100,10 @@ public class AIAgent : MonoBehaviour
|
|
|
pathfinder = new MazePathfinder(maze);
|
|
pathfinder = new MazePathfinder(maze);
|
|
|
roomMemory = AIRoomMemoryManager.GetMemory(agentCharacterType);
|
|
roomMemory = AIRoomMemoryManager.GetMemory(agentCharacterType);
|
|
|
|
|
|
|
|
|
|
+ // 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));
|
|
|
|
|
+
|
|
|
// Initialize personalization
|
|
// Initialize personalization
|
|
|
agentName = AgentNameGenerator.GenerateRandomName();
|
|
agentName = AgentNameGenerator.GenerateRandomName();
|
|
|
agentStats = new AgentStats();
|
|
agentStats = new AgentStats();
|
|
@@ -87,6 +113,9 @@ public class AIAgent : MonoBehaviour
|
|
|
groupUpAffinity = agentStats.GroupUpAffinity;
|
|
groupUpAffinity = agentStats.GroupUpAffinity;
|
|
|
riskTolerance = agentStats.RiskTolerance;
|
|
riskTolerance = agentStats.RiskTolerance;
|
|
|
|
|
|
|
|
|
|
+ // Equip fists as default weapon
|
|
|
|
|
+ weapon = Weapon.Fists();
|
|
|
|
|
+
|
|
|
// Initialize with a random start point
|
|
// Initialize with a random start point
|
|
|
if (maze.StartPoints.Count > 0)
|
|
if (maze.StartPoints.Count > 0)
|
|
|
{
|
|
{
|
|
@@ -130,7 +159,8 @@ public class AIAgent : MonoBehaviour
|
|
|
float speedMultiplier = 1f + (agentStats.Speed / 100f);
|
|
float speedMultiplier = 1f + (agentStats.Speed / 100f);
|
|
|
actualMovementSpeed = movementSpeed * speedMultiplier * Random.Range(0.9f, 1.1f);
|
|
actualMovementSpeed = movementSpeed * speedMultiplier * Random.Range(0.9f, 1.1f);
|
|
|
|
|
|
|
|
- Debug.Log($"[Agent {agentId}] {agentName} | {agentStats} | GroupUp: {groupUpAffinity:P0} | Risk: {riskTolerance:P0}");
|
|
|
|
|
|
|
+ // Suppress per-agent spawn log for large counts; enable only when debugging individuals
|
|
|
|
|
+ // Debug.Log($"[Agent {agentId}] {agentName} | {agentStats} | GroupUp: {groupUpAffinity:P0} | Risk: {riskTolerance:P0}");
|
|
|
|
|
|
|
|
// Setup path renderer
|
|
// Setup path renderer
|
|
|
if (showPath && pathRenderer == null)
|
|
if (showPath && pathRenderer == null)
|
|
@@ -147,42 +177,70 @@ public class AIAgent : MonoBehaviour
|
|
|
|
|
|
|
|
void Update()
|
|
void Update()
|
|
|
{
|
|
{
|
|
|
- if (maze == null) return;
|
|
|
|
|
|
|
+ if (maze == null || isDead) return;
|
|
|
|
|
|
|
|
// Followers defer all movement to the group leader
|
|
// Followers defer all movement to the group leader
|
|
|
if (isGroupFollower)
|
|
if (isGroupFollower)
|
|
|
{
|
|
{
|
|
|
if (currentGroup != null && currentGroup.Leader != null)
|
|
if (currentGroup != null && currentGroup.Leader != null)
|
|
|
transform.position = currentGroup.Leader.transform.position;
|
|
transform.position = currentGroup.Leader.transform.position;
|
|
|
|
|
+ // Followers still fight monsters in range
|
|
|
|
|
+ CombatUpdate();
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // Fight any adjacent monsters
|
|
|
|
|
+ CombatUpdate();
|
|
|
|
|
+
|
|
|
// Update current room
|
|
// Update current room
|
|
|
UpdateCurrentRoom();
|
|
UpdateCurrentRoom();
|
|
|
|
|
|
|
|
- // Update pathfinding periodically with agent-specific offset to desync behavior
|
|
|
|
|
- if (Time.time - lastPathUpdate > pathUpdateInterval + agentRandomOffset)
|
|
|
|
|
|
|
+ // Update pathfinding periodically – skip if an async request is already in-flight
|
|
|
|
|
+ if (!pathRequestPending && Time.time - lastPathUpdate > pathUpdateInterval + agentRandomOffset)
|
|
|
{
|
|
{
|
|
|
UpdatePathToGoal();
|
|
UpdatePathToGoal();
|
|
|
lastPathUpdate = Time.time;
|
|
lastPathUpdate = Time.time;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Move along path
|
|
|
|
|
- FollowPath();
|
|
|
|
|
|
|
+ // Move along path – suppressed while fighting
|
|
|
|
|
+ if (!isInCombat)
|
|
|
|
|
+ FollowPath();
|
|
|
|
|
|
|
|
- // Debug visualization
|
|
|
|
|
- if (showPath && pathRenderer != null)
|
|
|
|
|
|
|
+ // Debug visualization – only update for agents near the camera to save draw calls
|
|
|
|
|
+ if (showPath && pathRenderer != null && IsNearCamera(80f))
|
|
|
{
|
|
{
|
|
|
UpdatePathVisualization();
|
|
UpdatePathVisualization();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private static Camera _mainCam;
|
|
|
|
|
+ private static float _lastCamFetch;
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>Returns true if this agent is within <paramref name="sqrDist"/> world units of the main camera.</summary>
|
|
|
|
|
+ private bool IsNearCamera(float sqrDist)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (_mainCam == null || Time.time - _lastCamFetch > 2f)
|
|
|
|
|
+ {
|
|
|
|
|
+ _mainCam = Camera.main;
|
|
|
|
|
+ _lastCamFetch = Time.time;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (_mainCam == null) return true;
|
|
|
|
|
+ return (transform.position - _mainCam.transform.position).sqrMagnitude < sqrDist * sqrDist;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Vector2Int _lastRoomCheckTile = new Vector2Int(-9999, -9999);
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
- /// Updates the current room based on position
|
|
|
|
|
|
|
+ /// Updates the current room based on position.
|
|
|
|
|
+ /// Only queries the maze when the agent has moved to a different tile.
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
private void UpdateCurrentRoom()
|
|
private void UpdateCurrentRoom()
|
|
|
{
|
|
{
|
|
|
Vector2Int tilePos = WorldToTile(transform.position);
|
|
Vector2Int tilePos = WorldToTile(transform.position);
|
|
|
|
|
+ // Skip if we're still on the same tile – saves GetRoomAtTile call every frame
|
|
|
|
|
+ if (tilePos == _lastRoomCheckTile) return;
|
|
|
|
|
+ _lastRoomCheckTile = tilePos;
|
|
|
|
|
+
|
|
|
MazeRoom room = maze.GetRoomAtTile(tilePos.x, tilePos.y);
|
|
MazeRoom room = maze.GetRoomAtTile(tilePos.x, tilePos.y);
|
|
|
|
|
|
|
|
if (room != null)
|
|
if (room != null)
|
|
@@ -218,8 +276,7 @@ public class AIAgent : MonoBehaviour
|
|
|
MazeRoom currentRoomData = maze.GetRoomAtTile(currentPos.x, currentPos.y);
|
|
MazeRoom currentRoomData = maze.GetRoomAtTile(currentPos.x, currentPos.y);
|
|
|
|
|
|
|
|
// CHECK FOR GOAL ROOM FIRST - this should work even while following path
|
|
// CHECK FOR GOAL ROOM FIRST - this should work even while following path
|
|
|
- var goalRooms = maze.GetRoomsByType(MazeRoom.RoomType.End);
|
|
|
|
|
- if (currentRoomData != null && goalRooms.Contains(currentRoomData))
|
|
|
|
|
|
|
+ if (currentRoomData != null && _goalRooms.Contains(currentRoomData))
|
|
|
{
|
|
{
|
|
|
if (!hasReachedGoal)
|
|
if (!hasReachedGoal)
|
|
|
{
|
|
{
|
|
@@ -249,19 +306,42 @@ public class AIAgent : MonoBehaviour
|
|
|
return; // Keep following existing path
|
|
return; // Keep following existing path
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // If we've committed to reaching an exit and haven't reached it yet, keep trying
|
|
|
|
|
- if (commitedToExit && targetExitTile != Vector2Int.zero)
|
|
|
|
|
|
|
+ // If we've committed to reaching an exit and haven't reached it yet, keep trying.
|
|
|
|
|
+ // Guard with pathRequestPending to avoid writing currentPath from two places.
|
|
|
|
|
+ if (commitedToExit && targetExitTile != Vector2Int.zero && !pathRequestPending)
|
|
|
{
|
|
{
|
|
|
- // Try to find a path to the committed exit
|
|
|
|
|
- if (currentPath.Count == 0)
|
|
|
|
|
|
|
+ if (currentPath.Count == 0 && currentRoomData != null)
|
|
|
{
|
|
{
|
|
|
- currentPath = FindPathInRoom(currentPos, targetExitTile, currentRoomData);
|
|
|
|
|
- currentPathIndex = 0;
|
|
|
|
|
- if (currentPath.Count > 0)
|
|
|
|
|
|
|
+ pathRequestPending = true;
|
|
|
|
|
+ var capturedExit = targetExitTile;
|
|
|
|
|
+ var capturedRoom = currentRoomData;
|
|
|
|
|
+ if (PathfindingScheduler.Instance != null)
|
|
|
|
|
+ {
|
|
|
|
|
+ PathfindingScheduler.Instance.RequestPath(new PathRequest
|
|
|
|
|
+ {
|
|
|
|
|
+ AgentId = agentId,
|
|
|
|
|
+ Start = currentPos,
|
|
|
|
|
+ Goal = capturedExit,
|
|
|
|
|
+ RoomContext = capturedRoom,
|
|
|
|
|
+ IsHallwayMode = false,
|
|
|
|
|
+ Callback = result =>
|
|
|
|
|
+ {
|
|
|
|
|
+ pathRequestPending = false;
|
|
|
|
|
+ if (result.Count > 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ currentPath = result;
|
|
|
|
|
+ currentPathIndex = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
{
|
|
{
|
|
|
- Debug.Log($"AIAgent {agentId}: Re-committed to exit {targetExitTile}");
|
|
|
|
|
- return;
|
|
|
|
|
|
|
+ currentPath = FindPathInRoom(currentPos, capturedExit, capturedRoom);
|
|
|
|
|
+ currentPathIndex = 0;
|
|
|
|
|
+ pathRequestPending = false;
|
|
|
}
|
|
}
|
|
|
|
|
+ return;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -280,7 +360,7 @@ public class AIAgent : MonoBehaviour
|
|
|
|
|
|
|
|
// Collect all unvisited rooms
|
|
// Collect all unvisited rooms
|
|
|
List<MazeRoom> unvisitedRooms = new();
|
|
List<MazeRoom> unvisitedRooms = new();
|
|
|
- foreach (var room in maze.Rooms)
|
|
|
|
|
|
|
+ foreach (var room in _allRooms)
|
|
|
{
|
|
{
|
|
|
// Skip the room we just exited (backtracking prevention)
|
|
// Skip the room we just exited (backtracking prevention)
|
|
|
if (lastRoomExitedFrom != null && room.Id == lastRoomExitedFrom.Id)
|
|
if (lastRoomExitedFrom != null && room.Id == lastRoomExitedFrom.Id)
|
|
@@ -324,7 +404,7 @@ public class AIAgent : MonoBehaviour
|
|
|
if (targetRoom == null)
|
|
if (targetRoom == null)
|
|
|
{
|
|
{
|
|
|
float closestDistance = float.MaxValue;
|
|
float closestDistance = float.MaxValue;
|
|
|
- foreach (var room in maze.Rooms)
|
|
|
|
|
|
|
+ foreach (var room in _allRooms)
|
|
|
{
|
|
{
|
|
|
// Skip the room we just exited
|
|
// Skip the room we just exited
|
|
|
if (lastRoomExitedFrom != null && room.Id == lastRoomExitedFrom.Id)
|
|
if (lastRoomExitedFrom != null && room.Id == lastRoomExitedFrom.Id)
|
|
@@ -346,8 +426,7 @@ public class AIAgent : MonoBehaviour
|
|
|
// and may wait for allies — once group logic is implemented.)
|
|
// and may wait for allies — once group logic is implemented.)
|
|
|
if (targetRoom != null)
|
|
if (targetRoom != null)
|
|
|
{
|
|
{
|
|
|
- var goalRoomsCheck = maze.GetRoomsByType(MazeRoom.RoomType.End);
|
|
|
|
|
- if (goalRoomsCheck.Contains(targetRoom))
|
|
|
|
|
|
|
+ if (_goalRooms.Contains(targetRoom))
|
|
|
{
|
|
{
|
|
|
knowsExitLocation = true;
|
|
knowsExitLocation = true;
|
|
|
// High-intelligence agents (INT > 50) pause briefly to "assess"
|
|
// High-intelligence agents (INT > 50) pause briefly to "assess"
|
|
@@ -361,33 +440,36 @@ public class AIAgent : MonoBehaviour
|
|
|
|
|
|
|
|
if (targetRoom != null)
|
|
if (targetRoom != null)
|
|
|
{
|
|
{
|
|
|
- currentPath = FindPathToNearestRoom(currentPos, targetRoom);
|
|
|
|
|
- currentPathIndex = 0;
|
|
|
|
|
-
|
|
|
|
|
- if (currentPath.Count > 0)
|
|
|
|
|
- {
|
|
|
|
|
- bool isUnvisited = !roomMemory.HasVisited(targetRoom.Id);
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- else
|
|
|
|
|
|
|
+ // Request async path – result arrives next frame via callback
|
|
|
|
|
+ if (PathfindingScheduler.Instance != null)
|
|
|
{
|
|
{
|
|
|
- // Debug.LogWarning($"AIAgent {agentId}: Failed to path to room {targetRoom.Id} from hallway at {currentPos}. Trying any adjacent room.");
|
|
|
|
|
- // Fallback: try to find ANY adjacent room and move directly toward it
|
|
|
|
|
- foreach (var room in maze.Rooms)
|
|
|
|
|
|
|
+ pathRequestPending = true;
|
|
|
|
|
+ var capturedRoom = targetRoom;
|
|
|
|
|
+ PathfindingScheduler.Instance.RequestPath(new PathRequest
|
|
|
{
|
|
{
|
|
|
- if (lastRoomExitedFrom != null && room.Id == lastRoomExitedFrom.Id)
|
|
|
|
|
- continue;
|
|
|
|
|
-
|
|
|
|
|
- currentPath = FindPathToNearestRoom(currentPos, room);
|
|
|
|
|
- if (currentPath.Count > 0)
|
|
|
|
|
|
|
+ AgentId = agentId,
|
|
|
|
|
+ Start = currentPos,
|
|
|
|
|
+ Goal = capturedRoom.GetCenter(),
|
|
|
|
|
+ IsHallwayMode = true,
|
|
|
|
|
+ Callback = result =>
|
|
|
{
|
|
{
|
|
|
- return;
|
|
|
|
|
|
|
+ pathRequestPending = false;
|
|
|
|
|
+ if (result.Count > 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ currentPath = result;
|
|
|
|
|
+ currentPathIndex = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ // else: fallback handled next update cycle
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- Debug.LogError($"AIAgent {agentId}: STUCK in hallway at {currentPos} - no paths found to any room!");
|
|
|
|
|
- return;
|
|
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ // Fallback: synchronous (scheduler not ready yet)
|
|
|
|
|
+ currentPath = FindPathToNearestRoom(currentPos, targetRoom);
|
|
|
|
|
+ currentPathIndex = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
Debug.LogWarning($"AIAgent {agentId}: In hallway but no reachable rooms!");
|
|
Debug.LogWarning($"AIAgent {agentId}: In hallway but no reachable rooms!");
|
|
@@ -403,48 +485,80 @@ public class AIAgent : MonoBehaviour
|
|
|
if (hallwayExits.Count > 0)
|
|
if (hallwayExits.Count > 0)
|
|
|
{
|
|
{
|
|
|
// Peek through each exit: if the corridor leads directly to the exit room
|
|
// Peek through each exit: if the corridor leads directly to the exit room
|
|
|
- // and the agent has enough intelligence, avoid that exit for now and pick another.
|
|
|
|
|
|
|
+ // or a dangerous room, apply intelligence/danger avoidance logic.
|
|
|
List<Vector2Int> safeExits = new List<Vector2Int>(hallwayExits);
|
|
List<Vector2Int> safeExits = new List<Vector2Int>(hallwayExits);
|
|
|
- if (!knowsExitLocation)
|
|
|
|
|
|
|
+ foreach (var exit in hallwayExits)
|
|
|
{
|
|
{
|
|
|
- var goalRoomsList = maze.GetRoomsByType(MazeRoom.RoomType.End);
|
|
|
|
|
- foreach (var exit in hallwayExits)
|
|
|
|
|
|
|
+ MazeRoom peekedRoom = PeekCorridorDestination(exit, currentRoomData);
|
|
|
|
|
+ if (peekedRoom == null) continue;
|
|
|
|
|
+
|
|
|
|
|
+ // Check if exit leads to the goal room
|
|
|
|
|
+ if (!knowsExitLocation && _goalRooms.Contains(peekedRoom))
|
|
|
{
|
|
{
|
|
|
- MazeRoom peekedRoom = PeekCorridorDestination(exit, currentRoomData);
|
|
|
|
|
- if (peekedRoom != null && goalRoomsList.Contains(peekedRoom))
|
|
|
|
|
- {
|
|
|
|
|
- knowsExitLocation = true;
|
|
|
|
|
- // Low-intelligence agents rush the exit; high-intelligence ones
|
|
|
|
|
- // reconsider (they may want allies or a better moment)
|
|
|
|
|
- if (agentStats.Intelligence > 40 && safeExits.Count > 1)
|
|
|
|
|
- {
|
|
|
|
|
- safeExits.Remove(exit); // Avoid this corridor for now
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ knowsExitLocation = true;
|
|
|
|
|
+ if (agentStats.Intelligence > 40 && safeExits.Count > 1)
|
|
|
|
|
+ safeExits.Remove(exit);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if exit leads to a monster-heavy room
|
|
|
|
|
+ if (ShouldAvoidRoom(peekedRoom) && safeExits.Count > 1)
|
|
|
|
|
+ {
|
|
|
|
|
+ avoidedRooms.Add(peekedRoom.Id);
|
|
|
|
|
+ safeExits.Remove(exit);
|
|
|
|
|
+ Debug.Log($"[Agent {agentId}] {agentName}: avoiding room {peekedRoom.Id} (threat {MonsterSpawner.GetRoomThreat(peekedRoom.Id)})");
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- if (safeExits.Count == 0) safeExits = hallwayExits; // Fallback: all exits
|
|
|
|
|
|
|
+ if (safeExits.Count == 0) safeExits = hallwayExits; // No alternative – must go through
|
|
|
|
|
|
|
|
// Pick a random hallway exit from the (possibly filtered) list
|
|
// Pick a random hallway exit from the (possibly filtered) list
|
|
|
Vector2Int chosenExit = safeExits[Random.Range(0, safeExits.Count)];
|
|
Vector2Int chosenExit = safeExits[Random.Range(0, safeExits.Count)];
|
|
|
|
|
|
|
|
- // Path directly to that exit
|
|
|
|
|
- currentPath = FindPathInRoom(currentPos, chosenExit, currentRoomData);
|
|
|
|
|
- currentPathIndex = 0;
|
|
|
|
|
-
|
|
|
|
|
- if (currentPath.Count > 0)
|
|
|
|
|
|
|
+ // Request path asynchronously via scheduler
|
|
|
|
|
+ if (PathfindingScheduler.Instance != null)
|
|
|
{
|
|
{
|
|
|
- targetExitTile = chosenExit;
|
|
|
|
|
- commitedToExit = true;
|
|
|
|
|
-
|
|
|
|
|
- // Add random delay before next decision to increase variety
|
|
|
|
|
- nextRandomWait = Time.time + Random.Range(1.5f, 3.5f);
|
|
|
|
|
-
|
|
|
|
|
|
|
+ pathRequestPending = true;
|
|
|
|
|
+ var capturedExit = chosenExit;
|
|
|
|
|
+ var capturedRoom = currentRoomData;
|
|
|
|
|
+ PathfindingScheduler.Instance.RequestPath(new PathRequest
|
|
|
|
|
+ {
|
|
|
|
|
+ AgentId = agentId,
|
|
|
|
|
+ Start = currentPos,
|
|
|
|
|
+ Goal = capturedExit,
|
|
|
|
|
+ RoomContext = capturedRoom,
|
|
|
|
|
+ IsHallwayMode = false,
|
|
|
|
|
+ Callback = result =>
|
|
|
|
|
+ {
|
|
|
|
|
+ pathRequestPending = false;
|
|
|
|
|
+ if (result.Count > 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ currentPath = result;
|
|
|
|
|
+ currentPathIndex = 0;
|
|
|
|
|
+ targetExitTile = capturedExit;
|
|
|
|
|
+ commitedToExit = true;
|
|
|
|
|
+ nextRandomWait = Time.time + Random.Range(1.5f, 3.5f);
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ commitedToExit = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
else
|
|
else
|
|
|
{
|
|
{
|
|
|
- Debug.LogWarning($"AIAgent {agentId}: Could not path to hallway exit");
|
|
|
|
|
- commitedToExit = false;
|
|
|
|
|
|
|
+ // Fallback: synchronous
|
|
|
|
|
+ currentPath = FindPathInRoom(currentPos, chosenExit, currentRoomData);
|
|
|
|
|
+ currentPathIndex = 0;
|
|
|
|
|
+ if (currentPath.Count > 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ targetExitTile = chosenExit;
|
|
|
|
|
+ commitedToExit = true;
|
|
|
|
|
+ nextRandomWait = Time.time + Random.Range(1.5f, 3.5f);
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ commitedToExit = false;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
else
|
|
else
|
|
@@ -670,80 +784,56 @@ public class AIAgent : MonoBehaviour
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
- /// Finds a path from current position to the nearest room (when in hallway)
|
|
|
|
|
|
|
+ /// Finds a path from current position to the nearest room (when in hallway).
|
|
|
|
|
+ /// Uses a min-heap open set for O(n log n) instead of O(n²).
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
private List<Vector2Int> FindPathToNearestRoom(Vector2Int start, MazeRoom targetRoom)
|
|
private List<Vector2Int> FindPathToNearestRoom(Vector2Int start, MazeRoom targetRoom)
|
|
|
{
|
|
{
|
|
|
- // Simple pathfinding through hallways using A*
|
|
|
|
|
- var openSet = new List<Vector2Int> { start };
|
|
|
|
|
|
|
+ var openSet = new MinHeap<Vector2Int>();
|
|
|
var cameFrom = new Dictionary<Vector2Int, Vector2Int>();
|
|
var cameFrom = new Dictionary<Vector2Int, Vector2Int>();
|
|
|
- var gScore = new Dictionary<Vector2Int, float> { [start] = 0 };
|
|
|
|
|
|
|
+ var gScore = new Dictionary<Vector2Int, float> { [start] = 0f };
|
|
|
var targetCenter = targetRoom.GetCenter();
|
|
var targetCenter = targetRoom.GetCenter();
|
|
|
- var fScore = new Dictionary<Vector2Int, float> { [start] = Vector2Int.Distance(start, targetCenter) };
|
|
|
|
|
|
|
|
|
|
|
|
+ openSet.Enqueue(start, Heuristic(start, targetCenter));
|
|
|
|
|
+
|
|
|
|
|
+ const int maxIterations = 2000;
|
|
|
int iterations = 0;
|
|
int iterations = 0;
|
|
|
- const int maxIterations = 2000; // Increased from 500 to handle larger mazes
|
|
|
|
|
|
|
|
|
|
- while (openSet.Count > 0 && iterations < maxIterations)
|
|
|
|
|
|
|
+ while (openSet.Count > 0 && iterations++ < maxIterations)
|
|
|
{
|
|
{
|
|
|
- iterations++;
|
|
|
|
|
-
|
|
|
|
|
- // Find node with lowest fScore
|
|
|
|
|
- Vector2Int current = openSet[0];
|
|
|
|
|
- float lowestF = fScore[current];
|
|
|
|
|
-
|
|
|
|
|
- for (int i = 1; i < openSet.Count; i++)
|
|
|
|
|
- {
|
|
|
|
|
- if (fScore[openSet[i]] < lowestF)
|
|
|
|
|
- {
|
|
|
|
|
- current = openSet[i];
|
|
|
|
|
- lowestF = fScore[current];
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ Vector2Int current = openSet.Dequeue();
|
|
|
|
|
|
|
|
- // If we reached the target room, we're done
|
|
|
|
|
- MazeRoom currentRoom = maze.GetRoomAtTile(current.x, current.y);
|
|
|
|
|
- if (currentRoom != null && currentRoom.Id == targetRoom.Id)
|
|
|
|
|
- {
|
|
|
|
|
|
|
+ // Reached the target room
|
|
|
|
|
+ MazeRoom roomAtCurrent = maze.GetRoomAtTile(current.x, current.y);
|
|
|
|
|
+ if (roomAtCurrent != null && roomAtCurrent.Id == targetRoom.Id)
|
|
|
return ReconstructPath(cameFrom, current);
|
|
return ReconstructPath(cameFrom, current);
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- openSet.Remove(current);
|
|
|
|
|
-
|
|
|
|
|
- // Check neighbors
|
|
|
|
|
- Vector2Int[] neighbors = new[]
|
|
|
|
|
- {
|
|
|
|
|
- new Vector2Int(current.x + 1, current.y),
|
|
|
|
|
- new Vector2Int(current.x - 1, current.y),
|
|
|
|
|
- new Vector2Int(current.x, current.y + 1),
|
|
|
|
|
- new Vector2Int(current.x, current.y - 1),
|
|
|
|
|
|
|
+ Vector2Int[] dirs = {
|
|
|
|
|
+ new(current.x + 1, current.y), new(current.x - 1, current.y),
|
|
|
|
|
+ new(current.x, current.y + 1), new(current.x, current.y - 1)
|
|
|
};
|
|
};
|
|
|
-
|
|
|
|
|
- foreach (var neighbor in neighbors)
|
|
|
|
|
|
|
+ foreach (var nb in dirs)
|
|
|
{
|
|
{
|
|
|
- if (!maze.IsInBounds(neighbor.x, neighbor.y) || !maze.IsWalkable(neighbor.x, neighbor.y))
|
|
|
|
|
- continue;
|
|
|
|
|
|
|
+ if (!maze.IsInBounds(nb.x, nb.y) || !maze.IsWalkable(nb.x, nb.y)) continue;
|
|
|
|
|
|
|
|
- float tentativeG = gScore[current] + 1;
|
|
|
|
|
-
|
|
|
|
|
- if (!gScore.ContainsKey(neighbor) || tentativeG < gScore[neighbor])
|
|
|
|
|
|
|
+ float tentativeG = gScore[current] + 1f;
|
|
|
|
|
+ if (!gScore.TryGetValue(nb, out float existingG) || tentativeG < existingG)
|
|
|
{
|
|
{
|
|
|
- cameFrom[neighbor] = current;
|
|
|
|
|
- gScore[neighbor] = tentativeG;
|
|
|
|
|
- fScore[neighbor] = tentativeG + Vector2Int.Distance(neighbor, targetCenter);
|
|
|
|
|
-
|
|
|
|
|
- if (!openSet.Contains(neighbor))
|
|
|
|
|
- {
|
|
|
|
|
- openSet.Add(neighbor);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ cameFrom[nb] = current;
|
|
|
|
|
+ gScore[nb] = tentativeG;
|
|
|
|
|
+ float f = tentativeG + Heuristic(nb, targetCenter);
|
|
|
|
|
+ if (openSet.Contains(nb, out _)) openSet.UpdatePriority(nb, f);
|
|
|
|
|
+ else openSet.Enqueue(nb, f);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Debug.LogWarning($"AIAgent {agentId}: FindPathToNearestRoom FAILED after {iterations} iterations. Start: {start}, Target room {targetRoom.Id} center: {targetCenter}");
|
|
|
|
|
return new List<Vector2Int>();
|
|
return new List<Vector2Int>();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private static float Heuristic(Vector2Int a, Vector2Int b)
|
|
|
|
|
+ => Mathf.Abs(a.x - b.x) + Mathf.Abs(a.y - b.y);
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Finds a boundary tile of the current room that points toward the next room
|
|
/// Finds a boundary tile of the current room that points toward the next room
|
|
|
/// </summary>
|
|
/// </summary>
|
|
@@ -777,59 +867,37 @@ public class AIAgent : MonoBehaviour
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
- /// Finds a path within a single room (limited knowledge)
|
|
|
|
|
|
|
+ /// Finds a path within a single room (limited knowledge).
|
|
|
|
|
+ /// Uses a min-heap open set for O(n log n) performance instead of O(n²).
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
private List<Vector2Int> FindPathInRoom(Vector2Int start, Vector2Int goal, MazeRoom room)
|
|
private List<Vector2Int> FindPathInRoom(Vector2Int start, Vector2Int goal, MazeRoom room)
|
|
|
{
|
|
{
|
|
|
- // Use simple A* within room bounds
|
|
|
|
|
- var openSet = new List<Vector2Int> { start };
|
|
|
|
|
|
|
+ var openSet = new MinHeap<Vector2Int>();
|
|
|
var cameFrom = new Dictionary<Vector2Int, Vector2Int>();
|
|
var cameFrom = new Dictionary<Vector2Int, Vector2Int>();
|
|
|
- var gScore = new Dictionary<Vector2Int, float> { [start] = 0 };
|
|
|
|
|
- var fScore = new Dictionary<Vector2Int, float> { [start] = Vector2Int.Distance(start, goal) };
|
|
|
|
|
|
|
+ var gScore = new Dictionary<Vector2Int, float> { [start] = 0f };
|
|
|
|
|
+
|
|
|
|
|
+ openSet.Enqueue(start, Heuristic(start, goal));
|
|
|
|
|
|
|
|
- int iterations = 0;
|
|
|
|
|
const int maxIterations = 1000;
|
|
const int maxIterations = 1000;
|
|
|
|
|
+ int iterations = 0;
|
|
|
|
|
|
|
|
- while (openSet.Count > 0 && iterations < maxIterations)
|
|
|
|
|
|
|
+ while (openSet.Count > 0 && iterations++ < maxIterations)
|
|
|
{
|
|
{
|
|
|
- iterations++;
|
|
|
|
|
-
|
|
|
|
|
- // Find node with lowest fScore
|
|
|
|
|
- Vector2Int current = openSet[0];
|
|
|
|
|
- float lowestF = fScore[current];
|
|
|
|
|
-
|
|
|
|
|
- for (int i = 1; i < openSet.Count; i++)
|
|
|
|
|
- {
|
|
|
|
|
- if (fScore[openSet[i]] < lowestF)
|
|
|
|
|
- {
|
|
|
|
|
- current = openSet[i];
|
|
|
|
|
- lowestF = fScore[current];
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ Vector2Int current = openSet.Dequeue();
|
|
|
|
|
|
|
|
if (current == goal)
|
|
if (current == goal)
|
|
|
- {
|
|
|
|
|
return ReconstructPath(cameFrom, current);
|
|
return ReconstructPath(cameFrom, current);
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- openSet.Remove(current);
|
|
|
|
|
|
|
|
|
|
- // Check only neighbors within the room
|
|
|
|
|
- var neighbors = GetRoomNeighbors(current, room);
|
|
|
|
|
- foreach (var neighbor in neighbors)
|
|
|
|
|
|
|
+ foreach (var nb in GetRoomNeighbors(current, room))
|
|
|
{
|
|
{
|
|
|
- float tentativeG = gScore[current] + 1;
|
|
|
|
|
-
|
|
|
|
|
- if (!gScore.ContainsKey(neighbor) || tentativeG < gScore[neighbor])
|
|
|
|
|
|
|
+ float tentativeG = gScore[current] + 1f;
|
|
|
|
|
+ if (!gScore.TryGetValue(nb, out float existingG) || tentativeG < existingG)
|
|
|
{
|
|
{
|
|
|
- cameFrom[neighbor] = current;
|
|
|
|
|
- gScore[neighbor] = tentativeG;
|
|
|
|
|
- fScore[neighbor] = tentativeG + Vector2Int.Distance(neighbor, goal);
|
|
|
|
|
-
|
|
|
|
|
- if (!openSet.Contains(neighbor))
|
|
|
|
|
- {
|
|
|
|
|
- openSet.Add(neighbor);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ cameFrom[nb] = current;
|
|
|
|
|
+ gScore[nb] = tentativeG;
|
|
|
|
|
+ float f = tentativeG + Heuristic(nb, goal);
|
|
|
|
|
+ if (openSet.Contains(nb, out _)) openSet.UpdatePriority(nb, f);
|
|
|
|
|
+ else openSet.Enqueue(nb, f);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -871,16 +939,19 @@ public class AIAgent : MonoBehaviour
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
- /// Reconstructs path from A* results
|
|
|
|
|
|
|
+ /// Reconstructs path from A* results.
|
|
|
|
|
+ /// Uses Add+Reverse (O(n)) instead of Insert(0,...) (O(n²)).
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
- private List<Vector2Int> ReconstructPath(Dictionary<Vector2Int, Vector2Int> cameFrom, Vector2Int current)
|
|
|
|
|
|
|
+ private static List<Vector2Int> ReconstructPath(Dictionary<Vector2Int, Vector2Int> cameFrom, Vector2Int current)
|
|
|
{
|
|
{
|
|
|
- var path = new List<Vector2Int> { current };
|
|
|
|
|
- while (cameFrom.ContainsKey(current))
|
|
|
|
|
|
|
+ var path = new List<Vector2Int>();
|
|
|
|
|
+ while (cameFrom.TryGetValue(current, out var prev))
|
|
|
{
|
|
{
|
|
|
- current = cameFrom[current];
|
|
|
|
|
- path.Insert(0, current);
|
|
|
|
|
|
|
+ path.Add(current);
|
|
|
|
|
+ current = prev;
|
|
|
}
|
|
}
|
|
|
|
|
+ path.Add(current); // start node
|
|
|
|
|
+ path.Reverse();
|
|
|
return path;
|
|
return path;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -906,7 +977,7 @@ public class AIAgent : MonoBehaviour
|
|
|
if (roomAtPos == null && currentRoom.x != -1)
|
|
if (roomAtPos == null && currentRoom.x != -1)
|
|
|
{
|
|
{
|
|
|
// We've entered a hallway - track which room we came from to prevent immediate backtracking
|
|
// We've entered a hallway - track which room we came from to prevent immediate backtracking
|
|
|
- lastRoomExitedFrom = maze.Rooms.FirstOrDefault(r => r.Id == currentRoom.x);
|
|
|
|
|
|
|
+ lastRoomExitedFrom = maze.GetRoomById(currentRoom.x);
|
|
|
commitedToExit = false; // No longer committed to that exit, now in hallway
|
|
commitedToExit = false; // No longer committed to that exit, now in hallway
|
|
|
targetExitTile = Vector2Int.zero; // Clear the target exit
|
|
targetExitTile = Vector2Int.zero; // Clear the target exit
|
|
|
}
|
|
}
|
|
@@ -1009,6 +1080,140 @@ public class AIAgent : MonoBehaviour
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
public bool HasReachedGoal => hasReachedGoal;
|
|
public bool HasReachedGoal => hasReachedGoal;
|
|
|
|
|
|
|
|
|
|
+ /// <summary>True once the agent's health reaches 0.</summary>
|
|
|
|
|
+ public bool IsDead => isDead;
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>Current health points (mirrors AgentStats).</summary>
|
|
|
|
|
+ public int CurrentHealth => agentStats?.CurrentHealth ?? 0;
|
|
|
|
|
+
|
|
|
|
|
+ // ------------------------------------------------------------------ //
|
|
|
|
|
+ // Combat //
|
|
|
|
|
+ // ------------------------------------------------------------------ //
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Called by monsters when they deal damage to this agent.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public void TakeDamage(int amount, Monster source)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (isDead) return;
|
|
|
|
|
+ agentStats.ApplyDamage(amount);
|
|
|
|
|
+ if (agentStats.IsDead)
|
|
|
|
|
+ Die();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void Die()
|
|
|
|
|
+ {
|
|
|
|
|
+ if (isDead) return;
|
|
|
|
|
+ isDead = true;
|
|
|
|
|
+ currentPath.Clear();
|
|
|
|
|
+ Debug.Log($"[Agent {agentId}] {agentName} has died!");
|
|
|
|
|
+
|
|
|
|
|
+ // Notify the manager so death stats can be tracked
|
|
|
|
|
+ var manager = FindAnyObjectByType<AIAgentManager>();
|
|
|
|
|
+ manager?.RegisterAgentDeath(this);
|
|
|
|
|
+
|
|
|
|
|
+ // Leave group cleanly
|
|
|
|
|
+ currentGroup?.RemoveMember(this);
|
|
|
|
|
+
|
|
|
|
|
+ Destroy(gameObject, 0.1f);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Combat tick: find the nearest in-range monster and attack it.
|
|
|
|
|
+ /// Sets isInCombat to halt movement while a monster is close.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private void CombatUpdate()
|
|
|
|
|
+ {
|
|
|
|
|
+ // Scan for the nearest living monster within melee range
|
|
|
|
|
+ Monster nearestMonster = null;
|
|
|
|
|
+ float bestDist = weapon.MeleeRange * weapon.MeleeRange;
|
|
|
|
|
+
|
|
|
|
|
+ foreach (var m in FindObjectsByType<Monster>())
|
|
|
|
|
+ {
|
|
|
|
|
+ if (m.IsDead) continue;
|
|
|
|
|
+ float sqDist = (m.transform.position - transform.position).sqrMagnitude;
|
|
|
|
|
+ if (sqDist < bestDist)
|
|
|
|
|
+ {
|
|
|
|
|
+ bestDist = sqDist;
|
|
|
|
|
+ nearestMonster = m;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Update combat lock – halts FollowPath while a monster is adjacent
|
|
|
|
|
+ isInCombat = nearestMonster != null;
|
|
|
|
|
+
|
|
|
|
|
+ if (nearestMonster == null) return;
|
|
|
|
|
+
|
|
|
|
|
+ // Attack on cooldown
|
|
|
|
|
+ if (Time.time - lastAttackTime < ATTACK_COOLDOWN) return;
|
|
|
|
|
+ lastAttackTime = Time.time;
|
|
|
|
|
+
|
|
|
|
|
+ // Agents have advantage on hit
|
|
|
|
|
+ if (weapon.TryHit(AGENT_HIT_ADVANTAGE))
|
|
|
|
|
+ {
|
|
|
|
|
+ int dmg = weapon.RollDamage();
|
|
|
|
|
+ bool killed = nearestMonster.TakeDamage(dmg);
|
|
|
|
|
+ Debug.Log($"[Agent {agentId}] {agentName} hit monster for {dmg} (monster HP left: {nearestMonster.CurrentHealth})");
|
|
|
|
|
+ if (killed)
|
|
|
|
|
+ {
|
|
|
|
|
+ var manager = FindAnyObjectByType<AIAgentManager>();
|
|
|
|
|
+ manager?.RegisterMonsterKill();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ Debug.Log($"[Agent {agentId}] {agentName} missed monster");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ------------------------------------------------------------------ //
|
|
|
|
|
+ // Room danger assessment //
|
|
|
|
|
+ // ------------------------------------------------------------------ //
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Returns the perceived danger score of a room.
|
|
|
|
|
+ /// Uses MonsterSpawner's threat lookup (0 = safe, higher = more dangerous).
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private int GetRoomThreat(MazeRoom room)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (room == null) return 0;
|
|
|
|
|
+ return MonsterSpawner.GetRoomThreat(room.Id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Decides whether to avoid, run through, or fight through a room.
|
|
|
|
|
+ /// Called before committing to an exit that leads to a dangerous room.
|
|
|
|
|
+ /// Returns true if the agent should avoid the target room.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private bool ShouldAvoidRoom(MazeRoom targetRoom)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (targetRoom == null) return false;
|
|
|
|
|
+ int threat = GetRoomThreat(targetRoom);
|
|
|
|
|
+ if (threat == 0) return false;
|
|
|
|
|
+
|
|
|
|
|
+ // Estimate own fighting power: health * (Strength / 50)
|
|
|
|
|
+ float power = agentStats.CurrentHealth * (agentStats.Strength / 50f);
|
|
|
|
|
+ // Risk tolerance 0=cautious, 1=reckless
|
|
|
|
|
+ float dangerThreshold = Mathf.Lerp(0.5f, 2.0f, riskTolerance);
|
|
|
|
|
+
|
|
|
|
|
+ bool tooRisky = threat > power * dangerThreshold;
|
|
|
|
|
+
|
|
|
|
|
+ if (!tooRisky) return false;
|
|
|
|
|
+
|
|
|
|
|
+ // Fast agents (Speed > 60) might choose to sprint through instead of avoiding
|
|
|
|
|
+ if (agentStats.Speed > 60)
|
|
|
|
|
+ {
|
|
|
|
|
+ float runChance = (agentStats.Speed - 60f) / 40f; // 0 at spd=60, 1 at spd=100
|
|
|
|
|
+ if (Random.value < runChance)
|
|
|
|
|
+ {
|
|
|
|
|
+ Debug.Log($"[Agent {agentId}] {agentName}: sprinting through dangerous room {targetRoom.Id}!");
|
|
|
|
|
+ return false; // Will run, not avoid
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return true; // Should avoid
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Gets agent's personalized name
|
|
/// Gets agent's personalized name
|
|
|
/// </summary>
|
|
/// </summary>
|