using UnityEngine;
using System.Collections.Generic;
using System.Linq;
///
/// AI Agent that navigates the maze with limited knowledge
/// Only knows about the room it's currently in and rooms visited by same character type
///
public class AIAgent : MonoBehaviour
{
[Header("Agent Identity")]
[SerializeField] private string agentCharacterType = "Default";
[SerializeField] private int agentId;
[Header("AI Settings")]
[SerializeField] private float movementSpeed = 2f;
[SerializeField] private float pathUpdateInterval = 0.5f;
[SerializeField] private float stoppingDistance = 0.1f;
private float actualMovementSpeed; // Per-agent speed variation to reduce synchronization
[Header("Pathfinding")]
[SerializeField] private bool showPath = true;
[SerializeField] private LineRenderer pathRenderer;
[SerializeField] private Color pathColor = Color.yellow;
private MazeController mazeController;
private MazeData maze;
private MazePathfinder pathfinder;
private AIRoomMemory roomMemory;
private List currentPath = new();
private int currentPathIndex = 0;
private float lastPathUpdate = 0f;
private Vector2Int currentRoom = Vector2Int.zero;
private Vector2Int targetRoom = Vector2Int.zero;
private Vector2Int targetExitTile = Vector2Int.zero; // Target hallway exit to move toward
private Queue recentRooms = new Queue(); // Track last N rooms to avoid backtracking
private const int RECENT_ROOMS_BUFFER_SIZE = 10; // Larger buffer = less backtracking
private MazeRoom lastRoomExitedFrom = null; // Track which room we exited to prevent immediate backtracking
private bool hasReachedGoal = false; // Track if agent has reached the goal room
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 agentRandomOffset = 0f; // Per-agent random offset to desync movement
void Start()
{
mazeController = FindAnyObjectByType();
if (mazeController == null)
{
Debug.LogError("AIAgent: MazeController not found in scene!");
enabled = false;
return;
}
maze = mazeController.GetCurrentMaze();
if (maze == null)
{
Debug.LogError("AIAgent: Current maze is null!");
enabled = false;
return;
}
pathfinder = new MazePathfinder(maze);
roomMemory = AIRoomMemoryManager.GetMemory(agentCharacterType);
// Initialize with a random start point
if (maze.StartPoints.Count > 0)
{
int startIndex = agentId % maze.StartPoints.Count; // Distribute agents across start points
Vector2Int startPos = maze.StartPoints[startIndex];
// Get the start room and spawn randomly within it
MazeRoom startRoom = maze.GetRoomAtTile(startPos.x, startPos.y);
if (startRoom != null)
{
// Spawn at random position within start room
Vector2Int randomPosInRoom = new Vector2Int(
Random.Range(startRoom.MinX + 1, startRoom.MaxX),
Random.Range(startRoom.MinY + 1, startRoom.MaxY)
);
transform.position = new Vector3(randomPosInRoom.x + 0.5f, 1f, randomPosInRoom.y + 0.5f);
}
else
{
// Fallback to exact start point
transform.position = new Vector3(startPos.x + 0.5f, 1f, startPos.y + 0.5f);
}
// Rotate to be visible from above - 90 degrees around X axis
transform.rotation = Quaternion.Euler(90, 0, 0);
UpdateCurrentRoom(); // This will add to recentRooms and visit room
Debug.Log($"AIAgent {agentId}: Initialized at {transform.position}, in room {currentRoom.x}");
}
else
{
Debug.LogError("AIAgent: No start points found in maze!");
enabled = false;
return;
}
// Setup per-agent random offset to desync decision timings (prevents all agents moving in sync)
agentRandomOffset = Random.Range(0f, pathUpdateInterval * 0.5f);
nextRandomWait = Time.time + Random.Range(2f, 4f); // Random initial wait before first decision
// Randomize movement speed slightly per agent (±20% variation)
actualMovementSpeed = movementSpeed * Random.Range(0.8f, 1.2f);
// Setup path renderer
if (showPath && pathRenderer == null)
{
pathRenderer = gameObject.AddComponent();
pathRenderer.startWidth = 0.1f;
pathRenderer.endWidth = 0.1f;
pathRenderer.material = new Material(Shader.Find("Sprites/Default"));
pathRenderer.startColor = pathColor;
pathRenderer.endColor = pathColor;
}
Debug.Log($"AIAgent {agentId} ({agentCharacterType}) spawned at {transform.position}");
}
void Update()
{
if (maze == null) return;
// Update current room
UpdateCurrentRoom();
// Update pathfinding periodically with agent-specific offset to desync behavior
if (Time.time - lastPathUpdate > pathUpdateInterval + agentRandomOffset)
{
UpdatePathToGoal();
lastPathUpdate = Time.time;
}
// Move along path
FollowPath();
// Debug visualization
if (showPath && pathRenderer != null)
{
UpdatePathVisualization();
}
}
///
/// Updates the current room based on position
///
private void UpdateCurrentRoom()
{
Vector2Int tilePos = WorldToTile(transform.position);
MazeRoom room = maze.GetRoomAtTile(tilePos.x, tilePos.y);
if (room != null)
{
if (currentRoom.x != room.Id)
{
currentRoom = new Vector2Int(room.Id, 0);
roomMemory.VisitRoom(room.Id);
// Track recent rooms to avoid immediate backtracking
recentRooms.Enqueue(room.Id);
if (recentRooms.Count > RECENT_ROOMS_BUFFER_SIZE)
recentRooms.Dequeue();
// Debug.Log($"AIAgent {agentId}: Entered room {room.Id} ({room.Type})");
}
}
}
///
/// Updates pathfinding to reach the goal
/// Agent only knows about current room and visited rooms
/// NEW LOGIC: Pick a hallway exit from current room and move straight to it
///
private void UpdatePathToGoal()
{
if (maze.ExitPoints.Count == 0)
{
Debug.LogWarning($"AIAgent {agentId}: No exit points in maze!");
return;
}
Vector2Int currentPos = WorldToTile(transform.position);
MazeRoom currentRoomData = maze.GetRoomAtTile(currentPos.x, currentPos.y);
// 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 (!hasReachedGoal)
{
hasReachedGoal = true;
currentPath.Clear();
currentPathIndex = 0;
Debug.Log($"AIAgent {agentId}: *** GOAL DETECTED *** Stopped at position {currentPos} in goal room");
}
return; // Stay stopped
}
// If we already have a valid path and we're following it, don't recalculate (stay committed)
if (currentPath.Count > 0 && currentPathIndex < currentPath.Count)
{
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)
{
// Try to find a path to the committed exit
if (currentPath.Count == 0)
{
currentPath = FindPathInRoom(currentPos, targetExitTile, currentRoomData);
currentPathIndex = 0;
if (currentPath.Count > 0)
{
Debug.Log($"AIAgent {agentId}: Re-committed to exit {targetExitTile}");
return;
}
}
}
// If in hallway (no room), just keep moving along current path
// The hallway pathfinding will naturally move us toward exits
if (currentRoomData == null)
{
// If we have a current path, keep following it through hallway
if (currentPath.Count > 0 && currentPathIndex < currentPath.Count)
{
return; // Keep following path
}
// In hallway with no path - find next room to enter
// PRIORITY 1: Find unvisited rooms, avoid the room we just exited
// Collect all unvisited rooms
List unvisitedRooms = new();
foreach (var room in maze.Rooms)
{
// Skip the room we just exited (backtracking prevention)
if (lastRoomExitedFrom != null && room.Id == lastRoomExitedFrom.Id)
continue;
// Prioritize unvisited rooms
if (!roomMemory.HasVisited(room.Id))
{
unvisitedRooms.Add(room);
}
}
MazeRoom targetRoom = null;
// Random decision: 70% closest unvisited, 30% random unvisited (increases exploration variety)
if (unvisitedRooms.Count > 0)
{
if (Random.value < 0.7f && unvisitedRooms.Count > 0)
{
// Pick closest unvisited room
float closestUnvisitedDistance = float.MaxValue;
foreach (var room in unvisitedRooms)
{
Vector2Int roomCenter = room.GetCenter();
float distance = Vector2Int.Distance(currentPos, roomCenter);
if (distance < closestUnvisitedDistance)
{
closestUnvisitedDistance = distance;
targetRoom = room;
}
}
}
else
{
// Pick random unvisited room for variety
targetRoom = unvisitedRooms[Random.Range(0, unvisitedRooms.Count)];
Debug.Log($"AIAgent {agentId}: Randomly chose unvisited room {targetRoom.Id} for variety");
}
}
// If no unvisited room found, pick nearest room (except the one we came from)
if (targetRoom == null)
{
float closestDistance = float.MaxValue;
foreach (var room in maze.Rooms)
{
// Skip the room we just exited
if (lastRoomExitedFrom != null && room.Id == lastRoomExitedFrom.Id)
continue;
Vector2Int roomCenter = room.GetCenter();
float distance = Vector2Int.Distance(currentPos, roomCenter);
if (distance < closestDistance)
{
closestDistance = distance;
targetRoom = room;
}
}
}
if (targetRoom != null)
{
currentPath = FindPathToNearestRoom(currentPos, targetRoom);
currentPathIndex = 0;
if (currentPath.Count > 0)
{
bool isUnvisited = !roomMemory.HasVisited(targetRoom.Id);
Debug.Log($"AIAgent {agentId}: In hallway, moving toward room {targetRoom.Id} ({(isUnvisited ? "unvisited" : "visited")}), path length: {currentPath.Count}");
return;
}
else
{
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)
{
if (lastRoomExitedFrom != null && room.Id == lastRoomExitedFrom.Id)
continue;
currentPath = FindPathToNearestRoom(currentPos, room);
if (currentPath.Count > 0)
{
Debug.Log($"AIAgent {agentId}: Fallback path found to room {room.Id}, length: {currentPath.Count}");
return;
}
}
Debug.LogError($"AIAgent {agentId}: STUCK in hallway at {currentPos} - no paths found to any room!");
return;
}
}
Debug.LogWarning($"AIAgent {agentId}: In hallway but no reachable rooms!");
return;
}
// MAIN LOGIC: In regular room, find a hallway exit and path to it
// Get all hallway tiles adjacent to this room
List hallwayExits = FindHallwayExits(currentRoomData, currentPos);
if (hallwayExits.Count > 0)
{
// Pick a random hallway exit to move toward (commit to it)
Vector2Int chosenExit = hallwayExits[Random.Range(0, hallwayExits.Count)];
// Path directly to that exit
currentPath = FindPathInRoom(currentPos, chosenExit, currentRoomData);
currentPathIndex = 0;
if (currentPath.Count > 0)
{
targetExitTile = chosenExit;
commitedToExit = true;
// Add random delay before next decision to increase variety
nextRandomWait = Time.time + Random.Range(1.5f, 3.5f);
Debug.Log($"AIAgent {agentId}: Moving to hallway exit at {chosenExit}, path length: {currentPath.Count}, committed");
}
else
{
Debug.LogWarning($"AIAgent {agentId}: Could not path to hallway exit");
commitedToExit = false;
}
}
else
{
Debug.LogWarning($"AIAgent {agentId}: No hallway exits found from room {currentRoomData.Id}");
commitedToExit = false;
}
}
///
/// Chooses the next room to move to
/// Searches around the current room to find adjacent rooms
/// Avoids recently visited rooms to prevent backtracking
///
private MazeRoom ChooseNextRoom(MazeRoom currentRoom)
{
// Find all adjacent rooms by checking walkable tiles in all directions from room boundaries
List connectedRooms = new();
Vector2Int roomCenter = currentRoom.GetCenter();
// Check from each direction outward from the room center
Vector2Int[] directions = new Vector2Int[]
{
Vector2Int.up, Vector2Int.down, Vector2Int.left, Vector2Int.right,
Vector2Int.up + Vector2Int.left, Vector2Int.up + Vector2Int.right,
Vector2Int.down + Vector2Int.left, Vector2Int.down + Vector2Int.right
};
// Shuffle directions for randomness
for (int i = directions.Length - 1; i > 0; i--)
{
int randomIndex = Random.Range(0, i + 1);
var temp = directions[i];
directions[i] = directions[randomIndex];
directions[randomIndex] = temp;
}
// Try each direction and look for tiles that lead to other rooms
foreach (var dir in directions)
{
for (int dist = 1; dist < 20; dist++) // Search up to 20 tiles away
{
Vector2Int testPos = roomCenter + (dir * dist);
if (!maze.IsInBounds(testPos.x, testPos.y) || !maze.IsWalkable(testPos.x, testPos.y))
continue;
MazeRoom testRoom = maze.GetRoomAtTile(testPos.x, testPos.y);
if (testRoom != null && testRoom.Id != currentRoom.Id && !connectedRooms.Contains(testRoom))
{
connectedRooms.Add(testRoom);
break; // Found a room in this direction, move to next direction
}
}
}
if (connectedRooms.Count == 0)
{
Debug.LogWarning($"AIAgent {agentId}: No adjacent rooms found from room {currentRoom.Id}");
return null;
}
// Filter out recently visited rooms (to avoid backtracking)
List nonRecentRooms = connectedRooms.Where(r => !recentRooms.Contains(r.Id)).ToList();
Debug.Log($"AIAgent {agentId}: Found {connectedRooms.Count} adjacent rooms, {nonRecentRooms.Count} non-recent");
// PRIORITY 1: Strongly prefer completely unvisited rooms (not recently visited either)
var completelyUnvisited = nonRecentRooms.Where(r => !roomMemory.HasVisited(r.Id)).ToList();
if (completelyUnvisited.Count > 0)
{
MazeRoom chosen = completelyUnvisited[Random.Range(0, completelyUnvisited.Count)];
Debug.Log($"AIAgent {agentId}: Choosing completely unvisited room {chosen.Id}");
return chosen;
}
// PRIORITY 2: Try non-recent rooms even if visited by this character type
if (nonRecentRooms.Count > 0)
{
// Among non-recent rooms, prefer unvisited by this agent's character type
var unvisitedByType = nonRecentRooms.Where(r => !roomMemory.HasVisited(r.Id)).ToList();
if (unvisitedByType.Count > 0)
{
MazeRoom chosen = unvisitedByType[Random.Range(0, unvisitedByType.Count)];
Debug.Log($"AIAgent {agentId}: Choosing unvisited-by-type room {chosen.Id}");
return chosen;
}
// Otherwise just pick a random non-recent room
MazeRoom chosen2 = nonRecentRooms[Random.Range(0, nonRecentRooms.Count)];
Debug.Log($"AIAgent {agentId}: Choosing non-recent room {chosen2.Id}");
return chosen2;
}
// PRIORITY 3: If all rooms are recent, pick one that's been visited least recently
// Find the room in connectedRooms that was added to recentRooms earliest (will be dequeued first)
MazeRoom leastRecentRoom = connectedRooms[0];
foreach (var room in connectedRooms)
{
if (!roomMemory.HasVisited(room.Id))
{
leastRecentRoom = room;
break;
}
}
Debug.Log($"AIAgent {agentId}: All rooms recent, choosing least-recent room {leastRecentRoom.Id}");
return leastRecentRoom;
}
///
/// Finds the nearest room from a position (used when in hallway)
///
private MazeRoom FindNearestRoom(Vector2Int position)
{
MazeRoom nearest = null;
float nearestDistance = float.MaxValue;
foreach (var room in maze.Rooms)
{
Vector2Int roomCenter = room.GetCenter();
float distance = Vector2Int.Distance(position, roomCenter);
if (distance < nearestDistance)
{
nearestDistance = distance;
nearest = room;
}
}
return nearest;
}
///
/// Finds all hallway exit tiles adjacent to a room
/// These are walkable tiles just outside the room boundary
///
private List FindHallwayExits(MazeRoom room, Vector2Int currentPos)
{
List exits = new();
HashSet addedExits = new();
// Check all tiles on the boundary of the room
// North boundary
for (int x = room.MinX; x <= room.MaxX; x++)
{
int y = room.MinY - 1;
if (maze.IsInBounds(x, y) && maze.IsWalkable(x, y) && !addedExits.Contains(new Vector2Int(x, y)))
{
exits.Add(new Vector2Int(x, y));
addedExits.Add(new Vector2Int(x, y));
}
}
// South boundary
for (int x = room.MinX; x <= room.MaxX; x++)
{
int y = room.MaxY + 1;
if (maze.IsInBounds(x, y) && maze.IsWalkable(x, y) && !addedExits.Contains(new Vector2Int(x, y)))
{
exits.Add(new Vector2Int(x, y));
addedExits.Add(new Vector2Int(x, y));
}
}
// West boundary
for (int y = room.MinY; y <= room.MaxY; y++)
{
int x = room.MinX - 1;
if (maze.IsInBounds(x, y) && maze.IsWalkable(x, y) && !addedExits.Contains(new Vector2Int(x, y)))
{
exits.Add(new Vector2Int(x, y));
addedExits.Add(new Vector2Int(x, y));
}
}
// East boundary
for (int y = room.MinY; y <= room.MaxY; y++)
{
int x = room.MaxX + 1;
if (maze.IsInBounds(x, y) && maze.IsWalkable(x, y) && !addedExits.Contains(new Vector2Int(x, y)))
{
exits.Add(new Vector2Int(x, y));
addedExits.Add(new Vector2Int(x, y));
}
}
if (exits.Count > 0)
Debug.Log($"AIAgent {agentId}: Found {exits.Count} hallway exits from room {room.Id}");
else
Debug.LogWarning($"AIAgent {agentId}: No hallway exits found from room {room.Id} (bounds: {room.MinX},{room.MinY} to {room.MaxX},{room.MaxY})");
return exits;
}
///
/// Finds a path from current position to the nearest room (when in hallway)
///
private List FindPathToNearestRoom(Vector2Int start, MazeRoom targetRoom)
{
// Simple pathfinding through hallways using A*
var openSet = new List { start };
var cameFrom = new Dictionary();
var gScore = new Dictionary { [start] = 0 };
var targetCenter = targetRoom.GetCenter();
var fScore = new Dictionary { [start] = Vector2Int.Distance(start, targetCenter) };
int iterations = 0;
const int maxIterations = 2000; // Increased from 500 to handle larger mazes
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];
}
}
// If we reached the target room, we're done
MazeRoom currentRoom = maze.GetRoomAtTile(current.x, current.y);
if (currentRoom != null && currentRoom.Id == targetRoom.Id)
{
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),
};
foreach (var neighbor in neighbors)
{
if (!maze.IsInBounds(neighbor.x, neighbor.y) || !maze.IsWalkable(neighbor.x, neighbor.y))
continue;
float tentativeG = gScore[current] + 1;
if (!gScore.ContainsKey(neighbor) || tentativeG < gScore[neighbor])
{
cameFrom[neighbor] = current;
gScore[neighbor] = tentativeG;
fScore[neighbor] = tentativeG + Vector2Int.Distance(neighbor, targetCenter);
if (!openSet.Contains(neighbor))
{
openSet.Add(neighbor);
}
}
}
}
Debug.LogWarning($"AIAgent {agentId}: FindPathToNearestRoom FAILED after {iterations} iterations. Start: {start}, Target room {targetRoom.Id} center: {targetCenter}");
return new List();
}
///
/// Finds a boundary tile of the current room that points toward the next room
///
private Vector2Int FindBoundaryTileToward(MazeRoom currentRoom, MazeRoom nextRoom, Vector2Int currentPos)
{
Vector2Int nextRoomCenter = nextRoom.GetCenter();
Vector2Int closestBoundary = currentRoom.GetCenter();
float closestDistance = float.MaxValue;
// Check all boundary tiles of current room
for (int x = currentRoom.MinX; x <= currentRoom.MaxX; x++)
{
for (int y = currentRoom.MinY; y <= currentRoom.MaxY; y++)
{
// Only check boundary and walkable tiles
if ((x == currentRoom.MinX || x == currentRoom.MaxX || y == currentRoom.MinY || y == currentRoom.MaxY) &&
maze.IsWalkable(x, y))
{
// Find boundary tile closest to next room center
float distToNext = Vector2Int.Distance(new Vector2Int(x, y), nextRoomCenter);
if (distToNext < closestDistance)
{
closestDistance = distToNext;
closestBoundary = new Vector2Int(x, y);
}
}
}
}
return closestBoundary;
}
///
/// Finds a path within a single room (limited knowledge)
///
private List FindPathInRoom(Vector2Int start, Vector2Int goal, MazeRoom room)
{
// Use simple A* within room bounds
var openSet = new List { start };
var cameFrom = new Dictionary();
var gScore = new Dictionary { [start] = 0 };
var fScore = new Dictionary { [start] = Vector2Int.Distance(start, goal) };
int iterations = 0;
const int maxIterations = 1000;
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];
}
}
if (current == goal)
{
return ReconstructPath(cameFrom, current);
}
openSet.Remove(current);
// Check only neighbors within the room
var neighbors = GetRoomNeighbors(current, room);
foreach (var neighbor in neighbors)
{
float tentativeG = gScore[current] + 1;
if (!gScore.ContainsKey(neighbor) || tentativeG < gScore[neighbor])
{
cameFrom[neighbor] = current;
gScore[neighbor] = tentativeG;
fScore[neighbor] = tentativeG + Vector2Int.Distance(neighbor, goal);
if (!openSet.Contains(neighbor))
{
openSet.Add(neighbor);
}
}
}
}
return new List();
}
///
/// Gets walkable neighbors within a room or at room boundary
/// Allows pathfinding to reach hallway exits outside room
///
private List GetRoomNeighbors(Vector2Int position, MazeRoom room)
{
var neighbors = new List();
Vector2Int[] directions = new[]
{
new Vector2Int(position.x + 1, position.y),
new Vector2Int(position.x - 1, position.y),
new Vector2Int(position.x, position.y + 1),
new Vector2Int(position.x, position.y - 1),
};
foreach (var dir in directions)
{
// Allow tiles within room OR immediately adjacent to room (boundary)
bool inBounds = maze.IsInBounds(dir.x, dir.y);
bool isWalkable = maze.IsWalkable(dir.x, dir.y);
bool inRoom = room.Contains(dir.x, dir.y);
bool nearBoundary = (dir.x == room.MinX - 1 || dir.x == room.MaxX + 1 ||
dir.y == room.MinY - 1 || dir.y == room.MaxY + 1);
if (inBounds && isWalkable && (inRoom || nearBoundary))
{
neighbors.Add(dir);
}
}
return neighbors;
}
///
/// Reconstructs path from A* results
///
private List ReconstructPath(Dictionary cameFrom, Vector2Int current)
{
var path = new List { current };
while (cameFrom.ContainsKey(current))
{
current = cameFrom[current];
path.Insert(0, current);
}
return path;
}
///
/// Follows the current path
///
private void FollowPath()
{
if (currentPath.Count == 0) return;
Vector2Int currentTarget = currentPath[currentPathIndex];
// Maze coordinates: X,Y → World: X,Z (Y=0 is ground level)
// Keep Y at 1f to stay above the maze floor
Vector3 targetWorldPos = new Vector3(currentTarget.x + 0.5f, 1f, currentTarget.y + 0.5f);
Vector3 direction = (targetWorldPos - transform.position).normalized;
// Move directly instead of using Translate (no rigidbody)
transform.position += direction * actualMovementSpeed * Time.deltaTime;
// Check if we've exited the room (entered hallway)
Vector2Int currentPos = WorldToTile(transform.position);
MazeRoom roomAtPos = maze.GetRoomAtTile(currentPos.x, currentPos.y);
if (roomAtPos == null && currentRoom.x != -1)
{
// We've entered a hallway - track which room we came from to prevent immediate backtracking
lastRoomExitedFrom = maze.Rooms.FirstOrDefault(r => r.Id == currentRoom.x);
commitedToExit = false; // No longer committed to that exit, now in hallway
targetExitTile = Vector2Int.zero; // Clear the target exit
Debug.Log($"AIAgent {agentId}: Exited room {currentRoom.x} into hallway");
}
// Move to next waypoint when close enough
if (Vector3.Distance(transform.position, targetWorldPos) < stoppingDistance)
{
currentPathIndex++;
if (currentPathIndex >= currentPath.Count)
{
Debug.Log($"AIAgent {agentId}: Reached path end");
currentPath.Clear();
}
}
}
///
/// Updates the path visualization
///
private void UpdatePathVisualization()
{
if (currentPath.Count == 0)
{
pathRenderer.positionCount = 0;
return;
}
var positions = new Vector3[currentPath.Count];
for (int i = 0; i < currentPath.Count; i++)
{
// Maze coordinates: X,Y → World: X,Z (Y=0 is ground level)
// Keep Y at 1f to stay above the maze floor
positions[i] = new Vector3(currentPath[i].x + 0.5f, 1f, currentPath[i].y + 0.5f);
}
pathRenderer.positionCount = positions.Length;
pathRenderer.SetPositions(positions);
}
///
/// Converts world position to tile coordinate
/// Maze coordinates: X,Y ← World: X,Z (Y=0 is ground level)
///
private Vector2Int WorldToTile(Vector3 worldPos)
{
return new Vector2Int(Mathf.FloorToInt(worldPos.x), Mathf.FloorToInt(worldPos.z));
}
///
/// Gets agent ID
///
public int AgentId => agentId;
///
/// Gets agent character type
///
public string CharacterType => agentCharacterType;
///
/// Gets current room
///
public Vector2Int CurrentRoom => currentRoom;
///
/// Gets room memory
///
public AIRoomMemory RoomMemory => roomMemory;
///
/// Gets agent ID (public accessor for triggers)
///
public int GetAgentId() => agentId;
///
/// Called by ExitRoomTrigger when agent enters the goal room
/// Immediately stops all movement
///
public void StopAtGoal()
{
hasReachedGoal = true;
currentPath.Clear();
currentPathIndex = 0;
commitedToExit = false;
Debug.Log($"AIAgent {agentId}: STOPPED by exit room trigger!");
}
}