AIAgent.cs 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906
  1. using UnityEngine;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. /// <summary>
  5. /// AI Agent that navigates the maze with limited knowledge
  6. /// Only knows about the room it's currently in and rooms visited by same character type
  7. /// </summary>
  8. public class AIAgent : MonoBehaviour
  9. {
  10. [Header("Agent Identity")]
  11. [SerializeField] private string agentCharacterType = "Default";
  12. [SerializeField] private int agentId;
  13. [Header("Agent Personalization")]
  14. private string agentName;
  15. private AgentStats agentStats;
  16. [Header("AI Settings")]
  17. [SerializeField] private float movementSpeed = 2f;
  18. [SerializeField] private float pathUpdateInterval = 0.5f;
  19. [SerializeField] private float stoppingDistance = 0.1f;
  20. private float actualMovementSpeed; // Per-agent speed variation to reduce synchronization
  21. [Header("Pathfinding")]
  22. [SerializeField] private bool showPath = true;
  23. [SerializeField] private LineRenderer pathRenderer;
  24. [SerializeField] private Color pathColor = Color.yellow;
  25. private MazeController mazeController;
  26. private MazeData maze;
  27. private MazePathfinder pathfinder;
  28. private AIRoomMemory roomMemory;
  29. private List<Vector2Int> currentPath = new();
  30. private int currentPathIndex = 0;
  31. private float lastPathUpdate = 0f;
  32. private Vector2Int currentRoom = Vector2Int.zero;
  33. private Vector2Int targetRoom = Vector2Int.zero;
  34. private Vector2Int targetExitTile = Vector2Int.zero; // Target hallway exit to move toward
  35. private Queue<int> recentRooms = new Queue<int>(); // Track last N rooms to avoid backtracking
  36. private const int RECENT_ROOMS_BUFFER_SIZE = 10; // Larger buffer = less backtracking
  37. private MazeRoom lastRoomExitedFrom = null; // Track which room we exited to prevent immediate backtracking
  38. private bool hasReachedGoal = false; // Track if agent has reached the goal room
  39. private bool commitedToExit = false; // Track if agent has committed to a specific hallway exit
  40. private float nextRandomWait = 0f; // Random wait time before exploring new areas
  41. private float agentRandomOffset = 0f; // Per-agent random offset to desync movement
  42. void Start()
  43. {
  44. mazeController = FindAnyObjectByType<MazeController>();
  45. if (mazeController == null)
  46. {
  47. Debug.LogError("AIAgent: MazeController not found in scene!");
  48. enabled = false;
  49. return;
  50. }
  51. maze = mazeController.GetCurrentMaze();
  52. if (maze == null)
  53. {
  54. Debug.LogError("AIAgent: Current maze is null!");
  55. enabled = false;
  56. return;
  57. }
  58. pathfinder = new MazePathfinder(maze);
  59. roomMemory = AIRoomMemoryManager.GetMemory(agentCharacterType);
  60. // Initialize personalization
  61. agentName = AgentNameGenerator.GenerateRandomName();
  62. agentStats = new AgentStats();
  63. gameObject.name = $"Agent_{agentId} ({agentName})";
  64. // Initialize with a random start point
  65. if (maze.StartPoints.Count > 0)
  66. {
  67. int startIndex = agentId % maze.StartPoints.Count; // Distribute agents across start points
  68. Vector2Int startPos = maze.StartPoints[startIndex];
  69. // Get the start room and spawn randomly within it
  70. MazeRoom startRoom = maze.GetRoomAtTile(startPos.x, startPos.y);
  71. if (startRoom != null)
  72. {
  73. // Spawn at random position within start room
  74. Vector2Int randomPosInRoom = new Vector2Int(
  75. Random.Range(startRoom.MinX + 1, startRoom.MaxX),
  76. Random.Range(startRoom.MinY + 1, startRoom.MaxY)
  77. );
  78. transform.position = new Vector3(randomPosInRoom.x + 0.5f, 1f, randomPosInRoom.y + 0.5f);
  79. }
  80. else
  81. {
  82. // Fallback to exact start point
  83. transform.position = new Vector3(startPos.x + 0.5f, 1f, startPos.y + 0.5f);
  84. }
  85. // Rotate to be visible from above - 90 degrees around X axis
  86. transform.rotation = Quaternion.Euler(90, 0, 0);
  87. UpdateCurrentRoom(); // This will add to recentRooms and visit room
  88. }
  89. else
  90. {
  91. Debug.LogError("AIAgent: No start points found in maze!");
  92. enabled = false;
  93. return;
  94. }
  95. // Setup per-agent random offset to desync decision timings (prevents all agents moving in sync)
  96. agentRandomOffset = Random.Range(0f, pathUpdateInterval * 0.5f);
  97. nextRandomWait = Time.time + Random.Range(2f, 4f); // Random initial wait before first decision
  98. // Scale movement speed by the agent's Speed stat:
  99. // Speed=0 → 1× base, Speed=100 → 2× base, plus ±10% desync jitter
  100. float speedMultiplier = 1f + (agentStats.Speed / 100f);
  101. actualMovementSpeed = movementSpeed * speedMultiplier * Random.Range(0.9f, 1.1f);
  102. // Setup path renderer
  103. if (showPath && pathRenderer == null)
  104. {
  105. pathRenderer = gameObject.AddComponent<LineRenderer>();
  106. pathRenderer.startWidth = 0.1f;
  107. pathRenderer.endWidth = 0.1f;
  108. pathRenderer.material = new Material(Shader.Find("Sprites/Default"));
  109. pathRenderer.startColor = pathColor;
  110. pathRenderer.endColor = pathColor;
  111. }
  112. }
  113. void Update()
  114. {
  115. if (maze == null) return;
  116. // Update current room
  117. UpdateCurrentRoom();
  118. // Update pathfinding periodically with agent-specific offset to desync behavior
  119. if (Time.time - lastPathUpdate > pathUpdateInterval + agentRandomOffset)
  120. {
  121. UpdatePathToGoal();
  122. lastPathUpdate = Time.time;
  123. }
  124. // Move along path
  125. FollowPath();
  126. // Debug visualization
  127. if (showPath && pathRenderer != null)
  128. {
  129. UpdatePathVisualization();
  130. }
  131. }
  132. /// <summary>
  133. /// Updates the current room based on position
  134. /// </summary>
  135. private void UpdateCurrentRoom()
  136. {
  137. Vector2Int tilePos = WorldToTile(transform.position);
  138. MazeRoom room = maze.GetRoomAtTile(tilePos.x, tilePos.y);
  139. if (room != null)
  140. {
  141. if (currentRoom.x != room.Id)
  142. {
  143. currentRoom = new Vector2Int(room.Id, 0);
  144. roomMemory.VisitRoom(room.Id);
  145. // Track recent rooms to avoid immediate backtracking
  146. recentRooms.Enqueue(room.Id);
  147. if (recentRooms.Count > RECENT_ROOMS_BUFFER_SIZE)
  148. recentRooms.Dequeue();
  149. }
  150. }
  151. }
  152. /// <summary>
  153. /// Updates pathfinding to reach the goal
  154. /// Agent only knows about current room and visited rooms
  155. /// NEW LOGIC: Pick a hallway exit from current room and move straight to it
  156. /// </summary>
  157. private void UpdatePathToGoal()
  158. {
  159. if (maze.ExitPoints.Count == 0)
  160. {
  161. Debug.LogWarning($"AIAgent {agentId}: No exit points in maze!");
  162. return;
  163. }
  164. Vector2Int currentPos = WorldToTile(transform.position);
  165. MazeRoom currentRoomData = maze.GetRoomAtTile(currentPos.x, currentPos.y);
  166. // CHECK FOR GOAL ROOM FIRST - this should work even while following path
  167. var goalRooms = maze.GetRoomsByType(MazeRoom.RoomType.End);
  168. if (currentRoomData != null && goalRooms.Contains(currentRoomData))
  169. {
  170. if (!hasReachedGoal)
  171. {
  172. hasReachedGoal = true;
  173. currentPath.Clear();
  174. currentPathIndex = 0;
  175. }
  176. return; // Stay stopped
  177. }
  178. // If we already have a valid path and we're following it, don't recalculate (stay committed)
  179. if (currentPath.Count > 0 && currentPathIndex < currentPath.Count)
  180. {
  181. return; // Keep following existing path
  182. }
  183. // If we've committed to reaching an exit and haven't reached it yet, keep trying
  184. if (commitedToExit && targetExitTile != Vector2Int.zero)
  185. {
  186. // Try to find a path to the committed exit
  187. if (currentPath.Count == 0)
  188. {
  189. currentPath = FindPathInRoom(currentPos, targetExitTile, currentRoomData);
  190. currentPathIndex = 0;
  191. if (currentPath.Count > 0)
  192. {
  193. Debug.Log($"AIAgent {agentId}: Re-committed to exit {targetExitTile}");
  194. return;
  195. }
  196. }
  197. }
  198. // If in hallway (no room), just keep moving along current path
  199. // The hallway pathfinding will naturally move us toward exits
  200. if (currentRoomData == null)
  201. {
  202. // If we have a current path, keep following it through hallway
  203. if (currentPath.Count > 0 && currentPathIndex < currentPath.Count)
  204. {
  205. return; // Keep following path
  206. }
  207. // In hallway with no path - find next room to enter
  208. // PRIORITY 1: Find unvisited rooms, avoid the room we just exited
  209. // Collect all unvisited rooms
  210. List<MazeRoom> unvisitedRooms = new();
  211. foreach (var room in maze.Rooms)
  212. {
  213. // Skip the room we just exited (backtracking prevention)
  214. if (lastRoomExitedFrom != null && room.Id == lastRoomExitedFrom.Id)
  215. continue;
  216. // Prioritize unvisited rooms
  217. if (!roomMemory.HasVisited(room.Id))
  218. {
  219. unvisitedRooms.Add(room);
  220. }
  221. }
  222. MazeRoom targetRoom = null;
  223. // Random decision: 70% closest unvisited, 30% random unvisited (increases exploration variety)
  224. if (unvisitedRooms.Count > 0)
  225. {
  226. if (Random.value < 0.7f && unvisitedRooms.Count > 0)
  227. {
  228. // Pick closest unvisited room
  229. float closestUnvisitedDistance = float.MaxValue;
  230. foreach (var room in unvisitedRooms)
  231. {
  232. Vector2Int roomCenter = room.GetCenter();
  233. float distance = Vector2Int.Distance(currentPos, roomCenter);
  234. if (distance < closestUnvisitedDistance)
  235. {
  236. closestUnvisitedDistance = distance;
  237. targetRoom = room;
  238. }
  239. }
  240. }
  241. else
  242. {
  243. // Pick random unvisited room for variety
  244. targetRoom = unvisitedRooms[Random.Range(0, unvisitedRooms.Count)];
  245. }
  246. }
  247. // If no unvisited room found, pick nearest room (except the one we came from)
  248. if (targetRoom == null)
  249. {
  250. float closestDistance = float.MaxValue;
  251. foreach (var room in maze.Rooms)
  252. {
  253. // Skip the room we just exited
  254. if (lastRoomExitedFrom != null && room.Id == lastRoomExitedFrom.Id)
  255. continue;
  256. Vector2Int roomCenter = room.GetCenter();
  257. float distance = Vector2Int.Distance(currentPos, roomCenter);
  258. if (distance < closestDistance)
  259. {
  260. closestDistance = distance;
  261. targetRoom = room;
  262. }
  263. }
  264. }
  265. if (targetRoom != null)
  266. {
  267. currentPath = FindPathToNearestRoom(currentPos, targetRoom);
  268. currentPathIndex = 0;
  269. if (currentPath.Count > 0)
  270. {
  271. bool isUnvisited = !roomMemory.HasVisited(targetRoom.Id);
  272. return;
  273. }
  274. else
  275. {
  276. // Debug.LogWarning($"AIAgent {agentId}: Failed to path to room {targetRoom.Id} from hallway at {currentPos}. Trying any adjacent room.");
  277. // Fallback: try to find ANY adjacent room and move directly toward it
  278. foreach (var room in maze.Rooms)
  279. {
  280. if (lastRoomExitedFrom != null && room.Id == lastRoomExitedFrom.Id)
  281. continue;
  282. currentPath = FindPathToNearestRoom(currentPos, room);
  283. if (currentPath.Count > 0)
  284. {
  285. return;
  286. }
  287. }
  288. Debug.LogError($"AIAgent {agentId}: STUCK in hallway at {currentPos} - no paths found to any room!");
  289. return;
  290. }
  291. }
  292. Debug.LogWarning($"AIAgent {agentId}: In hallway but no reachable rooms!");
  293. return;
  294. }
  295. // MAIN LOGIC: In regular room, find a hallway exit and path to it
  296. // Get all hallway tiles adjacent to this room
  297. List<Vector2Int> hallwayExits = FindHallwayExits(currentRoomData, currentPos);
  298. if (hallwayExits.Count > 0)
  299. {
  300. // Pick a random hallway exit to move toward (commit to it)
  301. Vector2Int chosenExit = hallwayExits[Random.Range(0, hallwayExits.Count)];
  302. // Path directly to that exit
  303. currentPath = FindPathInRoom(currentPos, chosenExit, currentRoomData);
  304. currentPathIndex = 0;
  305. if (currentPath.Count > 0)
  306. {
  307. targetExitTile = chosenExit;
  308. commitedToExit = true;
  309. // Add random delay before next decision to increase variety
  310. nextRandomWait = Time.time + Random.Range(1.5f, 3.5f);
  311. }
  312. else
  313. {
  314. Debug.LogWarning($"AIAgent {agentId}: Could not path to hallway exit");
  315. commitedToExit = false;
  316. }
  317. }
  318. else
  319. {
  320. Debug.LogWarning($"AIAgent {agentId}: No hallway exits found from room {currentRoomData.Id}");
  321. commitedToExit = false;
  322. }
  323. }
  324. /// <summary>
  325. /// Chooses the next room to move to
  326. /// Searches around the current room to find adjacent rooms
  327. /// Avoids recently visited rooms to prevent backtracking
  328. /// </summary>
  329. private MazeRoom ChooseNextRoom(MazeRoom currentRoom)
  330. {
  331. // Find all adjacent rooms by checking walkable tiles in all directions from room boundaries
  332. List<MazeRoom> connectedRooms = new();
  333. Vector2Int roomCenter = currentRoom.GetCenter();
  334. // Check from each direction outward from the room center
  335. Vector2Int[] directions = new Vector2Int[]
  336. {
  337. Vector2Int.up, Vector2Int.down, Vector2Int.left, Vector2Int.right,
  338. Vector2Int.up + Vector2Int.left, Vector2Int.up + Vector2Int.right,
  339. Vector2Int.down + Vector2Int.left, Vector2Int.down + Vector2Int.right
  340. };
  341. // Shuffle directions for randomness
  342. for (int i = directions.Length - 1; i > 0; i--)
  343. {
  344. int randomIndex = Random.Range(0, i + 1);
  345. var temp = directions[i];
  346. directions[i] = directions[randomIndex];
  347. directions[randomIndex] = temp;
  348. }
  349. // Try each direction and look for tiles that lead to other rooms
  350. foreach (var dir in directions)
  351. {
  352. for (int dist = 1; dist < 20; dist++) // Search up to 20 tiles away
  353. {
  354. Vector2Int testPos = roomCenter + (dir * dist);
  355. if (!maze.IsInBounds(testPos.x, testPos.y) || !maze.IsWalkable(testPos.x, testPos.y))
  356. continue;
  357. MazeRoom testRoom = maze.GetRoomAtTile(testPos.x, testPos.y);
  358. if (testRoom != null && testRoom.Id != currentRoom.Id && !connectedRooms.Contains(testRoom))
  359. {
  360. connectedRooms.Add(testRoom);
  361. break; // Found a room in this direction, move to next direction
  362. }
  363. }
  364. }
  365. if (connectedRooms.Count == 0)
  366. {
  367. Debug.LogWarning($"AIAgent {agentId}: No adjacent rooms found from room {currentRoom.Id}");
  368. return null;
  369. }
  370. // Filter out recently visited rooms (to avoid backtracking)
  371. List<MazeRoom> nonRecentRooms = connectedRooms.Where(r => !recentRooms.Contains(r.Id)).ToList();
  372. // PRIORITY 1: Strongly prefer completely unvisited rooms (not recently visited either)
  373. var completelyUnvisited = nonRecentRooms.Where(r => !roomMemory.HasVisited(r.Id)).ToList();
  374. if (completelyUnvisited.Count > 0)
  375. {
  376. MazeRoom chosen = completelyUnvisited[Random.Range(0, completelyUnvisited.Count)];
  377. return chosen;
  378. }
  379. // PRIORITY 2: Try non-recent rooms even if visited by this character type
  380. if (nonRecentRooms.Count > 0)
  381. {
  382. // Among non-recent rooms, prefer unvisited by this agent's character type
  383. var unvisitedByType = nonRecentRooms.Where(r => !roomMemory.HasVisited(r.Id)).ToList();
  384. if (unvisitedByType.Count > 0)
  385. {
  386. MazeRoom chosen = unvisitedByType[Random.Range(0, unvisitedByType.Count)];
  387. return chosen;
  388. }
  389. // Otherwise just pick a random non-recent room
  390. MazeRoom chosen2 = nonRecentRooms[Random.Range(0, nonRecentRooms.Count)];
  391. return chosen2;
  392. }
  393. // PRIORITY 3: If all rooms are recent, pick one that's been visited least recently
  394. // Find the room in connectedRooms that was added to recentRooms earliest (will be dequeued first)
  395. MazeRoom leastRecentRoom = connectedRooms[0];
  396. foreach (var room in connectedRooms)
  397. {
  398. if (!roomMemory.HasVisited(room.Id))
  399. {
  400. leastRecentRoom = room;
  401. break;
  402. }
  403. }
  404. Debug.Log($"AIAgent {agentId}: All rooms recent, choosing least-recent room {leastRecentRoom.Id}");
  405. return leastRecentRoom;
  406. }
  407. /// <summary>
  408. /// Finds the nearest room from a position (used when in hallway)
  409. /// </summary>
  410. private MazeRoom FindNearestRoom(Vector2Int position)
  411. {
  412. MazeRoom nearest = null;
  413. float nearestDistance = float.MaxValue;
  414. foreach (var room in maze.Rooms)
  415. {
  416. Vector2Int roomCenter = room.GetCenter();
  417. float distance = Vector2Int.Distance(position, roomCenter);
  418. if (distance < nearestDistance)
  419. {
  420. nearestDistance = distance;
  421. nearest = room;
  422. }
  423. }
  424. return nearest;
  425. }
  426. /// <summary>
  427. /// Finds all hallway exit tiles adjacent to a room
  428. /// These are walkable tiles just outside the room boundary
  429. /// </summary>
  430. private List<Vector2Int> FindHallwayExits(MazeRoom room, Vector2Int currentPos)
  431. {
  432. List<Vector2Int> exits = new();
  433. HashSet<Vector2Int> addedExits = new();
  434. // Check all tiles on the boundary of the room
  435. // North boundary
  436. for (int x = room.MinX; x <= room.MaxX; x++)
  437. {
  438. int y = room.MinY - 1;
  439. if (maze.IsInBounds(x, y) && maze.IsWalkable(x, y) && !addedExits.Contains(new Vector2Int(x, y)))
  440. {
  441. exits.Add(new Vector2Int(x, y));
  442. addedExits.Add(new Vector2Int(x, y));
  443. }
  444. }
  445. // South boundary
  446. for (int x = room.MinX; x <= room.MaxX; x++)
  447. {
  448. int y = room.MaxY + 1;
  449. if (maze.IsInBounds(x, y) && maze.IsWalkable(x, y) && !addedExits.Contains(new Vector2Int(x, y)))
  450. {
  451. exits.Add(new Vector2Int(x, y));
  452. addedExits.Add(new Vector2Int(x, y));
  453. }
  454. }
  455. // West boundary
  456. for (int y = room.MinY; y <= room.MaxY; y++)
  457. {
  458. int x = room.MinX - 1;
  459. if (maze.IsInBounds(x, y) && maze.IsWalkable(x, y) && !addedExits.Contains(new Vector2Int(x, y)))
  460. {
  461. exits.Add(new Vector2Int(x, y));
  462. addedExits.Add(new Vector2Int(x, y));
  463. }
  464. }
  465. // East boundary
  466. for (int y = room.MinY; y <= room.MaxY; y++)
  467. {
  468. int x = room.MaxX + 1;
  469. if (maze.IsInBounds(x, y) && maze.IsWalkable(x, y) && !addedExits.Contains(new Vector2Int(x, y)))
  470. {
  471. exits.Add(new Vector2Int(x, y));
  472. addedExits.Add(new Vector2Int(x, y));
  473. }
  474. }
  475. return exits;
  476. }
  477. /// <summary>
  478. /// Finds a path from current position to the nearest room (when in hallway)
  479. /// </summary>
  480. private List<Vector2Int> FindPathToNearestRoom(Vector2Int start, MazeRoom targetRoom)
  481. {
  482. // Simple pathfinding through hallways using A*
  483. var openSet = new List<Vector2Int> { start };
  484. var cameFrom = new Dictionary<Vector2Int, Vector2Int>();
  485. var gScore = new Dictionary<Vector2Int, float> { [start] = 0 };
  486. var targetCenter = targetRoom.GetCenter();
  487. var fScore = new Dictionary<Vector2Int, float> { [start] = Vector2Int.Distance(start, targetCenter) };
  488. int iterations = 0;
  489. const int maxIterations = 2000; // Increased from 500 to handle larger mazes
  490. while (openSet.Count > 0 && iterations < maxIterations)
  491. {
  492. iterations++;
  493. // Find node with lowest fScore
  494. Vector2Int current = openSet[0];
  495. float lowestF = fScore[current];
  496. for (int i = 1; i < openSet.Count; i++)
  497. {
  498. if (fScore[openSet[i]] < lowestF)
  499. {
  500. current = openSet[i];
  501. lowestF = fScore[current];
  502. }
  503. }
  504. // If we reached the target room, we're done
  505. MazeRoom currentRoom = maze.GetRoomAtTile(current.x, current.y);
  506. if (currentRoom != null && currentRoom.Id == targetRoom.Id)
  507. {
  508. return ReconstructPath(cameFrom, current);
  509. }
  510. openSet.Remove(current);
  511. // Check neighbors
  512. Vector2Int[] neighbors = new[]
  513. {
  514. new Vector2Int(current.x + 1, current.y),
  515. new Vector2Int(current.x - 1, current.y),
  516. new Vector2Int(current.x, current.y + 1),
  517. new Vector2Int(current.x, current.y - 1),
  518. };
  519. foreach (var neighbor in neighbors)
  520. {
  521. if (!maze.IsInBounds(neighbor.x, neighbor.y) || !maze.IsWalkable(neighbor.x, neighbor.y))
  522. continue;
  523. float tentativeG = gScore[current] + 1;
  524. if (!gScore.ContainsKey(neighbor) || tentativeG < gScore[neighbor])
  525. {
  526. cameFrom[neighbor] = current;
  527. gScore[neighbor] = tentativeG;
  528. fScore[neighbor] = tentativeG + Vector2Int.Distance(neighbor, targetCenter);
  529. if (!openSet.Contains(neighbor))
  530. {
  531. openSet.Add(neighbor);
  532. }
  533. }
  534. }
  535. }
  536. // Debug.LogWarning($"AIAgent {agentId}: FindPathToNearestRoom FAILED after {iterations} iterations. Start: {start}, Target room {targetRoom.Id} center: {targetCenter}");
  537. return new List<Vector2Int>();
  538. }
  539. /// <summary>
  540. /// Finds a boundary tile of the current room that points toward the next room
  541. /// </summary>
  542. private Vector2Int FindBoundaryTileToward(MazeRoom currentRoom, MazeRoom nextRoom, Vector2Int currentPos)
  543. {
  544. Vector2Int nextRoomCenter = nextRoom.GetCenter();
  545. Vector2Int closestBoundary = currentRoom.GetCenter();
  546. float closestDistance = float.MaxValue;
  547. // Check all boundary tiles of current room
  548. for (int x = currentRoom.MinX; x <= currentRoom.MaxX; x++)
  549. {
  550. for (int y = currentRoom.MinY; y <= currentRoom.MaxY; y++)
  551. {
  552. // Only check boundary and walkable tiles
  553. if ((x == currentRoom.MinX || x == currentRoom.MaxX || y == currentRoom.MinY || y == currentRoom.MaxY) &&
  554. maze.IsWalkable(x, y))
  555. {
  556. // Find boundary tile closest to next room center
  557. float distToNext = Vector2Int.Distance(new Vector2Int(x, y), nextRoomCenter);
  558. if (distToNext < closestDistance)
  559. {
  560. closestDistance = distToNext;
  561. closestBoundary = new Vector2Int(x, y);
  562. }
  563. }
  564. }
  565. }
  566. return closestBoundary;
  567. }
  568. /// <summary>
  569. /// Finds a path within a single room (limited knowledge)
  570. /// </summary>
  571. private List<Vector2Int> FindPathInRoom(Vector2Int start, Vector2Int goal, MazeRoom room)
  572. {
  573. // Use simple A* within room bounds
  574. var openSet = new List<Vector2Int> { start };
  575. var cameFrom = new Dictionary<Vector2Int, Vector2Int>();
  576. var gScore = new Dictionary<Vector2Int, float> { [start] = 0 };
  577. var fScore = new Dictionary<Vector2Int, float> { [start] = Vector2Int.Distance(start, goal) };
  578. int iterations = 0;
  579. const int maxIterations = 1000;
  580. while (openSet.Count > 0 && iterations < maxIterations)
  581. {
  582. iterations++;
  583. // Find node with lowest fScore
  584. Vector2Int current = openSet[0];
  585. float lowestF = fScore[current];
  586. for (int i = 1; i < openSet.Count; i++)
  587. {
  588. if (fScore[openSet[i]] < lowestF)
  589. {
  590. current = openSet[i];
  591. lowestF = fScore[current];
  592. }
  593. }
  594. if (current == goal)
  595. {
  596. return ReconstructPath(cameFrom, current);
  597. }
  598. openSet.Remove(current);
  599. // Check only neighbors within the room
  600. var neighbors = GetRoomNeighbors(current, room);
  601. foreach (var neighbor in neighbors)
  602. {
  603. float tentativeG = gScore[current] + 1;
  604. if (!gScore.ContainsKey(neighbor) || tentativeG < gScore[neighbor])
  605. {
  606. cameFrom[neighbor] = current;
  607. gScore[neighbor] = tentativeG;
  608. fScore[neighbor] = tentativeG + Vector2Int.Distance(neighbor, goal);
  609. if (!openSet.Contains(neighbor))
  610. {
  611. openSet.Add(neighbor);
  612. }
  613. }
  614. }
  615. }
  616. return new List<Vector2Int>();
  617. }
  618. /// <summary>
  619. /// Gets walkable neighbors within a room or at room boundary
  620. /// Allows pathfinding to reach hallway exits outside room
  621. /// </summary>
  622. private List<Vector2Int> GetRoomNeighbors(Vector2Int position, MazeRoom room)
  623. {
  624. var neighbors = new List<Vector2Int>();
  625. Vector2Int[] directions = new[]
  626. {
  627. new Vector2Int(position.x + 1, position.y),
  628. new Vector2Int(position.x - 1, position.y),
  629. new Vector2Int(position.x, position.y + 1),
  630. new Vector2Int(position.x, position.y - 1),
  631. };
  632. foreach (var dir in directions)
  633. {
  634. // Allow tiles within room OR immediately adjacent to room (boundary)
  635. bool inBounds = maze.IsInBounds(dir.x, dir.y);
  636. bool isWalkable = maze.IsWalkable(dir.x, dir.y);
  637. bool inRoom = room.Contains(dir.x, dir.y);
  638. bool nearBoundary = (dir.x == room.MinX - 1 || dir.x == room.MaxX + 1 ||
  639. dir.y == room.MinY - 1 || dir.y == room.MaxY + 1);
  640. if (inBounds && isWalkable && (inRoom || nearBoundary))
  641. {
  642. neighbors.Add(dir);
  643. }
  644. }
  645. return neighbors;
  646. }
  647. /// <summary>
  648. /// Reconstructs path from A* results
  649. /// </summary>
  650. private List<Vector2Int> ReconstructPath(Dictionary<Vector2Int, Vector2Int> cameFrom, Vector2Int current)
  651. {
  652. var path = new List<Vector2Int> { current };
  653. while (cameFrom.ContainsKey(current))
  654. {
  655. current = cameFrom[current];
  656. path.Insert(0, current);
  657. }
  658. return path;
  659. }
  660. /// <summary>
  661. /// Follows the current path
  662. /// </summary>
  663. private void FollowPath()
  664. {
  665. if (currentPath.Count == 0) return;
  666. Vector2Int currentTarget = currentPath[currentPathIndex];
  667. // Maze coordinates: X,Y → World: X,Z (Y=0 is ground level)
  668. // Keep Y at 1f to stay above the maze floor
  669. Vector3 targetWorldPos = new Vector3(currentTarget.x + 0.5f, 1f, currentTarget.y + 0.5f);
  670. Vector3 direction = (targetWorldPos - transform.position).normalized;
  671. // Move directly instead of using Translate (no rigidbody)
  672. transform.position += direction * actualMovementSpeed * Time.deltaTime;
  673. // Check if we've exited the room (entered hallway)
  674. Vector2Int currentPos = WorldToTile(transform.position);
  675. MazeRoom roomAtPos = maze.GetRoomAtTile(currentPos.x, currentPos.y);
  676. if (roomAtPos == null && currentRoom.x != -1)
  677. {
  678. // We've entered a hallway - track which room we came from to prevent immediate backtracking
  679. lastRoomExitedFrom = maze.Rooms.FirstOrDefault(r => r.Id == currentRoom.x);
  680. commitedToExit = false; // No longer committed to that exit, now in hallway
  681. targetExitTile = Vector2Int.zero; // Clear the target exit
  682. }
  683. // Move to next waypoint when close enough
  684. if (Vector3.Distance(transform.position, targetWorldPos) < stoppingDistance)
  685. {
  686. currentPathIndex++;
  687. if (currentPathIndex >= currentPath.Count)
  688. {
  689. currentPath.Clear();
  690. }
  691. }
  692. }
  693. /// <summary>
  694. /// Updates the path visualization
  695. /// </summary>
  696. private void UpdatePathVisualization()
  697. {
  698. if (currentPath.Count == 0)
  699. {
  700. pathRenderer.positionCount = 0;
  701. return;
  702. }
  703. var positions = new Vector3[currentPath.Count];
  704. for (int i = 0; i < currentPath.Count; i++)
  705. {
  706. // Maze coordinates: X,Y → World: X,Z (Y=0 is ground level)
  707. // Keep Y at 1f to stay above the maze floor
  708. positions[i] = new Vector3(currentPath[i].x + 0.5f, 1f, currentPath[i].y + 0.5f);
  709. }
  710. pathRenderer.positionCount = positions.Length;
  711. pathRenderer.SetPositions(positions);
  712. }
  713. /// <summary>
  714. /// Converts world position to tile coordinate
  715. /// Maze coordinates: X,Y ← World: X,Z (Y=0 is ground level)
  716. /// </summary>
  717. private Vector2Int WorldToTile(Vector3 worldPos)
  718. {
  719. return new Vector2Int(Mathf.FloorToInt(worldPos.x), Mathf.FloorToInt(worldPos.z));
  720. }
  721. /// <summary>
  722. /// Gets agent ID
  723. /// </summary>
  724. public int AgentId => agentId;
  725. /// <summary>
  726. /// Gets agent character type
  727. /// </summary>
  728. public string CharacterType => agentCharacterType;
  729. /// <summary>
  730. /// Gets current room
  731. /// </summary>
  732. public Vector2Int CurrentRoom => currentRoom;
  733. /// <summary>
  734. /// Gets room memory
  735. /// </summary>
  736. public AIRoomMemory RoomMemory => roomMemory;
  737. /// <summary>
  738. /// Gets agent ID (public accessor for triggers)
  739. /// </summary>
  740. public int GetAgentId() => agentId;
  741. /// <summary>
  742. /// Called by ExitRoomTrigger when agent enters the goal room
  743. /// Immediately stops all movement
  744. /// </summary>
  745. public void StopAtGoal()
  746. {
  747. hasReachedGoal = true;
  748. currentPath.Clear();
  749. currentPathIndex = 0;
  750. commitedToExit = false;
  751. }
  752. /// <summary>
  753. /// Gets whether this agent has reached the goal
  754. /// </summary>
  755. public bool HasReachedGoal => hasReachedGoal;
  756. /// <summary>
  757. /// Gets agent's personalized name
  758. /// </summary>
  759. public string AgentName => agentName;
  760. /// <summary>
  761. /// Gets agent's stats
  762. /// </summary>
  763. public AgentStats Stats => agentStats;
  764. void OnMouseDown()
  765. {
  766. // Called when this GameObject is clicked
  767. AgentInfoPanel.ShowAgentInfo(this);
  768. }
  769. public void SetShowPath(bool show)
  770. {
  771. showPath = show;
  772. if (pathRenderer != null)
  773. {
  774. pathRenderer.enabled = show;
  775. }
  776. }
  777. public bool GetShowPath() => showPath;
  778. }