TeamTravelSystem.cs 82 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219
  1. using UnityEngine;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. /// <summary>
  6. /// New comprehensive travel system that handles movement costs, pathfinding, and visual planning.
  7. /// Integrates with SimpleTeamPlacement and MapMaker2 for seamless map-based travel.
  8. /// </summary>
  9. public class TeamTravelSystem : MonoBehaviour
  10. {
  11. [Header("Movement Costs")]
  12. [Range(0.1f, 5f)]
  13. public float plainsMovementCost = 1f;
  14. [Range(0.1f, 5f)]
  15. public float forestMovementCost = 2f;
  16. [Range(0.1f, 5f)]
  17. public float mountainMovementCost = 4f;
  18. [Range(0.1f, 2f)]
  19. public float roadMovementCost = 0.5f;
  20. [Range(0.1f, 2f)]
  21. public float townMovementCost = 0.1f; // Almost instant travel inside towns
  22. [Range(0.1f, 2f)]
  23. public float bridgeMovementCost = 0.6f;
  24. [Header("Special Travel Costs")]
  25. [Range(1, 100)]
  26. public int ferryBaseCost = 25; // Gold cost for ferry
  27. [Range(0.1f, 2f)]
  28. public float ferryMovementCost = 0.7f; // Faster than walking but costs money
  29. [Range(1, 50)]
  30. public int tunnelTorchCost = 10; // Torch cost for tunnel travel
  31. [Range(0.1f, 2f)]
  32. public float tunnelWithTorchCost = 0.8f; // Fast with torch
  33. [Range(1f, 5f)]
  34. public float tunnelWithoutTorchCost = 3f; // Slow and dangerous without torch
  35. [Range(1, 20)]
  36. public int bridgeTollCost = 5; // Gold cost for major bridge crossings
  37. [Range(1, 30)]
  38. public int mountainPassCost = 15; // Guide cost for dangerous mountain passes
  39. [Header("River Crossing")]
  40. [Range(2f, 10f)]
  41. public float riverCrossingCost = 2f; // Reduced from 6f - more reasonable river crossing
  42. [Range(1f, 8f)]
  43. public float lakeCrossingCost = 2.5f; // Reduced from 4f - more reasonable lake crossing
  44. [Range(5f, 15f)]
  45. public float oceanCrossingCost = 8f; // Reduced from 10f but still expensive
  46. [Header("Visual Path Settings")]
  47. public Material pathLineMaterial;
  48. public Color standardPathColor = Color.blue;
  49. public Color expensivePathColor = Color.red;
  50. public Color alternativePathColor = Color.yellow;
  51. [Range(0.1f, 5f)]
  52. public float pathLineWidth = 2.5f; // Increased for much better visibility
  53. [Range(-10f, 10f)]
  54. public float pathLineHeight = 1f; // Height above map tiles for path lines
  55. [Header("Debug")]
  56. public bool showDebugLogs = false;
  57. public bool showMovementCosts = false;
  58. // References
  59. private SimpleTeamPlacement teamPlacement;
  60. private MapMaker2 mapMaker;
  61. private Camera mapCamera;
  62. private TravelUI travelUI;
  63. private CombatEventPopupUXML combatEventPopup;
  64. // Travel state
  65. private Vector2Int? plannedDestination;
  66. private List<TravelRoute> availableRoutes = new List<TravelRoute>();
  67. private List<GameObject> currentPathVisualizations = new List<GameObject>(); // Store multiple route lines
  68. private bool isTraveling = false;
  69. private int currentRouteIndex = 0; // For cycling through route visualizations
  70. // Journey cancellation
  71. private bool shouldCancelJourney = false;
  72. private Coroutine currentTravelCoroutine = null;
  73. private TravelRoute currentRoute = null;
  74. // Time skip functionality
  75. [Header("Time Controls")]
  76. public float timeMultiplier = 1f; // Current time speed multiplier
  77. public float[] timeSpeedOptions = { 1f, 2f, 4f, 8f }; // Available speed options
  78. private int currentTimeSpeedIndex = 0;
  79. // Static instance
  80. public static TeamTravelSystem Instance { get; private set; }
  81. void Awake()
  82. {
  83. if (Instance == null)
  84. {
  85. Instance = this;
  86. }
  87. else
  88. {
  89. Debug.LogWarning("Multiple TeamTravelSystem instances found. Destroying this one.");
  90. Destroy(gameObject);
  91. return;
  92. }
  93. }
  94. void Start()
  95. {
  96. // Find required components
  97. teamPlacement = FindFirstObjectByType<SimpleTeamPlacement>();
  98. mapMaker = FindFirstObjectByType<MapMaker2>();
  99. mapCamera = Camera.main;
  100. travelUI = FindFirstObjectByType<TravelUI>();
  101. combatEventPopup = FindFirstObjectByType<CombatEventPopupUXML>();
  102. // Initialize pathLineMaterial if not assigned
  103. if (pathLineMaterial == null)
  104. {
  105. pathLineMaterial = CreateDefaultPathMaterial();
  106. }
  107. if (teamPlacement == null)
  108. {
  109. Debug.LogError("SimpleTeamPlacement not found! TeamTravelSystem requires it.");
  110. }
  111. if (mapMaker == null)
  112. {
  113. Debug.LogError("MapMaker2 not found! TeamTravelSystem requires it.");
  114. }
  115. if (travelUI == null)
  116. {
  117. Debug.LogWarning("TravelUI not found! UI features will be limited.");
  118. }
  119. }
  120. void Update()
  121. {
  122. HandleInput();
  123. }
  124. private void HandleInput()
  125. {
  126. // Time speed controls (available during travel)
  127. if (isTraveling)
  128. {
  129. if (Input.GetKeyDown(KeyCode.Period) || Input.GetKeyDown(KeyCode.RightArrow)) // > key or right arrow
  130. {
  131. IncreaseTimeSpeed();
  132. }
  133. else if (Input.GetKeyDown(KeyCode.Comma) || Input.GetKeyDown(KeyCode.LeftArrow)) // < key or left arrow
  134. {
  135. DecreaseTimeSpeed();
  136. }
  137. else if (Input.GetKeyDown(KeyCode.Space)) // Space to reset speed
  138. {
  139. ResetTimeSpeed();
  140. }
  141. else if (Input.GetKeyDown(KeyCode.Escape) || Input.GetKeyDown(KeyCode.C)) // ESC or C to cancel journey
  142. {
  143. CancelJourney();
  144. }
  145. }
  146. // Handle map clicking for travel planning (only if click is outside UI panel)
  147. if (Input.GetMouseButtonDown(0) && !isTraveling)
  148. {
  149. // Check if click is within the UI panel bounds
  150. if (travelUI != null && travelUI.IsVisible && travelUI.IsPointWithinUI(Input.mousePosition))
  151. {
  152. return; // Block map interaction if clicking within UI panel
  153. }
  154. // Check if click is within the combat popup bounds
  155. if (combatEventPopup != null && combatEventPopup.IsVisible && combatEventPopup.IsPointWithinUI(Input.mousePosition))
  156. {
  157. return; // Block map interaction if clicking within combat popup
  158. }
  159. Vector3 mouseWorldPos = GetMouseWorldPosition();
  160. if (mouseWorldPos != Vector3.zero)
  161. {
  162. Vector2Int clickedTile = WorldToTilePosition(mouseWorldPos);
  163. PlanTravelTo(clickedTile);
  164. }
  165. }
  166. // Check if TravelUI is blocking other input (non-map clicks)
  167. if (IsTravelUIBlocking())
  168. {
  169. return; // Block other inputs but allow map clicks above
  170. }
  171. // Cancel travel planning
  172. if (Input.GetKeyDown(KeyCode.Escape))
  173. {
  174. CancelTravelPlanning();
  175. }
  176. // Cycle through route visualizations
  177. if (Input.GetKeyDown(KeyCode.Tab) && availableRoutes.Count > 1)
  178. {
  179. currentRouteIndex = (currentRouteIndex + 1) % availableRoutes.Count;
  180. var selectedRoute = availableRoutes[currentRouteIndex];
  181. // Highlight the selected route without clearing others
  182. HighlightSpecificRoute(selectedRoute);
  183. // Update the UI to show the new selected route
  184. if (travelUI != null)
  185. {
  186. travelUI.UpdateUIForSelectedRoute(selectedRoute);
  187. }
  188. if (showDebugLogs)
  189. {
  190. Debug.Log($"Switched to {selectedRoute.routeType} route (Time: {selectedRoute.estimatedTime:F1}h)");
  191. }
  192. }
  193. // Debug key to show movement costs
  194. if (Input.GetKeyDown(KeyCode.C) && showMovementCosts)
  195. {
  196. ShowMovementCostInfo();
  197. }
  198. // Toggle debug logs
  199. if (Input.GetKeyDown(KeyCode.F8))
  200. {
  201. showDebugLogs = !showDebugLogs;
  202. Debug.Log($"Travel System Debug Logs: {(showDebugLogs ? "ENABLED" : "DISABLED")}");
  203. }
  204. }
  205. /// <summary>
  206. /// Plans travel to the specified destination and shows available routes
  207. /// </summary>
  208. public void PlanTravelTo(Vector2Int destination)
  209. {
  210. if (!teamPlacement.IsTeamPlaced())
  211. {
  212. Debug.LogWarning("❌ Cannot plan travel: Team not placed yet");
  213. return;
  214. }
  215. Vector2Int currentPosition = teamPlacement.GetTeamPosition();
  216. if (currentPosition == destination)
  217. {
  218. Debug.Log("Already at destination!");
  219. return;
  220. }
  221. plannedDestination = destination;
  222. currentRouteIndex = 0; // Reset route index for new destination
  223. if (showDebugLogs)
  224. {
  225. Debug.Log($"🗺️ Planning travel from {currentPosition} to {destination}");
  226. }
  227. // Calculate different route options
  228. CalculateRoutes(currentPosition, destination);
  229. // If UI is already visible, refresh it with new routes
  230. if (travelUI != null && travelUI.IsVisible)
  231. {
  232. Debug.Log("🔄 TeamTravelSystem: UI is visible, refreshing with new routes");
  233. travelUI.RefreshWithNewRoutes(availableRoutes);
  234. }
  235. // Show all available routes visually with different colors
  236. if (availableRoutes.Count > 0)
  237. {
  238. // Clear any existing visualizations first
  239. ClearPathVisualization();
  240. // Select the fastest route (shortest estimated time) by default
  241. var routesByTime = availableRoutes.OrderBy(r => r.estimatedTime).ToList();
  242. TravelRoute selectedRoute = routesByTime.First();
  243. // Update currentRouteIndex to match the selected route in the original list
  244. currentRouteIndex = availableRoutes.IndexOf(selectedRoute);
  245. // Visualize all routes with the fastest one highlighted on top
  246. VisualizeAllRoutes(availableRoutes);
  247. if (showDebugLogs)
  248. {
  249. Debug.Log($"🔧 About to show UI for {selectedRoute.routeType} route with {selectedRoute.path.Count} waypoints");
  250. }
  251. // Show travel UI if available
  252. ShowTravelUIIfAvailable(destination, availableRoutes);
  253. if (showDebugLogs)
  254. {
  255. Debug.Log($"Found {availableRoutes.Count} route options. Selected {selectedRoute.routeType} route: {selectedRoute.estimatedTime:F1}h time");
  256. Debug.Log("Use TAB to cycle through different route options");
  257. }
  258. }
  259. }
  260. /// <summary>
  261. /// Calculates different route options including fastest, shortest, and cheapest routes
  262. /// </summary>
  263. private void CalculateRoutes(Vector2Int start, Vector2Int destination)
  264. {
  265. availableRoutes.Clear();
  266. var mapData = mapMaker.GetMapData();
  267. if (mapData == null) return;
  268. if (showDebugLogs)
  269. {
  270. Debug.Log($"🔍 Calculating routes from {start} to {destination}");
  271. }
  272. // Fastest route (heavily prefers roads and fast terrain)
  273. var fastestRoute = FindPath(start, destination, RouteType.RoadPreferred);
  274. if (fastestRoute != null)
  275. {
  276. availableRoutes.Add(fastestRoute);
  277. if (showDebugLogs)
  278. {
  279. Debug.Log($"Fastest route: {fastestRoute.estimatedTime:F1}h time, {fastestRoute.path.Count} waypoints");
  280. }
  281. }
  282. // Shortest route (direct path, minimal waypoints)
  283. var shortestRoute = FindPath(start, destination, RouteType.Standard);
  284. if (shortestRoute != null)
  285. {
  286. bool similar = RoutesSimilar(fastestRoute, shortestRoute);
  287. if (showDebugLogs)
  288. {
  289. Debug.Log($"Shortest route: {shortestRoute.estimatedTime:F1}h time, {shortestRoute.path.Count} waypoints, similar to fastest: {similar}");
  290. }
  291. if (!similar)
  292. {
  293. availableRoutes.Add(shortestRoute);
  294. }
  295. }
  296. // Cheapest route (minimizes resource costs first, then travel time)
  297. var cheapestRoute = FindCheapestPath(start, destination);
  298. if (cheapestRoute != null)
  299. {
  300. bool similarToFastest = RoutesSimilar(fastestRoute, cheapestRoute);
  301. bool similarToShortest = RoutesSimilar(shortestRoute, cheapestRoute);
  302. if (showDebugLogs)
  303. {
  304. Debug.Log($"Cheapest route: {cheapestRoute.estimatedTime:F1}h time, {GetTotalResourceCost(cheapestRoute)} resource cost, {cheapestRoute.path.Count} waypoints");
  305. }
  306. if (!similarToFastest && !similarToShortest)
  307. {
  308. availableRoutes.Add(cheapestRoute);
  309. }
  310. }
  311. // If we ended up with only one route, try to generate alternatives with different parameters
  312. if (availableRoutes.Count == 1)
  313. {
  314. if (showDebugLogs)
  315. {
  316. Debug.Log("Only one unique route found, generating alternatives...");
  317. }
  318. // Try a more direct route that ignores some terrain penalties
  319. var directRoute = FindDirectPath(start, destination);
  320. if (directRoute != null && !RoutesSimilar(availableRoutes[0], directRoute))
  321. {
  322. availableRoutes.Add(directRoute);
  323. if (showDebugLogs)
  324. {
  325. Debug.Log($"Direct route added: {directRoute.estimatedTime:F1}h time");
  326. }
  327. }
  328. }
  329. if (showDebugLogs)
  330. {
  331. Debug.Log($"📊 Total unique routes calculated: {availableRoutes.Count}");
  332. }
  333. }
  334. /// <summary>
  335. /// Finds the best path between two points using A* pathfinding with different route preferences
  336. /// </summary>
  337. private TravelRoute FindPath(Vector2Int start, Vector2Int destination, RouteType routeType)
  338. {
  339. var mapData = mapMaker.GetMapData();
  340. Debug.Log($"🗺️ Finding {routeType} path from {start} to {destination}");
  341. var openSet = new List<PathNode>();
  342. var closedSet = new HashSet<Vector2Int>();
  343. var allNodes = new Dictionary<Vector2Int, PathNode>();
  344. var startNode = new PathNode
  345. {
  346. position = start,
  347. gCost = 0,
  348. hCost = CalculateHeuristic(start, destination, routeType),
  349. parent = null
  350. };
  351. openSet.Add(startNode);
  352. allNodes[start] = startNode;
  353. int iterations = 0;
  354. const int maxIterations = 10000; // Prevent infinite loops
  355. while (openSet.Count > 0 && iterations < maxIterations)
  356. {
  357. iterations++;
  358. // Find node with lowest fCost
  359. var currentNode = openSet.OrderBy(n => n.fCost).ThenBy(n => n.hCost).First();
  360. openSet.Remove(currentNode);
  361. closedSet.Add(currentNode.position);
  362. // Reached destination
  363. if (currentNode.position == destination)
  364. {
  365. var route = ReconstructRoute(currentNode, routeType);
  366. if (showDebugLogs)
  367. {
  368. Debug.Log($"{routeType} path found in {iterations} iterations, final cost: {currentNode.gCost:F1}");
  369. }
  370. // Analyze the route composition
  371. if (showDebugLogs)
  372. {
  373. AnalyzeRouteComposition(route);
  374. }
  375. return route;
  376. }
  377. // Check neighbors
  378. var neighbors = GetNeighbors(currentNode.position);
  379. foreach (var neighbor in neighbors)
  380. {
  381. if (closedSet.Contains(neighbor) || !mapData.IsValidPosition(neighbor.x, neighbor.y))
  382. continue;
  383. float movementCost = GetMovementCost(currentNode.position, neighbor, routeType);
  384. if (movementCost < 0) continue; // Impassable terrain
  385. float tentativeGCost = currentNode.gCost + movementCost;
  386. if (!allNodes.ContainsKey(neighbor))
  387. {
  388. allNodes[neighbor] = new PathNode
  389. {
  390. position = neighbor,
  391. gCost = float.MaxValue,
  392. hCost = CalculateHeuristic(neighbor, destination, routeType),
  393. parent = null
  394. };
  395. }
  396. var neighborNode = allNodes[neighbor];
  397. if (tentativeGCost < neighborNode.gCost)
  398. {
  399. neighborNode.parent = currentNode;
  400. neighborNode.gCost = tentativeGCost;
  401. if (!openSet.Contains(neighborNode))
  402. {
  403. openSet.Add(neighborNode);
  404. }
  405. }
  406. }
  407. }
  408. Debug.LogWarning($"❌ {routeType} path not found after {iterations} iterations (max: {maxIterations})");
  409. return null; // No path found
  410. }
  411. /// <summary>
  412. /// Gets movement cost for moving from one tile to another, considering route type preferences
  413. /// </summary>
  414. private float GetMovementCost(Vector2Int from, Vector2Int to, RouteType routeType)
  415. {
  416. var mapData = mapMaker.GetMapData();
  417. var toTile = mapData.GetTile(to.x, to.y);
  418. // Base movement cost by terrain
  419. float baseCost = GetTerrainMovementCost(toTile);
  420. // Feature modifications
  421. float featureCost = GetFeatureMovementCost(toTile, routeType);
  422. // Combine costs
  423. float totalCost = baseCost * featureCost;
  424. // Route type adjustments
  425. switch (routeType)
  426. {
  427. case RouteType.Standard:
  428. // Shortest route: minimize distance regardless of terrain difficulty
  429. // No terrain or road preferences - pure distance optimization
  430. break;
  431. case RouteType.RoadPreferred:
  432. // Fastest route: heavily favors roads and fast terrain, considers costly but fast options
  433. if (toTile.featureType == FeatureType.Road)
  434. {
  435. totalCost *= 0.3f; // Strong road preference (70% discount) - roads are much faster but realistic
  436. }
  437. else if (toTile.featureType == FeatureType.Tunnel)
  438. {
  439. // Tunnels are fast for fastest route (ignore resource cost for speed optimization)
  440. totalCost *= 0.4f; // Very fast travel through tunnels
  441. }
  442. else if (toTile.featureType == FeatureType.Ferry)
  443. {
  444. // Ferries are reasonably fast for fastest route
  445. totalCost *= 0.6f; // Fast water crossing
  446. }
  447. else
  448. {
  449. // Penalize non-road movement for road-preferred routes
  450. totalCost *= 1.5f; // 50% penalty for off-road movement
  451. }
  452. break;
  453. case RouteType.Special:
  454. // Cheapest route: avoid resource costs, but don't make travel time unrealistic
  455. // Moderate penalties for features that cost resources
  456. if (toTile.featureType == FeatureType.Ferry || toTile.featureType == FeatureType.Tunnel)
  457. totalCost *= 3.0f; // Significant penalty for resource-costing features (was 10x)
  458. // Moderate penalties for features that might trigger resource costs
  459. if (toTile.featureType == FeatureType.Bridge)
  460. totalCost *= 2.0f; // Penalty for bridges (potential tolls) (was 5x)
  461. // Light penalty for terrain that requires guides/special costs
  462. if (toTile.terrainType == TerrainType.Mountain)
  463. totalCost *= 2.0f; // Penalty for mountains (guide costs) (was 5x)
  464. // Still avoid slow terrain, but secondary to cost avoidance
  465. if (toTile.terrainType == TerrainType.River || toTile.terrainType == TerrainType.Lake)
  466. totalCost *= 1.5f; // Light penalty for water crossings (was 2x)
  467. // Prefer roads since they're free and fast
  468. if (toTile.featureType == FeatureType.Road)
  469. totalCost *= 0.5f; // Good road preference (50% discount)
  470. break;
  471. }
  472. return totalCost;
  473. }
  474. /// <summary>
  475. /// Gets base movement cost for different terrain types
  476. /// </summary>
  477. private float GetTerrainMovementCost(MapTile tile)
  478. {
  479. switch (tile.terrainType)
  480. {
  481. case TerrainType.Plains:
  482. return plainsMovementCost;
  483. case TerrainType.Forest:
  484. return forestMovementCost;
  485. case TerrainType.Mountain:
  486. return mountainMovementCost;
  487. case TerrainType.Ocean:
  488. return oceanCrossingCost; // Allow dangerous ocean crossing
  489. case TerrainType.Lake:
  490. return lakeCrossingCost; // Allow swimming across lakes
  491. case TerrainType.River:
  492. return riverCrossingCost; // Allow slow river crossing
  493. default:
  494. return plainsMovementCost;
  495. }
  496. }
  497. /// <summary>
  498. /// Gets movement cost modifications from features (roads, bridges, etc.)
  499. /// </summary>
  500. private float GetFeatureMovementCost(MapTile tile, RouteType routeType)
  501. {
  502. switch (tile.featureType)
  503. {
  504. case FeatureType.Road:
  505. return roadMovementCost; // Fast movement on roads
  506. case FeatureType.Bridge:
  507. return roadMovementCost; // Bridges are as fast as roads (crossing structures)
  508. case FeatureType.Town:
  509. case FeatureType.Village:
  510. return townMovementCost; // Very fast through settlements
  511. case FeatureType.Ferry:
  512. return ferryMovementCost; // Ferries are fast transportation (cost is in gold, not time)
  513. case FeatureType.Tunnel:
  514. return tunnelWithTorchCost; // Tunnels are fast with torch (cost is in torches, not time)
  515. default:
  516. return 1f; // No modification
  517. }
  518. }
  519. /// <summary>
  520. /// Calculates heuristic distance for A* pathfinding with route type considerations
  521. /// </summary>
  522. private float CalculateHeuristic(Vector2Int from, Vector2Int to, RouteType routeType = RouteType.Standard)
  523. {
  524. float baseHeuristic = Vector2Int.Distance(from, to) * plainsMovementCost;
  525. // For road-preferred routes, make the heuristic slightly more optimistic about finding roads
  526. if (routeType == RouteType.RoadPreferred)
  527. {
  528. // Assume we'll find roads for some of the journey
  529. baseHeuristic *= 0.8f; // Slightly more optimistic heuristic
  530. }
  531. return baseHeuristic;
  532. }
  533. /// <summary>
  534. /// Gets valid neighboring tiles for pathfinding
  535. /// </summary>
  536. private List<Vector2Int> GetNeighbors(Vector2Int position)
  537. {
  538. var neighbors = new List<Vector2Int>();
  539. // 8-directional movement
  540. for (int dx = -1; dx <= 1; dx++)
  541. {
  542. for (int dy = -1; dy <= 1; dy++)
  543. {
  544. if (dx == 0 && dy == 0) continue;
  545. Vector2Int neighbor = new Vector2Int(position.x + dx, position.y + dy);
  546. neighbors.Add(neighbor);
  547. }
  548. }
  549. return neighbors;
  550. }
  551. /// <summary>
  552. /// Reconstructs the final route from pathfinding results
  553. /// </summary>
  554. private TravelRoute ReconstructRoute(PathNode destinationNode, RouteType routeType)
  555. {
  556. var route = new TravelRoute
  557. {
  558. routeType = routeType,
  559. path = new List<Vector2Int>(),
  560. totalCost = destinationNode.gCost,
  561. totalDistance = 0f,
  562. estimatedTime = 0f,
  563. specialCosts = new Dictionary<string, int>()
  564. };
  565. var current = destinationNode;
  566. while (current != null)
  567. {
  568. route.path.Insert(0, current.position);
  569. current = current.parent;
  570. }
  571. // Calculate additional route information
  572. CalculateRouteDetails(route);
  573. return route;
  574. }
  575. /// <summary>
  576. /// Finds a more direct path that ignores some terrain penalties for route variety
  577. /// </summary>
  578. private TravelRoute FindDirectPath(Vector2Int start, Vector2Int destination)
  579. {
  580. var mapData = mapMaker.GetMapData();
  581. Debug.Log($"🎯 Finding direct path from {start} to {destination}");
  582. var openSet = new List<PathNode>();
  583. var closedSet = new HashSet<Vector2Int>();
  584. var allNodes = new Dictionary<Vector2Int, PathNode>();
  585. var startNode = new PathNode
  586. {
  587. position = start,
  588. gCost = 0,
  589. hCost = CalculateHeuristic(start, destination),
  590. parent = null
  591. };
  592. openSet.Add(startNode);
  593. allNodes[start] = startNode;
  594. int iterations = 0;
  595. const int maxIterations = 10000;
  596. while (openSet.Count > 0 && iterations < maxIterations)
  597. {
  598. iterations++;
  599. var currentNode = openSet.OrderBy(n => n.fCost).ThenBy(n => n.hCost).First();
  600. openSet.Remove(currentNode);
  601. closedSet.Add(currentNode.position);
  602. if (currentNode.position == destination)
  603. {
  604. var route = ReconstructRoute(currentNode, RouteType.Special);
  605. route.routeType = RouteType.Special; // Mark as direct route
  606. Debug.Log($"✅ Direct path found in {iterations} iterations, final cost: {currentNode.gCost:F1}");
  607. CalculateRouteDetails(route);
  608. return route;
  609. }
  610. var neighbors = GetNeighbors(currentNode.position);
  611. foreach (var neighbor in neighbors)
  612. {
  613. if (closedSet.Contains(neighbor) || !mapData.IsValidPosition(neighbor.x, neighbor.y))
  614. continue;
  615. // Use simplified movement cost for direct routes (less terrain penalty)
  616. float movementCost = GetDirectMovementCost(currentNode.position, neighbor);
  617. if (movementCost < 0) continue;
  618. float tentativeGCost = currentNode.gCost + movementCost;
  619. if (!allNodes.ContainsKey(neighbor))
  620. {
  621. allNodes[neighbor] = new PathNode
  622. {
  623. position = neighbor,
  624. gCost = float.MaxValue,
  625. hCost = CalculateHeuristic(neighbor, destination),
  626. parent = null
  627. };
  628. }
  629. var neighborNode = allNodes[neighbor];
  630. if (tentativeGCost < neighborNode.gCost)
  631. {
  632. neighborNode.parent = currentNode;
  633. neighborNode.gCost = tentativeGCost;
  634. if (!openSet.Contains(neighborNode))
  635. {
  636. openSet.Add(neighborNode);
  637. }
  638. }
  639. }
  640. }
  641. Debug.LogWarning($"❌ Direct path not found after {iterations} iterations");
  642. return null;
  643. }
  644. /// <summary>
  645. /// Simplified movement cost calculation for direct routes
  646. /// </summary>
  647. private float GetDirectMovementCost(Vector2Int from, Vector2Int to)
  648. {
  649. var mapData = mapMaker.GetMapData();
  650. var toTile = mapData.GetTile(to.x, to.y);
  651. // Simplified costs that emphasize directness over terrain optimization
  652. switch (toTile.terrainType)
  653. {
  654. case TerrainType.Plains:
  655. return 1.0f;
  656. case TerrainType.Forest:
  657. return 1.5f; // Less penalty than normal
  658. case TerrainType.Mountain:
  659. return 2.0f; // Much less penalty for directness
  660. case TerrainType.River:
  661. return 1.8f;
  662. case TerrainType.Lake:
  663. return 2.0f;
  664. case TerrainType.Ocean:
  665. return 4.0f; // Still expensive but possible
  666. default:
  667. return 1.0f;
  668. }
  669. }
  670. /// <summary>
  671. /// Finds the cheapest path that minimizes resource costs (gold) first, then travel time
  672. /// Uses the Special route type but should calculate actual travel time correctly
  673. /// </summary>
  674. private TravelRoute FindCheapestPath(Vector2Int start, Vector2Int destination)
  675. {
  676. // Use the standard A* pathfinding with Special route type (which avoids costly features)
  677. return FindPath(start, destination, RouteType.Special);
  678. }
  679. /// <summary>
  680. /// Gets the resource cost for entering a specific tile
  681. /// </summary>
  682. private float GetResourceCostForTile(Vector2Int position)
  683. {
  684. var mapData = mapMaker.GetMapData();
  685. var tile = mapData.GetTile(position.x, position.y);
  686. switch (tile.featureType)
  687. {
  688. case FeatureType.Ferry:
  689. return ferryBaseCost; // Direct gold cost
  690. case FeatureType.Tunnel:
  691. return tunnelTorchCost; // Torch cost (treated as gold equivalent)
  692. case FeatureType.Bridge:
  693. return 2.0f; // Potential bridge toll (small cost to prefer other routes)
  694. default:
  695. break;
  696. }
  697. // Terrain-based resource costs (for guide fees, etc.)
  698. switch (tile.terrainType)
  699. {
  700. case TerrainType.Mountain:
  701. return 3.0f; // Potential guide cost for dangerous areas
  702. default:
  703. return 0.0f; // No resource cost
  704. }
  705. }
  706. /// <summary>
  707. /// Calculates detailed information about the route (distance, time, special costs)
  708. /// </summary>
  709. private void CalculateRouteDetails(TravelRoute route)
  710. {
  711. var mapData = mapMaker.GetMapData();
  712. route.totalDistance = route.path.Count;
  713. // Calculate actual travel time based on real movement costs, not pathfinding costs
  714. float actualTravelTime = 0f;
  715. for (int i = 0; i < route.path.Count; i++)
  716. {
  717. var position = route.path[i];
  718. var tile = mapData.GetTile(position.x, position.y);
  719. // Use actual movement speeds for time calculation
  720. float tileTime = GetActualMovementTime(tile);
  721. actualTravelTime += tileTime;
  722. }
  723. route.estimatedTime = actualTravelTime;
  724. // Check for special costs
  725. int bridgeCount = 0;
  726. int mountainTileCount = 0;
  727. foreach (var position in route.path)
  728. {
  729. var tile = mapData.GetTile(position.x, position.y);
  730. // Ferry costs
  731. if (tile.featureType == FeatureType.Ferry)
  732. {
  733. if (!route.specialCosts.ContainsKey("Ferry"))
  734. route.specialCosts["Ferry"] = ferryBaseCost;
  735. }
  736. // Tunnel costs
  737. else if (tile.featureType == FeatureType.Tunnel)
  738. {
  739. if (!route.specialCosts.ContainsKey("Tunnel Torch"))
  740. route.specialCosts["Tunnel Torch"] = tunnelTorchCost;
  741. }
  742. // Bridge tolls (for major bridges)
  743. else if (tile.featureType == FeatureType.Bridge)
  744. {
  745. bridgeCount++;
  746. }
  747. // Mountain passes (dangerous mountain areas need guides)
  748. else if (tile.terrainType == TerrainType.Mountain)
  749. {
  750. mountainTileCount++;
  751. }
  752. }
  753. // Add bridge toll costs (only for longer bridge crossings)
  754. if (bridgeCount >= 3) // Only charge for significant bridge crossings
  755. {
  756. route.specialCosts["Bridge Toll"] = bridgeTollCost;
  757. }
  758. // Add mountain guide costs (for extensive mountain travel)
  759. if (mountainTileCount >= 5) // Only charge for dangerous mountain passes
  760. {
  761. route.specialCosts["Mountain Guide"] = mountainPassCost;
  762. }
  763. }
  764. /// <summary>
  765. /// Calculates the total resource cost (gold equivalent) for a route
  766. /// </summary>
  767. private int GetTotalResourceCost(TravelRoute route)
  768. {
  769. if (route?.specialCosts == null) return 0;
  770. int totalCost = 0;
  771. foreach (var cost in route.specialCosts)
  772. {
  773. totalCost += cost.Value; // All costs are in gold or gold-equivalent
  774. }
  775. return totalCost;
  776. }
  777. /// <summary>
  778. /// Gets the actual movement time for a tile (used for time calculations, not pathfinding)
  779. /// </summary>
  780. private float GetActualMovementTime(MapTile tile)
  781. {
  782. // Special features that completely replace terrain costs
  783. switch (tile.featureType)
  784. {
  785. case FeatureType.Ferry:
  786. // Ferry completely replaces terrain cost - you're not swimming, you're riding!
  787. return ferryMovementCost; // 0.7f - direct ferry speed
  788. case FeatureType.Tunnel:
  789. // Tunnel completely replaces terrain cost
  790. return tunnelWithTorchCost; // 0.8f - tunnel with torch speed
  791. case FeatureType.Road:
  792. // Roads significantly speed up movement but don't completely replace terrain
  793. return GetTerrainMovementCost(tile) * roadMovementCost; // e.g., 1.0f * 0.5f = 0.5f
  794. case FeatureType.Bridge:
  795. // Bridges provide moderate speedup over terrain
  796. return GetTerrainMovementCost(tile) * bridgeMovementCost; // e.g., 1.0f * 0.6f = 0.6f
  797. case FeatureType.Town:
  798. case FeatureType.Village:
  799. // Towns are very fast to move through
  800. return townMovementCost; // 0.1f - direct town speed
  801. default:
  802. // No special feature, use base terrain cost
  803. return GetTerrainMovementCost(tile);
  804. }
  805. }
  806. /// <summary>
  807. /// Checks if two routes are essentially the same
  808. /// </summary>
  809. private bool RoutesSimilar(TravelRoute route1, TravelRoute route2)
  810. {
  811. if (route1 == null || route2 == null) return false;
  812. // Consider routes similar if they have >90% path overlap (more strict to allow more variations)
  813. var overlap = route1.path.Intersect(route2.path).Count();
  814. var maxLength = Mathf.Max(route1.path.Count, route2.path.Count);
  815. bool similar = (float)overlap / maxLength > 0.9f;
  816. Debug.Log($"🔍 Route similarity check: {overlap}/{maxLength} = {((float)overlap / maxLength):F2} > 0.9? {similar}");
  817. return similar;
  818. }
  819. /// <summary>
  820. /// Analyzes and logs the composition of a route for debugging
  821. /// </summary>
  822. private void AnalyzeRouteComposition(TravelRoute route)
  823. {
  824. if (route == null || route.path == null) return;
  825. var mapData = mapMaker.GetMapData();
  826. int plainsCount = 0, forestCount = 0, mountainCount = 0, roadCount = 0, riverCount = 0, lakeCount = 0;
  827. float totalMovementCost = 0;
  828. float roadBonusTotal = 0;
  829. foreach (var pos in route.path)
  830. {
  831. var tile = mapData.GetTile(pos.x, pos.y);
  832. float tileCost = GetMovementCost(pos, pos, route.routeType);
  833. totalMovementCost += tileCost;
  834. switch (tile.terrainType)
  835. {
  836. case TerrainType.Plains: plainsCount++; break;
  837. case TerrainType.Forest: forestCount++; break;
  838. case TerrainType.Mountain: mountainCount++; break;
  839. case TerrainType.River: riverCount++; break;
  840. case TerrainType.Lake: lakeCount++; break;
  841. }
  842. if (tile.featureType == FeatureType.Road)
  843. {
  844. roadCount++;
  845. // Calculate road bonus for this route type
  846. switch (route.routeType)
  847. {
  848. case RouteType.Standard:
  849. roadBonusTotal += GetTerrainMovementCost(tile) * (1f - 0.6f); // 40% discount
  850. break;
  851. case RouteType.RoadPreferred:
  852. roadBonusTotal += GetTerrainMovementCost(tile) * (1f - 0.2f); // 80% discount
  853. break;
  854. }
  855. }
  856. }
  857. float avgCostPerTile = totalMovementCost / route.path.Count;
  858. float roadPercentage = (float)roadCount / route.path.Count * 100f;
  859. Debug.Log($"📊 Route Analysis ({route.routeType}): {route.path.Count} tiles");
  860. Debug.Log($" Terrain: Plains:{plainsCount}, Forest:{forestCount}, Mountain:{mountainCount}, Rivers:{riverCount}, Lakes:{lakeCount}");
  861. Debug.Log($" Roads: {roadCount} tiles ({roadPercentage:F1}%), saved {roadBonusTotal:F1} cost");
  862. Debug.Log($" Average cost per tile: {avgCostPerTile:F2} (Total: {totalMovementCost:F1})");
  863. Debug.Log($" Time calculation: {route.estimatedTime:F1}h = calculated from actual tile movement speeds");
  864. // Explain why this route type might be different
  865. string explanation = "";
  866. switch (route.routeType)
  867. {
  868. case RouteType.Standard:
  869. explanation = "Shortest route minimizes distance and waypoints";
  870. break;
  871. case RouteType.RoadPreferred:
  872. explanation = $"Fastest route heavily favors roads (90% discount) - {roadPercentage:F1}% on roads";
  873. break;
  874. case RouteType.Special:
  875. explanation = "Cheapest/Direct route avoids special costs and takes direct paths";
  876. break;
  877. }
  878. Debug.Log($" Strategy: {explanation}");
  879. }
  880. /// <summary>
  881. /// Visualizes the selected route on the map
  882. /// </summary>
  883. private void VisualizeRoute(TravelRoute route)
  884. {
  885. if (showDebugLogs)
  886. {
  887. Debug.Log($"🎨 VisualizeRoute called with route: {route?.routeType} ({route?.path?.Count ?? 0} points)");
  888. }
  889. ClearPathVisualization();
  890. if (route == null || route.path.Count == 0)
  891. {
  892. if (showDebugLogs)
  893. {
  894. Debug.LogWarning("❌ Cannot visualize route: route is null or has no path points");
  895. }
  896. return;
  897. }
  898. // Create path visualization
  899. var pathObject = new GameObject("TravelPath");
  900. var lineRenderer = pathObject.AddComponent<LineRenderer>();
  901. // Configure line renderer with simple, reliable settings
  902. lineRenderer.material = pathLineMaterial ?? CreateDefaultPathMaterial();
  903. lineRenderer.material.color = GetRouteColor(route);
  904. // Force line width to 1.0 for consistent visibility
  905. lineRenderer.startWidth = 1.0f;
  906. lineRenderer.endWidth = 1.0f;
  907. lineRenderer.positionCount = route.path.Count;
  908. lineRenderer.useWorldSpace = true;
  909. // Simple LineRenderer settings for reliable visibility
  910. lineRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
  911. lineRenderer.receiveShadows = false;
  912. lineRenderer.alignment = LineAlignment.View; // Face camera
  913. lineRenderer.textureMode = LineTextureMode.Tile;
  914. lineRenderer.sortingLayerName = "Default";
  915. lineRenderer.sortingOrder = 50; // High sorting order but not excessive
  916. // Debug the width setting
  917. if (showDebugLogs)
  918. {
  919. Debug.Log($"🔧 Setting LineRenderer width to: {pathLineWidth} (startWidth: {lineRenderer.startWidth}, endWidth: {lineRenderer.endWidth})");
  920. }
  921. // Set path points
  922. for (int i = 0; i < route.path.Count; i++)
  923. {
  924. Vector3 worldPos = TileToWorldPosition(route.path[i]);
  925. worldPos.y = pathLineHeight; // Place above map tiles
  926. lineRenderer.SetPosition(i, worldPos);
  927. }
  928. currentPathVisualizations.Add(pathObject);
  929. if (showDebugLogs)
  930. {
  931. Debug.Log($"📍 Visualized {route.routeType} route with {route.path.Count} waypoints");
  932. Debug.Log($"🔧 LineRenderer config: startWidth={lineRenderer.startWidth}, endWidth={lineRenderer.endWidth}, material={lineRenderer.material?.name}, color={lineRenderer.material?.color}");
  933. Debug.Log($"🎯 GameObject created: {pathObject.name} at position {pathObject.transform.position}");
  934. }
  935. }
  936. /// <summary>
  937. /// Visualizes all available routes with different colors and proper layering
  938. /// </summary>
  939. private void VisualizeAllRoutes(List<TravelRoute> routes)
  940. {
  941. ClearPathVisualization();
  942. // Render all routes with the first one (fastest by time) selected by default
  943. var routesByTime = routes.OrderBy(r => r.estimatedTime).ToList();
  944. var defaultSelected = routesByTime.FirstOrDefault();
  945. for (int i = 0; i < routes.Count; i++)
  946. {
  947. var route = routes[i];
  948. if (route == null || route.path.Count == 0) continue;
  949. bool isSelected = (route == defaultSelected);
  950. RenderSingleRoute(route, i, isSelected);
  951. }
  952. if (showDebugLogs)
  953. {
  954. string selectedName = defaultSelected?.routeType == RouteType.RoadPreferred ? "Fastest" :
  955. defaultSelected?.routeType == RouteType.Standard ? "Shortest" : "Cheapest";
  956. Debug.Log($"🎨 Visualized {routes.Count} routes with {selectedName} as default selected (on top)");
  957. }
  958. } /// <summary>
  959. /// Gets the appropriate color for a route based on its type (GPS-style)
  960. /// </summary>
  961. private Color GetRouteColor(TravelRoute route)
  962. {
  963. // GPS-style color coding
  964. switch (route.routeType)
  965. {
  966. case RouteType.RoadPreferred: // Fastest route
  967. if (showDebugLogs) Debug.Log($"🎨 Route color: GREEN (fastest route)");
  968. return Color.green; // Green for fastest (like GPS)
  969. case RouteType.Standard: // Shortest route
  970. if (showDebugLogs) Debug.Log($"🎨 Route color: BLUE (shortest route)");
  971. return standardPathColor; // Blue for shortest
  972. case RouteType.Special: // Cheapest/Direct route
  973. if (showDebugLogs) Debug.Log($"🎨 Route color: YELLOW (cheapest/direct route)");
  974. return alternativePathColor; // Yellow for cheapest/alternative
  975. default:
  976. if (showDebugLogs) Debug.Log($"🎨 Route color: BLUE (default)");
  977. return standardPathColor;
  978. }
  979. }
  980. /// <summary>
  981. /// Creates a default material for path visualization
  982. /// </summary>
  983. private Material CreateDefaultPathMaterial()
  984. {
  985. // Use simple Sprites/Default shader for reliable rendering
  986. Shader shader = Shader.Find("Sprites/Default");
  987. if (shader == null)
  988. shader = Shader.Find("Legacy Shaders/Particles/Alpha Blended Premultiply");
  989. if (shader == null)
  990. shader = Shader.Find("Unlit/Color");
  991. if (shader == null)
  992. shader = Shader.Find("Standard");
  993. var material = new Material(shader ?? Shader.Find("Diffuse"));
  994. material.color = standardPathColor;
  995. if (showDebugLogs)
  996. {
  997. Debug.Log($"🎨 Created path material with shader: {shader?.name ?? "Unknown"}");
  998. }
  999. return material;
  1000. }
  1001. /// <summary>
  1002. /// Clears any existing path visualization
  1003. /// </summary>
  1004. private void ClearPathVisualization()
  1005. {
  1006. foreach (var pathViz in currentPathVisualizations)
  1007. {
  1008. if (pathViz != null)
  1009. {
  1010. DestroyImmediate(pathViz);
  1011. }
  1012. }
  1013. currentPathVisualizations.Clear();
  1014. }
  1015. /// <summary>
  1016. /// Shows only the selected route with transparency for better marker visibility during travel
  1017. /// </summary>
  1018. private void ShowTravelRouteOnly(TravelRoute route)
  1019. {
  1020. if (route == null || route.path.Count == 0)
  1021. {
  1022. if (showDebugLogs)
  1023. {
  1024. Debug.LogWarning("❌ Cannot show travel route: route is null or has no path");
  1025. }
  1026. return;
  1027. }
  1028. // Clear all existing route visualizations
  1029. ClearPathVisualization();
  1030. if (showDebugLogs)
  1031. {
  1032. Debug.Log($"🎯 Showing only {route.routeType} route with transparency for travel");
  1033. }
  1034. // Create a semi-transparent visualization of the selected route
  1035. var pathObject = new GameObject($"TravelPath_{route.routeType}_Traveling");
  1036. var lineRenderer = pathObject.AddComponent<LineRenderer>();
  1037. // Configure line renderer with semi-transparent material
  1038. var baseMaterial = pathLineMaterial ?? CreateDefaultPathMaterial();
  1039. var routeMaterial = new Material(baseMaterial);
  1040. var routeColor = GetRouteColor(route);
  1041. // Set to 50% transparency for better marker visibility
  1042. routeColor.a = 0.5f;
  1043. routeMaterial.color = routeColor;
  1044. lineRenderer.material = routeMaterial;
  1045. // Use a thinner line during travel to be less obtrusive
  1046. lineRenderer.startWidth = 1.2f;
  1047. lineRenderer.endWidth = 1.2f;
  1048. lineRenderer.positionCount = route.path.Count;
  1049. // Set sorting order to be below the team marker
  1050. lineRenderer.sortingOrder = 45; // Lower than team marker
  1051. // Set path points
  1052. for (int i = 0; i < route.path.Count; i++)
  1053. {
  1054. Vector3 worldPos = TileToWorldPosition(route.path[i]);
  1055. worldPos.y = pathLineHeight; // Keep consistent height
  1056. lineRenderer.SetPosition(i, worldPos);
  1057. }
  1058. currentPathVisualizations.Add(pathObject);
  1059. if (showDebugLogs)
  1060. {
  1061. Debug.Log($"✅ Travel route visualization created: {route.routeType} with 50% transparency");
  1062. }
  1063. }
  1064. /// <summary>
  1065. /// Cancels current travel planning
  1066. /// </summary>
  1067. public void CancelTravelPlanning()
  1068. {
  1069. plannedDestination = null;
  1070. availableRoutes.Clear();
  1071. ClearPathVisualization();
  1072. HideTravelUI();
  1073. if (showDebugLogs)
  1074. {
  1075. Debug.Log("❌ Travel planning cancelled");
  1076. }
  1077. }
  1078. /// <summary>
  1079. /// Starts travel using the specified route
  1080. /// </summary>
  1081. public void StartTravel(TravelRoute route)
  1082. {
  1083. if (route == null || isTraveling)
  1084. {
  1085. Debug.LogWarning("Cannot start travel: invalid route or already traveling");
  1086. return;
  1087. }
  1088. // Synchronize coordinates before starting travel
  1089. SynchronizeWithExplorationSystem();
  1090. // Reset cancellation flag and store current route
  1091. shouldCancelJourney = false;
  1092. currentRoute = route;
  1093. // Show only the selected route with transparency for better marker visibility
  1094. ShowTravelRouteOnly(route);
  1095. // Start the travel coroutine and store reference
  1096. currentTravelCoroutine = StartCoroutine(ExecuteTravel(route));
  1097. }
  1098. /// <summary>
  1099. /// Cancels the current journey if one is in progress
  1100. /// </summary>
  1101. public void CancelJourney()
  1102. {
  1103. if (!isTraveling)
  1104. {
  1105. if (showDebugLogs) Debug.Log("No journey to cancel");
  1106. return;
  1107. }
  1108. if (showDebugLogs)
  1109. {
  1110. Debug.Log("Journey cancellation requested by player");
  1111. }
  1112. // Set the cancellation flag
  1113. shouldCancelJourney = true;
  1114. // The actual cleanup will happen in the ExecuteTravel coroutine
  1115. }
  1116. /// <summary>
  1117. /// Executes the actual travel along the route
  1118. /// </summary>
  1119. private IEnumerator ExecuteTravel(TravelRoute route)
  1120. {
  1121. isTraveling = true;
  1122. if (showDebugLogs)
  1123. {
  1124. Debug.Log($"🚶 Starting travel along {route.routeType} route");
  1125. Debug.Log($"📍 Route has {route.path.Count} waypoints, estimated time: {route.estimatedTime:F1} hours");
  1126. }
  1127. // Ensure team marker is visible during travel (stop blinking)
  1128. EnsureTeamMarkerVisible(true);
  1129. // Debug: Check initial team marker state
  1130. var initialMarker = teamPlacement.GetTeamMarker();
  1131. if (initialMarker != null)
  1132. {
  1133. Debug.Log($"🔍 Initial team marker: Position={initialMarker.transform.position}, Active={initialMarker.activeInHierarchy}, WorldPos={initialMarker.transform.position}");
  1134. var initialRenderer = initialMarker.GetComponent<Renderer>();
  1135. if (initialRenderer != null)
  1136. {
  1137. Debug.Log($"🔍 Initial renderer: Enabled={initialRenderer.enabled}, Material={initialRenderer.material?.name}");
  1138. }
  1139. }
  1140. else
  1141. {
  1142. Debug.LogError("❌ No team marker found at start of travel!");
  1143. }
  1144. // Deduct special costs before travel
  1145. DeductTravelCosts(route);
  1146. // Move team marker along the path with smooth animation
  1147. for (int i = 1; i < route.path.Count; i++)
  1148. {
  1149. // Check for cancellation at the start of each step
  1150. if (shouldCancelJourney)
  1151. {
  1152. if (showDebugLogs)
  1153. {
  1154. Debug.Log($"🛑 Journey cancelled at step {i}/{route.path.Count - 1}. Stopping at {route.path[i - 1]}");
  1155. }
  1156. break; // Exit the movement loop
  1157. }
  1158. Vector2Int nextPosition = route.path[i];
  1159. Vector2Int currentPos = route.path[i - 1];
  1160. if (showDebugLogs && i % 3 == 0) // Log every 3rd step
  1161. {
  1162. Debug.Log($"🚶 Traveling step {i}/{route.path.Count - 1} to {nextPosition} ({((float)i / (route.path.Count - 1) * 100):F0}% complete)");
  1163. }
  1164. // Get current and target world positions for smooth movement
  1165. Vector3 currentWorldPos = TileToWorldPosition(currentPos);
  1166. Vector3 targetWorldPos = TileToWorldPosition(nextPosition);
  1167. // Animate the team marker moving smoothly between positions
  1168. float moveTime = 0.8f; // Base time per step (slower movement)
  1169. float stepCost = GetMovementCost(currentPos, nextPosition, route.routeType);
  1170. // Adjust move time based on terrain difficulty
  1171. moveTime *= Mathf.Clamp(stepCost * 0.3f, 0.5f, 3f); // Slower for difficult terrain
  1172. // Smooth movement animation
  1173. float elapsed = 0f;
  1174. while (elapsed < moveTime)
  1175. {
  1176. // Check for cancellation during movement animation
  1177. if (shouldCancelJourney)
  1178. {
  1179. if (showDebugLogs)
  1180. {
  1181. Debug.Log($"🛑 Journey cancelled during movement animation. Stopping at current position.");
  1182. }
  1183. break; // Exit the animation loop
  1184. }
  1185. elapsed += Time.deltaTime;
  1186. float t = Mathf.SmoothStep(0f, 1f, elapsed / moveTime);
  1187. // Interpolate between current and target position
  1188. Vector3 lerpedWorldPos = Vector3.Lerp(currentWorldPos, targetWorldPos, t);
  1189. Vector2Int lerpedTilePos = WorldToTilePosition(lerpedWorldPos);
  1190. // Update team position smoothly (but only if it's a valid position)
  1191. if (mapMaker.GetMapData().IsValidPosition(lerpedTilePos.x, lerpedTilePos.y))
  1192. {
  1193. teamPlacement.PlaceTeamAt(lerpedTilePos);
  1194. // Ensure marker stays visible during travel
  1195. var teamMarker = teamPlacement.GetTeamMarker();
  1196. if (teamMarker != null)
  1197. {
  1198. teamMarker.SetActive(true);
  1199. var renderer = teamMarker.GetComponent<Renderer>();
  1200. if (renderer != null && !renderer.enabled)
  1201. {
  1202. renderer.enabled = true;
  1203. if (showDebugLogs)
  1204. {
  1205. Debug.Log($"🔧 Re-enabled team marker renderer at {teamMarker.transform.position}");
  1206. }
  1207. }
  1208. }
  1209. }
  1210. yield return null; // Wait one frame
  1211. }
  1212. // Break out of the inner animation loop if cancelled
  1213. if (shouldCancelJourney)
  1214. {
  1215. break; // Exit the for loop as well
  1216. }
  1217. // Ensure we end up exactly at the target position (if not cancelled)
  1218. if (!shouldCancelJourney)
  1219. {
  1220. teamPlacement.PlaceTeamAt(nextPosition);
  1221. }
  1222. }
  1223. // Journey cleanup - whether completed or cancelled
  1224. isTraveling = false;
  1225. ResetTimeSpeed(); // Reset time scale to normal when travel ends
  1226. EnsureTeamMarkerVisible(false); // Restore blinking
  1227. ClearPathVisualization();
  1228. HideTravelUI();
  1229. // Synchronize coordinate systems after travel
  1230. SynchronizeWithExplorationSystem();
  1231. // Clear travel state
  1232. currentTravelCoroutine = null;
  1233. currentRoute = null;
  1234. if (shouldCancelJourney)
  1235. {
  1236. if (showDebugLogs)
  1237. {
  1238. Debug.Log($"🛑 Journey cancelled! Team stopped at {teamPlacement.GetTeamPosition()}");
  1239. }
  1240. shouldCancelJourney = false; // Reset the flag
  1241. }
  1242. else
  1243. {
  1244. if (showDebugLogs)
  1245. {
  1246. Debug.Log($"✅ Travel completed! Arrived at {route.path.Last()}");
  1247. }
  1248. }
  1249. }
  1250. /// <summary>
  1251. /// Synchronize travel system coordinates with exploration system
  1252. /// </summary>
  1253. private void SynchronizeWithExplorationSystem()
  1254. {
  1255. if (mapMaker != null && mapMaker.useExplorationSystem)
  1256. {
  1257. mapMaker.SyncTeamPosition();
  1258. if (showDebugLogs)
  1259. {
  1260. Debug.Log("🔄 Synchronized team position with exploration system");
  1261. }
  1262. }
  1263. }
  1264. /// <summary>
  1265. /// Deducts costs for travel (gold, items, etc.)
  1266. /// </summary>
  1267. private void DeductTravelCosts(TravelRoute route)
  1268. {
  1269. if (route.specialCosts.Count == 0) return;
  1270. if (showDebugLogs)
  1271. {
  1272. Debug.Log($"💰 Deducting travel costs: {string.Join(", ", route.specialCosts.Select(kvp => $"{kvp.Key}: {kvp.Value}"))}");
  1273. }
  1274. // TODO: Implement actual resource deduction
  1275. // This would integrate with your inventory/currency system
  1276. foreach (var cost in route.specialCosts)
  1277. {
  1278. switch (cost.Key)
  1279. {
  1280. case "Ferry":
  1281. // DeductGold(cost.Value);
  1282. Debug.Log($"💰 Would deduct {cost.Value} gold for ferry");
  1283. break;
  1284. case "Tunnel Torch":
  1285. // DeductTorches(cost.Value);
  1286. Debug.Log($"🔥 Would deduct {cost.Value} torches for tunnel");
  1287. break;
  1288. }
  1289. }
  1290. }
  1291. /// <summary>
  1292. /// Ensures the team marker is visible during travel by controlling its blinking behavior
  1293. /// </summary>
  1294. private void EnsureTeamMarkerVisible(bool forceVisible)
  1295. {
  1296. if (teamPlacement == null)
  1297. {
  1298. Debug.LogWarning("❌ TeamPlacement is null in EnsureTeamMarkerVisible");
  1299. return;
  1300. }
  1301. var teamMarker = teamPlacement.GetTeamMarker();
  1302. if (teamMarker == null)
  1303. {
  1304. Debug.LogWarning("❌ Team marker is null - creating new marker");
  1305. // Force creation of a new marker if it doesn't exist
  1306. teamPlacement.PlaceTeamAt(teamPlacement.GetTeamPosition());
  1307. teamMarker = teamPlacement.GetTeamMarker();
  1308. if (teamMarker == null)
  1309. {
  1310. Debug.LogError("❌ Failed to create team marker!");
  1311. return;
  1312. }
  1313. }
  1314. var renderer = teamMarker.GetComponent<Renderer>();
  1315. if (renderer == null)
  1316. {
  1317. Debug.LogWarning("❌ Team marker has no Renderer component");
  1318. return;
  1319. }
  1320. Debug.Log($"🔧 Team marker status: Position={teamMarker.transform.position}, Active={teamMarker.activeInHierarchy}, RendererEnabled={renderer.enabled}");
  1321. if (forceVisible)
  1322. {
  1323. // Force marker to be visible and stop blinking during travel
  1324. teamMarker.SetActive(true);
  1325. renderer.enabled = true;
  1326. // Make the marker more visible by adjusting its properties
  1327. teamMarker.transform.localScale = Vector3.one * 2f; // Make it bigger
  1328. // Try to change material color to bright red for visibility
  1329. var material = renderer.material;
  1330. if (material != null)
  1331. {
  1332. material.color = Color.red;
  1333. if (material.HasProperty("_BaseColor"))
  1334. material.SetColor("_BaseColor", Color.red);
  1335. if (material.HasProperty("_Color"))
  1336. material.SetColor("_Color", Color.red);
  1337. Debug.Log($"🔴 Set team marker to bright red for visibility");
  1338. }
  1339. // Force position to be visible above map (adjust Y for top-down view)
  1340. var pos = teamMarker.transform.position;
  1341. teamMarker.transform.position = new Vector3(pos.x, 2f, pos.z); // Raise above map for visibility
  1342. // Stop any blinking coroutines in SimpleTeamPlacement using reflection
  1343. try
  1344. {
  1345. var placementType = teamPlacement.GetType();
  1346. var stopCooroutinesMethod = placementType.GetMethod("StopAllCoroutines");
  1347. if (stopCooroutinesMethod != null)
  1348. {
  1349. stopCooroutinesMethod.Invoke(teamPlacement, null);
  1350. Debug.Log("✅ Stopped blinking coroutines");
  1351. }
  1352. // Set isBlinking to false using reflection
  1353. var isBlinkingField = placementType.GetField("isBlinking", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
  1354. if (isBlinkingField != null)
  1355. {
  1356. isBlinkingField.SetValue(teamPlacement, false);
  1357. Debug.Log("✅ Set isBlinking to false");
  1358. }
  1359. }
  1360. catch (System.Exception e)
  1361. {
  1362. Debug.LogWarning($"⚠️ Reflection failed: {e.Message}");
  1363. }
  1364. if (showDebugLogs)
  1365. {
  1366. Debug.Log($"👁️ Team marker forced visible for travel at {teamMarker.transform.position}");
  1367. }
  1368. }
  1369. else
  1370. {
  1371. // Restore normal blinking behavior after travel
  1372. try
  1373. {
  1374. var placementType = teamPlacement.GetType();
  1375. var enableBlinkingField = placementType.GetField("enableBlinking", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
  1376. if (enableBlinkingField != null && (bool)enableBlinkingField.GetValue(teamPlacement))
  1377. {
  1378. // Restart blinking using reflection
  1379. var startBlinkingMethod = placementType.GetMethod("StartBlinking", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
  1380. if (startBlinkingMethod != null)
  1381. {
  1382. startBlinkingMethod.Invoke(teamPlacement, null);
  1383. if (showDebugLogs)
  1384. {
  1385. Debug.Log("✨ Team marker blinking restored after travel");
  1386. }
  1387. }
  1388. }
  1389. }
  1390. catch (System.Exception e)
  1391. {
  1392. Debug.LogWarning($"⚠️ Reflection failed during restore: {e.Message}");
  1393. }
  1394. }
  1395. }
  1396. /// <summary>
  1397. /// Utility methods for coordinate conversion
  1398. /// </summary>
  1399. private Vector3 GetMouseWorldPosition()
  1400. {
  1401. if (mapCamera == null)
  1402. {
  1403. if (showDebugLogs) Debug.LogWarning("❌ Map camera is null, trying to find Camera.main");
  1404. mapCamera = Camera.main;
  1405. if (mapCamera == null) return Vector3.zero;
  1406. }
  1407. Ray ray = mapCamera.ScreenPointToRay(Input.mousePosition);
  1408. if (showDebugLogs) Debug.Log($"📐 Raycast from camera: {ray.origin} direction: {ray.direction}");
  1409. // Try raycast first (if there are colliders)
  1410. if (Physics.Raycast(ray, out RaycastHit hit, 1000f))
  1411. {
  1412. if (showDebugLogs) Debug.Log($"🎯 Raycast hit: {hit.point} on {hit.collider.name}");
  1413. return hit.point;
  1414. }
  1415. // Fallback: use camera plane projection at Y=0 (map level) for top-down view
  1416. Plane mapPlane = new Plane(Vector3.up, Vector3.zero); // Y=0 plane facing up for top-down camera
  1417. if (mapPlane.Raycast(ray, out float distance))
  1418. {
  1419. Vector3 point = ray.GetPoint(distance);
  1420. if (showDebugLogs) Debug.Log($"📍 Plane intersection: {point}");
  1421. return point;
  1422. }
  1423. if (showDebugLogs) Debug.LogWarning("❌ No raycast hit and no plane intersection");
  1424. return Vector3.zero;
  1425. }
  1426. private Vector2Int WorldToTilePosition(Vector3 worldPos)
  1427. {
  1428. float tileSize = mapMaker?.mapVisualizer?.tileSize ?? 1f;
  1429. return new Vector2Int(
  1430. Mathf.RoundToInt(worldPos.x / tileSize),
  1431. Mathf.RoundToInt(worldPos.z / tileSize) // Use Z coordinate for map Y in top-down view
  1432. );
  1433. }
  1434. private Vector3 TileToWorldPosition(Vector2Int tilePos)
  1435. {
  1436. float tileSize = mapMaker?.mapVisualizer?.tileSize ?? 1f;
  1437. return new Vector3(
  1438. tilePos.x * tileSize,
  1439. 0f, // Y=0 for tiles at ground level
  1440. tilePos.y * tileSize // Map Y coordinate becomes world Z coordinate for top-down view
  1441. );
  1442. }
  1443. /// <summary>
  1444. /// Debug method to show movement cost information
  1445. /// </summary>
  1446. private void ShowMovementCostInfo()
  1447. {
  1448. Debug.Log("=== MOVEMENT COST INFO ===");
  1449. Debug.Log("TERRAIN COSTS:");
  1450. Debug.Log($" Plains: {plainsMovementCost}x");
  1451. Debug.Log($" Forest: {forestMovementCost}x");
  1452. Debug.Log($" Mountain: {mountainMovementCost}x");
  1453. Debug.Log($" River Crossing: {riverCrossingCost}x (slow crossing)");
  1454. Debug.Log($" Lake Swimming: {lakeCrossingCost}x (swimming)");
  1455. Debug.Log($" Ocean Crossing: {oceanCrossingCost}x (very dangerous)");
  1456. Debug.Log("FEATURE COSTS:");
  1457. Debug.Log($" Road: {roadMovementCost}x");
  1458. Debug.Log($" Town/Village: {townMovementCost}x");
  1459. Debug.Log($" Bridge: {bridgeMovementCost}x");
  1460. Debug.Log($" Ferry: {ferryMovementCost}x (+ {ferryBaseCost} gold)");
  1461. Debug.Log($" Tunnel (with torch): {tunnelWithTorchCost}x (+ {tunnelTorchCost} torches)");
  1462. Debug.Log($" Tunnel (without torch): {tunnelWithoutTorchCost}x");
  1463. Debug.Log("CONTROLS:");
  1464. Debug.Log(" TAB: Cycle through route options");
  1465. Debug.Log(" ESC: Cancel travel planning (or cancel journey if traveling)");
  1466. Debug.Log(" C: Cancel journey while traveling");
  1467. Debug.Log(" F8: Toggle debug logs");
  1468. Debug.Log("TRAVEL CONTROLS (while traveling):");
  1469. Debug.Log(" >,→: Increase travel speed");
  1470. Debug.Log(" <,←: Decrease travel speed");
  1471. Debug.Log(" Space: Reset travel speed to normal");
  1472. Debug.Log("========================");
  1473. }
  1474. /// <summary>
  1475. /// Public getters for external systems
  1476. /// </summary>
  1477. public List<TravelRoute> GetAvailableRoutes() => availableRoutes;
  1478. public Vector2Int? GetPlannedDestination() => plannedDestination;
  1479. public bool IsTraveling() => isTraveling;
  1480. public bool CanCancelJourney() => isTraveling && currentTravelCoroutine != null;
  1481. public TravelRoute GetCurrentRoute() => currentRoute;
  1482. /// <summary>
  1483. /// Increases time speed for faster travel
  1484. /// </summary>
  1485. public void IncreaseTimeSpeed()
  1486. {
  1487. if (currentTimeSpeedIndex < timeSpeedOptions.Length - 1)
  1488. {
  1489. currentTimeSpeedIndex++;
  1490. timeMultiplier = timeSpeedOptions[currentTimeSpeedIndex];
  1491. Time.timeScale = timeMultiplier;
  1492. Debug.Log($"⏩ Time speed increased to {timeMultiplier}x");
  1493. }
  1494. }
  1495. /// <summary>
  1496. /// Decreases time speed
  1497. /// </summary>
  1498. public void DecreaseTimeSpeed()
  1499. {
  1500. if (currentTimeSpeedIndex > 0)
  1501. {
  1502. currentTimeSpeedIndex--;
  1503. timeMultiplier = timeSpeedOptions[currentTimeSpeedIndex];
  1504. Time.timeScale = timeMultiplier;
  1505. Debug.Log($"⏪ Time speed decreased to {timeMultiplier}x");
  1506. }
  1507. }
  1508. /// <summary>
  1509. /// Resets time speed to normal
  1510. /// </summary>
  1511. public void ResetTimeSpeed()
  1512. {
  1513. currentTimeSpeedIndex = 0;
  1514. timeMultiplier = timeSpeedOptions[0];
  1515. Time.timeScale = timeMultiplier;
  1516. Debug.Log($"⏸️ Time speed reset to {timeMultiplier}x");
  1517. }
  1518. /// <summary>
  1519. /// Gets current time speed multiplier
  1520. /// </summary>
  1521. public float GetTimeSpeed() => timeMultiplier;
  1522. /// <summary>
  1523. /// Methods for UI integration
  1524. /// </summary>
  1525. private bool IsTravelUIBlocking()
  1526. {
  1527. // Check both travel UI and combat popup for blocking input
  1528. bool travelUIBlocking = travelUI != null && travelUI.IsUIBlocking();
  1529. bool combatPopupBlocking = combatEventPopup != null && combatEventPopup.IsVisible;
  1530. return travelUIBlocking || combatPopupBlocking;
  1531. }
  1532. private void ShowTravelUIIfAvailable(Vector2Int destination, List<TravelRoute> routes)
  1533. {
  1534. // Direct call instead of reflection since TravelUI is now available
  1535. if (travelUI != null)
  1536. {
  1537. travelUI.ShowTravelUI(destination, routes);
  1538. }
  1539. else
  1540. {
  1541. Debug.LogWarning("⚠️ TravelUI not found, cannot show travel interface");
  1542. }
  1543. }
  1544. public void PlanTravelWithPreferences(Vector2Int destination, bool avoidFerries, bool avoidMountainPasses, bool preferRoads)
  1545. {
  1546. // Store preferences and recalculate routes
  1547. if (showDebugLogs)
  1548. {
  1549. Debug.Log($"🔄 Recalculating routes with preferences: AvoidFerries={avoidFerries}, AvoidMountains={avoidMountainPasses}, PreferRoads={preferRoads}");
  1550. }
  1551. // For now, just recalculate the same routes
  1552. // In a full implementation, these preferences would modify the pathfinding algorithm
  1553. PlanTravelTo(destination);
  1554. }
  1555. /// <summary>
  1556. /// Forces recalculation of routes and refreshes the UI - useful for debugging or preference changes
  1557. /// </summary>
  1558. [ContextMenu("DEBUG: Recalculate Routes")]
  1559. public void ForceRecalculateRoutes()
  1560. {
  1561. if (plannedDestination.HasValue)
  1562. {
  1563. Debug.Log($"🔄 TeamTravelSystem: Force recalculating routes to {plannedDestination.Value}");
  1564. Vector2Int currentPosition = teamPlacement.GetTeamPosition();
  1565. // Recalculate routes
  1566. CalculateRoutes(currentPosition, plannedDestination.Value);
  1567. // Refresh visualization
  1568. if (availableRoutes.Count > 0)
  1569. {
  1570. VisualizeAllRoutes(availableRoutes);
  1571. }
  1572. // Refresh UI if visible
  1573. if (travelUI != null && travelUI.IsVisible)
  1574. {
  1575. Debug.Log("🔄 TeamTravelSystem: Refreshing UI with recalculated routes");
  1576. travelUI.RefreshWithNewRoutes(availableRoutes);
  1577. }
  1578. Debug.Log($"✅ TeamTravelSystem: Routes recalculated - {availableRoutes.Count} routes available");
  1579. }
  1580. else
  1581. {
  1582. Debug.LogWarning("⚠️ TeamTravelSystem: No planned destination to recalculate routes for");
  1583. }
  1584. }
  1585. [ContextMenu("DEBUG: Analyze Route Times")]
  1586. public void DebugAnalyzeRouteTimes()
  1587. {
  1588. Debug.Log("🧪 Analyzing route time calculations...");
  1589. if (availableRoutes == null || availableRoutes.Count == 0)
  1590. {
  1591. Debug.LogWarning("⚠️ No routes available. Plan a route first!");
  1592. return;
  1593. }
  1594. foreach (var route in availableRoutes)
  1595. {
  1596. Debug.Log($"\n📊 === {route.routeType} Route Analysis ===");
  1597. Debug.Log($" Path length: {route.path.Count} tiles");
  1598. Debug.Log($" Pathfinding cost: {route.totalCost:F2}");
  1599. Debug.Log($" Estimated time: {route.estimatedTime:F2}h");
  1600. // Count different terrain and feature types
  1601. int ferryTiles = 0, roadTiles = 0, plainsTiles = 0, lakeTiles = 0, forestTiles = 0, tunnelTiles = 0;
  1602. float ferryTime = 0f, roadTime = 0f, plainsTime = 0f, lakeTime = 0f, forestTime = 0f, tunnelTime = 0f;
  1603. var mapData = mapMaker.GetMapData();
  1604. foreach (var pos in route.path)
  1605. {
  1606. var tile = mapData.GetTile(pos.x, pos.y);
  1607. float tileTime = GetActualMovementTime(tile);
  1608. if (tile.featureType == FeatureType.Ferry)
  1609. {
  1610. ferryTiles++;
  1611. ferryTime += tileTime;
  1612. }
  1613. else if (tile.featureType == FeatureType.Tunnel)
  1614. {
  1615. tunnelTiles++;
  1616. tunnelTime += tileTime;
  1617. }
  1618. else if (tile.featureType == FeatureType.Road)
  1619. {
  1620. roadTiles++;
  1621. roadTime += tileTime;
  1622. }
  1623. else if (tile.terrainType == TerrainType.Plains)
  1624. {
  1625. plainsTiles++;
  1626. plainsTime += tileTime;
  1627. }
  1628. else if (tile.terrainType == TerrainType.Lake)
  1629. {
  1630. lakeTiles++;
  1631. lakeTime += tileTime;
  1632. }
  1633. else if (tile.terrainType == TerrainType.Forest)
  1634. {
  1635. forestTiles++;
  1636. forestTime += tileTime;
  1637. }
  1638. }
  1639. Debug.Log($" Time Breakdown:");
  1640. if (ferryTiles > 0) Debug.Log($" 🚢 Ferry: {ferryTiles} tiles = {ferryTime:F2}h");
  1641. if (tunnelTiles > 0) Debug.Log($" 🕳️ Tunnel: {tunnelTiles} tiles = {tunnelTime:F2}h");
  1642. if (roadTiles > 0) Debug.Log($" 🛣️ Roads: {roadTiles} tiles = {roadTime:F2}h");
  1643. if (plainsTiles > 0) Debug.Log($" 🌾 Plains: {plainsTiles} tiles = {plainsTime:F2}h");
  1644. if (lakeTiles > 0) Debug.Log($" 🏊 Lake swimming: {lakeTiles} tiles = {lakeTime:F2}h");
  1645. if (forestTiles > 0) Debug.Log($" 🌲 Forest: {forestTiles} tiles = {forestTime:F2}h");
  1646. if (route.specialCosts.Count > 0)
  1647. {
  1648. Debug.Log($" 💰 Special costs: {string.Join(", ", route.specialCosts.Select(c => $"{c.Key}: {c.Value}"))}");
  1649. }
  1650. }
  1651. Debug.Log("\n🔧 Movement Speed Reference:");
  1652. Debug.Log($" Plains: {plainsMovementCost}f per tile");
  1653. Debug.Log($" Forest: {forestMovementCost}f per tile");
  1654. Debug.Log($" Mountains: {mountainMovementCost}f per tile");
  1655. Debug.Log($" Lake crossing (swimming): {lakeCrossingCost}f per tile");
  1656. Debug.Log($" Ferry: {ferryMovementCost}f per tile (should be much faster!)");
  1657. Debug.Log($" Tunnel (with torch): {tunnelWithTorchCost}f per tile");
  1658. Debug.Log($" Roads: {roadMovementCost}f per tile");
  1659. // Calculate how much faster ferry should be vs swimming
  1660. float ferrySpeedAdvantage = lakeCrossingCost / ferryMovementCost;
  1661. Debug.Log($" ⚡ Ferry is {ferrySpeedAdvantage:F1}x faster than swimming across lakes");
  1662. // Calculate how much faster tunnels are vs mountain climbing
  1663. float tunnelSpeedAdvantage = mountainMovementCost / tunnelWithTorchCost;
  1664. Debug.Log($" ⚡ Tunnels are {tunnelSpeedAdvantage:F1}x faster than climbing over mountains");
  1665. }
  1666. public void VisualizeSpecificRoute(TravelRoute route)
  1667. {
  1668. var callId = System.Guid.NewGuid().ToString("N")[..8]; // 8-character unique ID
  1669. if (showDebugLogs)
  1670. {
  1671. Debug.Log($"🎯 [{callId}] VisualizeSpecificRoute called with route: {route?.routeType}, availableRoutes count: {availableRoutes?.Count ?? 0}");
  1672. }
  1673. if (route == null)
  1674. {
  1675. Debug.LogError($"❌ [{callId}] Cannot visualize null route!");
  1676. return;
  1677. }
  1678. // If availableRoutes is empty, this might be because routes were cleared during UI interaction
  1679. // In this case, we can still visualize the single route that was passed to us
  1680. if (availableRoutes == null || availableRoutes.Count == 0)
  1681. {
  1682. Debug.LogWarning($"⚠️ [{callId}] availableRoutes is empty, visualizing single route {route.routeType}");
  1683. // Temporarily add this route to the list so the visualization system can work
  1684. if (availableRoutes == null) availableRoutes = new List<TravelRoute>();
  1685. availableRoutes.Add(route);
  1686. // Visualize just this route
  1687. ClearPathVisualization();
  1688. RenderSingleRoute(route, 0, true); // Render as selected
  1689. if (showDebugLogs)
  1690. {
  1691. Debug.Log($"🎨 [{callId}] Visualized single {route.routeType} route");
  1692. }
  1693. return;
  1694. }
  1695. // Normal path when we have available routes
  1696. // Verify the route is actually in our available routes
  1697. bool routeExists = availableRoutes.Contains(route);
  1698. if (!routeExists)
  1699. {
  1700. Debug.LogWarning($"⚠️ [{callId}] Selected route {route.routeType} is not in availableRoutes list!");
  1701. // Try to find a matching route by type
  1702. var matchingRoute = availableRoutes.FirstOrDefault(r => r?.routeType == route.routeType);
  1703. if (matchingRoute != null)
  1704. {
  1705. Debug.Log($"🔄 [{callId}] Found matching route by type, using that instead");
  1706. route = matchingRoute;
  1707. }
  1708. else
  1709. {
  1710. Debug.LogError($"❌ [{callId}] No matching route found, cannot visualize!");
  1711. return;
  1712. }
  1713. }
  1714. // Instead of clearing all routes, just highlight the selected one
  1715. HighlightSpecificRoute(route);
  1716. if (showDebugLogs)
  1717. {
  1718. Debug.Log($"🎨 [{callId}] Highlighted {route.routeType} route while keeping all routes visible");
  1719. }
  1720. }
  1721. /// <summary>
  1722. /// Highlights a specific route while keeping all routes visible (selected route on top)
  1723. /// </summary>
  1724. private void HighlightSpecificRoute(TravelRoute selectedRoute)
  1725. {
  1726. if (showDebugLogs)
  1727. {
  1728. Debug.Log($"🔍 HighlightSpecificRoute called with {selectedRoute?.routeType}, availableRoutes count: {availableRoutes.Count}");
  1729. for (int i = 0; i < availableRoutes.Count; i++)
  1730. {
  1731. Debug.Log($" Route {i}: {availableRoutes[i]?.routeType} with {availableRoutes[i]?.path?.Count ?? 0} waypoints");
  1732. }
  1733. }
  1734. // Validate that we have routes to work with
  1735. if (availableRoutes == null || availableRoutes.Count == 0)
  1736. {
  1737. Debug.LogError("❌ HighlightSpecificRoute: No available routes to render!");
  1738. return;
  1739. }
  1740. if (selectedRoute == null)
  1741. {
  1742. Debug.LogError("❌ HighlightSpecificRoute: Selected route is null!");
  1743. return;
  1744. }
  1745. // Clear and re-render all routes with proper transparency and layering
  1746. ClearPathVisualization();
  1747. // Create a copy of routes to avoid any modification issues
  1748. var routesToRender = new List<TravelRoute>(availableRoutes);
  1749. if (showDebugLogs)
  1750. {
  1751. Debug.Log($"🎨 About to render {routesToRender.Count} routes");
  1752. }
  1753. // First render non-selected routes with transparency
  1754. for (int i = 0; i < routesToRender.Count; i++)
  1755. {
  1756. var route = routesToRender[i];
  1757. if (route == null)
  1758. {
  1759. Debug.LogWarning($"⚠️ Route {i} is null, skipping");
  1760. continue;
  1761. }
  1762. if (route.path == null || route.path.Count == 0)
  1763. {
  1764. Debug.LogWarning($"⚠️ Route {i} ({route.routeType}) has no path, skipping");
  1765. continue;
  1766. }
  1767. bool isSelected = (route == selectedRoute);
  1768. if (showDebugLogs)
  1769. {
  1770. Debug.Log($"🎨 Rendering route {i}: {route.routeType}, selected: {isSelected}, pathCount: {route.path.Count}");
  1771. }
  1772. // All routes are rendered, but with different emphasis
  1773. RenderSingleRoute(route, i, isSelected);
  1774. }
  1775. if (showDebugLogs)
  1776. {
  1777. string routeName = selectedRoute?.routeType == RouteType.RoadPreferred ? "Fastest" :
  1778. selectedRoute?.routeType == RouteType.Standard ? "Shortest" : "Cheapest";
  1779. Debug.Log($"🔝 Highlighted {routeName} route - rendered {currentPathVisualizations.Count} route visualizations");
  1780. }
  1781. }
  1782. /// <summary>
  1783. /// Renders a single route with specified layering
  1784. /// </summary>
  1785. private void RenderSingleRoute(TravelRoute route, int layerIndex, bool isSelected)
  1786. {
  1787. if (route == null || route.path.Count == 0)
  1788. {
  1789. if (showDebugLogs)
  1790. {
  1791. Debug.LogWarning($"⚠️ RenderSingleRoute: Cannot render route - route is null: {route == null}, path count: {route?.path?.Count ?? 0}");
  1792. }
  1793. return;
  1794. }
  1795. if (showDebugLogs)
  1796. {
  1797. Debug.Log($"🎨 RenderSingleRoute: Creating visualization for {route.routeType}, selected: {isSelected}, layerIndex: {layerIndex}");
  1798. }
  1799. // Create path visualization
  1800. var pathObject = new GameObject($"TravelPath_{route.routeType}_{(isSelected ? "Selected" : "Normal")}");
  1801. var lineRenderer = pathObject.AddComponent<LineRenderer>();
  1802. if (showDebugLogs)
  1803. {
  1804. Debug.Log($"🔧 Created GameObject: {pathObject.name} with LineRenderer");
  1805. }
  1806. // Configure line renderer with a new material instance
  1807. var baseMaterial = pathLineMaterial ?? CreateDefaultPathMaterial();
  1808. var routeMaterial = new Material(baseMaterial);
  1809. var routeColor = GetRouteColor(route);
  1810. // Selected route gets full opacity, non-selected get good visibility
  1811. if (isSelected)
  1812. {
  1813. routeColor.a = 1.0f; // Full opacity for selected
  1814. }
  1815. else
  1816. {
  1817. routeColor.a = 0.6f; // Good visibility for non-selected (was 0.8f)
  1818. }
  1819. routeMaterial.color = routeColor;
  1820. lineRenderer.material = routeMaterial;
  1821. // Selected route gets thicker line, non-selected get thinner
  1822. float lineWidth = 1.0f;
  1823. int sortingOrder = 50;
  1824. switch (route.routeType)
  1825. {
  1826. case RouteType.RoadPreferred: // Fastest
  1827. lineWidth = isSelected ? 2.2f : 1.4f; // More dramatic difference
  1828. sortingOrder = isSelected ? 60 : 53;
  1829. break;
  1830. case RouteType.Standard: // Shortest
  1831. lineWidth = isSelected ? 2.0f : 1.2f; // More dramatic difference
  1832. sortingOrder = isSelected ? 59 : 52;
  1833. break;
  1834. case RouteType.Special: // Cheapest
  1835. lineWidth = isSelected ? 1.8f : 1.0f; // More dramatic difference
  1836. sortingOrder = isSelected ? 58 : 51;
  1837. break;
  1838. }
  1839. lineRenderer.startWidth = lineWidth;
  1840. lineRenderer.endWidth = lineWidth;
  1841. lineRenderer.positionCount = route.path.Count;
  1842. lineRenderer.useWorldSpace = true;
  1843. // Enhanced settings with proper layering
  1844. lineRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
  1845. lineRenderer.receiveShadows = false;
  1846. lineRenderer.alignment = LineAlignment.View;
  1847. lineRenderer.sortingLayerName = "Default";
  1848. lineRenderer.sortingOrder = sortingOrder;
  1849. lineRenderer.textureMode = LineTextureMode.Tile;
  1850. // Selected route gets priority Y-offset (higher above map)
  1851. float zOffset = pathLineHeight + (isSelected ? 1.0f : layerIndex * 0.2f);
  1852. // Set path points
  1853. for (int j = 0; j < route.path.Count; j++)
  1854. {
  1855. // Convert map coordinates to world coordinates properly for top-down view
  1856. Vector3 worldPos = TileToWorldPosition(route.path[j]);
  1857. // Add slight offset for non-selected routes to prevent exact overlap
  1858. if (!isSelected)
  1859. {
  1860. float offsetAmount = layerIndex * 0.02f;
  1861. worldPos.x += Mathf.Sin(j + layerIndex) * offsetAmount;
  1862. worldPos.z += Mathf.Cos(j + layerIndex) * offsetAmount; // Use Z for map Y offset
  1863. }
  1864. worldPos.y = zOffset; // Use Y for height/depth positioning
  1865. lineRenderer.SetPosition(j, worldPos);
  1866. }
  1867. currentPathVisualizations.Add(pathObject);
  1868. if (showDebugLogs)
  1869. {
  1870. string routeName = route.routeType == RouteType.RoadPreferred ? "Fastest" :
  1871. route.routeType == RouteType.Standard ? "Shortest" : "Cheapest";
  1872. Debug.Log($"✅ Successfully rendered {routeName} route: selected={isSelected}, width={lineWidth}, sorting={sortingOrder}, z-offset={zOffset}");
  1873. Debug.Log($"📊 Total visualizations now: {currentPathVisualizations.Count}");
  1874. }
  1875. }
  1876. public void HideTravelUI()
  1877. {
  1878. // Direct call instead of reflection since TravelUI is now available
  1879. if (travelUI != null)
  1880. {
  1881. travelUI.HideTravelPanel();
  1882. }
  1883. }
  1884. }
  1885. /// <summary>
  1886. /// Represents different types of routes with GPS-style optimization goals
  1887. /// </summary>
  1888. public enum RouteType
  1889. {
  1890. Standard, // Shortest path (minimal distance/waypoints)
  1891. RoadPreferred, // Fastest path (heavily prefers roads and fast terrain)
  1892. Special // Cheapest/Direct path (avoids special costs, takes more direct routes)
  1893. }
  1894. /// <summary>
  1895. /// Represents a complete travel route with all associated costs and information
  1896. /// </summary>
  1897. [System.Serializable]
  1898. public class TravelRoute
  1899. {
  1900. public RouteType routeType;
  1901. public List<Vector2Int> path;
  1902. public float totalCost;
  1903. public float totalDistance;
  1904. public float estimatedTime;
  1905. public Dictionary<string, int> specialCosts; // Special costs like ferry fees, torch usage
  1906. }
  1907. /// <summary>
  1908. /// Node used for A* pathfinding
  1909. /// </summary>
  1910. public class PathNode
  1911. {
  1912. public Vector2Int position;
  1913. public float gCost; // Distance from starting node
  1914. public float hCost; // Heuristic distance to target
  1915. public float fCost => gCost + hCost;
  1916. public PathNode parent;
  1917. }