| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085 |
- using UnityEngine;
- using System.Collections;
- using System.Collections.Generic;
- using System.Linq;
- /// <summary>
- /// New comprehensive travel system that handles movement costs, pathfinding, and visual planning.
- /// Integrates with SimpleTeamPlacement and MapMaker2 for seamless map-based travel.
- /// </summary>
- public class TeamTravelSystem : MonoBehaviour
- {
- [Header("Movement Costs")]
- [Range(0.1f, 5f)]
- public float plainsMovementCost = 1f;
- [Range(0.1f, 5f)]
- public float forestMovementCost = 2f;
- [Range(0.1f, 5f)]
- public float mountainMovementCost = 4f;
- [Range(0.1f, 2f)]
- public float roadMovementCost = 0.5f;
- [Range(0.1f, 2f)]
- public float townMovementCost = 0.1f; // Almost instant travel inside towns
- [Range(0.1f, 2f)]
- public float bridgeMovementCost = 0.6f;
- [Header("Special Travel Costs")]
- [Range(1, 100)]
- public int ferryBaseCost = 25; // Gold cost for ferry
- [Range(0.1f, 2f)]
- public float ferryMovementCost = 0.7f; // Faster than walking but costs money
- [Range(1, 50)]
- public int tunnelTorchCost = 10; // Torch cost for tunnel travel
- [Range(0.1f, 2f)]
- public float tunnelWithTorchCost = 0.8f; // Fast with torch
- [Range(1f, 5f)]
- public float tunnelWithoutTorchCost = 3f; // Slow and dangerous without torch
- [Range(1, 20)]
- public int bridgeTollCost = 5; // Gold cost for major bridge crossings
- [Range(1, 30)]
- public int mountainPassCost = 15; // Guide cost for dangerous mountain passes
- [Header("River Crossing")]
- [Range(2f, 10f)]
- public float riverCrossingCost = 2f; // Reduced from 6f - more reasonable river crossing
- [Range(1f, 8f)]
- public float lakeCrossingCost = 2.5f; // Reduced from 4f - more reasonable lake crossing
- [Range(5f, 15f)]
- public float oceanCrossingCost = 8f; // Reduced from 10f but still expensive
- [Header("Visual Path Settings")]
- public Material pathLineMaterial;
- public Color standardPathColor = Color.blue;
- public Color expensivePathColor = Color.red;
- public Color alternativePathColor = Color.yellow;
- [Range(0.1f, 5f)]
- public float pathLineWidth = 2.5f; // Increased for much better visibility
- [Range(-10f, 10f)]
- public float pathLineHeight = 1f; // Height above map tiles for path lines
- [Header("Debug")]
- public bool showDebugLogs = false;
- public bool showMovementCosts = false;
- // References
- private SimpleTeamPlacement teamPlacement;
- private MapMaker2 mapMaker;
- private Camera mapCamera;
- private TravelUI travelUI;
- private CombatEventPopupUXML combatEventPopup;
- // Travel state
- private Vector2Int? plannedDestination;
- private List<TravelRoute> availableRoutes = new List<TravelRoute>();
- private List<GameObject> currentPathVisualizations = new List<GameObject>(); // Store multiple route lines
- private bool isTraveling = false;
- private int currentRouteIndex = 0; // For cycling through route visualizations
- // Journey cancellation
- private bool shouldCancelJourney = false;
- private Coroutine currentTravelCoroutine = null;
- private TravelRoute currentRoute = null;
- // Time skip functionality
- [Header("Time Controls")]
- public float timeMultiplier = 1f; // Current time speed multiplier
- public float[] timeSpeedOptions = { 1f, 2f, 4f, 8f }; // Available speed options
- private int currentTimeSpeedIndex = 0;
- // Static instance
- public static TeamTravelSystem Instance { get; private set; }
- void Awake()
- {
- if (Instance == null)
- {
- Instance = this;
- }
- else
- {
- Debug.LogWarning("Multiple TeamTravelSystem instances found. Destroying this one.");
- Destroy(gameObject);
- return;
- }
- }
- void Start()
- {
- // Find required components
- teamPlacement = FindFirstObjectByType<SimpleTeamPlacement>();
- mapMaker = FindFirstObjectByType<MapMaker2>();
- mapCamera = Camera.main;
- travelUI = FindFirstObjectByType<TravelUI>();
- combatEventPopup = FindFirstObjectByType<CombatEventPopupUXML>();
- // Initialize pathLineMaterial if not assigned
- if (pathLineMaterial == null)
- {
- pathLineMaterial = CreateDefaultPathMaterial();
- }
- if (teamPlacement == null)
- {
- Debug.LogError("SimpleTeamPlacement not found! TeamTravelSystem requires it.");
- }
- if (mapMaker == null)
- {
- Debug.LogError("MapMaker2 not found! TeamTravelSystem requires it.");
- }
- if (travelUI == null)
- {
- Debug.LogWarning("TravelUI not found! UI features will be limited.");
- }
- }
- void Update()
- {
- HandleInput();
- }
- private void HandleInput()
- {
- // Time speed controls (available during travel)
- if (isTraveling)
- {
- if (Input.GetKeyDown(KeyCode.Period) || Input.GetKeyDown(KeyCode.RightArrow)) // > key or right arrow
- {
- IncreaseTimeSpeed();
- }
- else if (Input.GetKeyDown(KeyCode.Comma) || Input.GetKeyDown(KeyCode.LeftArrow)) // < key or left arrow
- {
- DecreaseTimeSpeed();
- }
- else if (Input.GetKeyDown(KeyCode.Space)) // Space to reset speed
- {
- ResetTimeSpeed();
- }
- else if (Input.GetKeyDown(KeyCode.Escape) || Input.GetKeyDown(KeyCode.C)) // ESC or C to cancel journey
- {
- CancelJourney();
- }
- }
- // Handle map clicking for travel planning (only if click is outside UI panel)
- if (Input.GetMouseButtonDown(0) && !isTraveling)
- {
- // Check if click is within the UI panel bounds
- if (travelUI != null && travelUI.IsVisible && travelUI.IsPointWithinUI(Input.mousePosition))
- {
- return; // Block map interaction if clicking within UI panel
- }
- // Check if click is within the combat popup bounds
- if (combatEventPopup != null && combatEventPopup.IsVisible && combatEventPopup.IsPointWithinUI(Input.mousePosition))
- {
- return; // Block map interaction if clicking within combat popup
- }
- Vector3 mouseWorldPos = GetMouseWorldPosition();
- if (mouseWorldPos != Vector3.zero)
- {
- Vector2Int clickedTile = WorldToTilePosition(mouseWorldPos);
- PlanTravelTo(clickedTile);
- }
- }
- // Check if TravelUI is blocking other input (non-map clicks)
- if (IsTravelUIBlocking())
- {
- return; // Block other inputs but allow map clicks above
- }
- // Cancel travel planning
- if (Input.GetKeyDown(KeyCode.Escape))
- {
- CancelTravelPlanning();
- }
- // Cycle through route visualizations
- if (Input.GetKeyDown(KeyCode.Tab) && availableRoutes.Count > 1)
- {
- currentRouteIndex = (currentRouteIndex + 1) % availableRoutes.Count;
- var selectedRoute = availableRoutes[currentRouteIndex];
- // Highlight the selected route without clearing others
- HighlightSpecificRoute(selectedRoute);
- // Update the UI to show the new selected route
- if (travelUI != null)
- {
- travelUI.UpdateUIForSelectedRoute(selectedRoute);
- }
- if (showDebugLogs)
- {
- Debug.Log($"Switched to {selectedRoute.routeType} route (Time: {selectedRoute.estimatedTime:F1}h)");
- }
- }
- // Debug key to show movement costs
- if (Input.GetKeyDown(KeyCode.C) && showMovementCosts)
- {
- ShowMovementCostInfo();
- }
- // Toggle debug logs
- if (Input.GetKeyDown(KeyCode.F8))
- {
- showDebugLogs = !showDebugLogs;
- Debug.Log($"Travel System Debug Logs: {(showDebugLogs ? "ENABLED" : "DISABLED")}");
- }
- }
- /// <summary>
- /// Plans travel to the specified destination and shows available routes
- /// </summary>
- public void PlanTravelTo(Vector2Int destination)
- {
- if (!teamPlacement.IsTeamPlaced())
- {
- Debug.LogWarning("❌ Cannot plan travel: Team not placed yet");
- return;
- }
- Vector2Int currentPosition = teamPlacement.GetTeamPosition();
- if (currentPosition == destination)
- {
- Debug.Log("Already at destination!");
- return;
- }
- plannedDestination = destination;
- currentRouteIndex = 0; // Reset route index for new destination
- if (showDebugLogs)
- {
- Debug.Log($"🗺️ Planning travel from {currentPosition} to {destination}");
- }
- // Calculate different route options
- CalculateRoutes(currentPosition, destination);
- // If UI is already visible, refresh it with new routes
- if (travelUI != null && travelUI.IsVisible)
- {
- Debug.Log("🔄 TeamTravelSystem: UI is visible, refreshing with new routes");
- travelUI.RefreshWithNewRoutes(availableRoutes);
- }
- // Show all available routes visually with different colors
- if (availableRoutes.Count > 0)
- {
- // Clear any existing visualizations first
- ClearPathVisualization();
- // Select the fastest route (shortest estimated time) by default
- var routesByTime = availableRoutes.OrderBy(r => r.estimatedTime).ToList();
- TravelRoute selectedRoute = routesByTime.First();
- // Update currentRouteIndex to match the selected route in the original list
- currentRouteIndex = availableRoutes.IndexOf(selectedRoute);
- // Visualize all routes with the fastest one highlighted on top
- VisualizeAllRoutes(availableRoutes);
- if (showDebugLogs)
- {
- Debug.Log($"🔧 About to show UI for {selectedRoute.routeType} route with {selectedRoute.path.Count} waypoints");
- }
- // Show travel UI if available
- ShowTravelUIIfAvailable(destination, availableRoutes);
- if (showDebugLogs)
- {
- Debug.Log($"Found {availableRoutes.Count} route options. Selected {selectedRoute.routeType} route: {selectedRoute.estimatedTime:F1}h time");
- Debug.Log("Use TAB to cycle through different route options");
- }
- }
- }
- /// <summary>
- /// Calculates different route options including fastest, shortest, and cheapest routes
- /// </summary>
- private void CalculateRoutes(Vector2Int start, Vector2Int destination)
- {
- availableRoutes.Clear();
- var mapData = mapMaker.GetMapData();
- if (mapData == null) return;
- if (showDebugLogs)
- {
- Debug.Log($"🔍 Calculating routes from {start} to {destination}");
- }
- // Fastest route (heavily prefers roads and fast terrain)
- var fastestRoute = FindPath(start, destination, RouteType.RoadPreferred);
- if (fastestRoute != null)
- {
- availableRoutes.Add(fastestRoute);
- if (showDebugLogs)
- {
- Debug.Log($"Fastest route: {fastestRoute.estimatedTime:F1}h time, {fastestRoute.path.Count} waypoints");
- }
- }
- // Shortest route (direct path, minimal waypoints)
- var shortestRoute = FindPath(start, destination, RouteType.Standard);
- if (shortestRoute != null)
- {
- bool similar = RoutesSimilar(fastestRoute, shortestRoute);
- if (showDebugLogs)
- {
- Debug.Log($"Shortest route: {shortestRoute.estimatedTime:F1}h time, {shortestRoute.path.Count} waypoints, similar to fastest: {similar}");
- }
- if (!similar)
- {
- availableRoutes.Add(shortestRoute);
- }
- }
- // Cheapest route (minimizes resource costs first, then travel time)
- var cheapestRoute = FindCheapestPath(start, destination);
- if (cheapestRoute != null)
- {
- bool similarToFastest = RoutesSimilar(fastestRoute, cheapestRoute);
- bool similarToShortest = RoutesSimilar(shortestRoute, cheapestRoute);
- if (showDebugLogs)
- {
- Debug.Log($"Cheapest route: {cheapestRoute.estimatedTime:F1}h time, {GetTotalResourceCost(cheapestRoute)} resource cost, {cheapestRoute.path.Count} waypoints");
- }
- if (!similarToFastest && !similarToShortest)
- {
- availableRoutes.Add(cheapestRoute);
- }
- }
- // If we ended up with only one route, try to generate alternatives with different parameters
- if (availableRoutes.Count == 1)
- {
- if (showDebugLogs)
- {
- Debug.Log("Only one unique route found, generating alternatives...");
- }
- // Try a more direct route that ignores some terrain penalties
- var directRoute = FindDirectPath(start, destination);
- if (directRoute != null && !RoutesSimilar(availableRoutes[0], directRoute))
- {
- availableRoutes.Add(directRoute);
- if (showDebugLogs)
- {
- Debug.Log($"Direct route added: {directRoute.estimatedTime:F1}h time");
- }
- }
- }
- if (showDebugLogs)
- {
- Debug.Log($"📊 Total unique routes calculated: {availableRoutes.Count}");
- }
- }
- /// <summary>
- /// Finds the best path between two points using A* pathfinding with different route preferences
- /// </summary>
- private TravelRoute FindPath(Vector2Int start, Vector2Int destination, RouteType routeType)
- {
- var mapData = mapMaker.GetMapData();
- Debug.Log($"🗺️ Finding {routeType} path from {start} to {destination}");
- var openSet = new List<PathNode>();
- var closedSet = new HashSet<Vector2Int>();
- var allNodes = new Dictionary<Vector2Int, PathNode>();
- var startNode = new PathNode
- {
- position = start,
- gCost = 0,
- hCost = CalculateHeuristic(start, destination, routeType),
- parent = null
- };
- openSet.Add(startNode);
- allNodes[start] = startNode;
- int iterations = 0;
- const int maxIterations = 10000; // Prevent infinite loops
- while (openSet.Count > 0 && iterations < maxIterations)
- {
- iterations++;
- // Find node with lowest fCost
- var currentNode = openSet.OrderBy(n => n.fCost).ThenBy(n => n.hCost).First();
- openSet.Remove(currentNode);
- closedSet.Add(currentNode.position);
- // Reached destination
- if (currentNode.position == destination)
- {
- var route = ReconstructRoute(currentNode, routeType);
- if (showDebugLogs)
- {
- Debug.Log($"{routeType} path found in {iterations} iterations, final cost: {currentNode.gCost:F1}");
- }
- // Analyze the route composition
- if (showDebugLogs)
- {
- AnalyzeRouteComposition(route);
- }
- return route;
- }
- // Check neighbors
- var neighbors = GetNeighbors(currentNode.position);
- foreach (var neighbor in neighbors)
- {
- if (closedSet.Contains(neighbor) || !mapData.IsValidPosition(neighbor.x, neighbor.y))
- continue;
- float movementCost = GetMovementCost(currentNode.position, neighbor, routeType);
- if (movementCost < 0) continue; // Impassable terrain
- float tentativeGCost = currentNode.gCost + movementCost;
- if (!allNodes.ContainsKey(neighbor))
- {
- allNodes[neighbor] = new PathNode
- {
- position = neighbor,
- gCost = float.MaxValue,
- hCost = CalculateHeuristic(neighbor, destination, routeType),
- parent = null
- };
- }
- var neighborNode = allNodes[neighbor];
- if (tentativeGCost < neighborNode.gCost)
- {
- neighborNode.parent = currentNode;
- neighborNode.gCost = tentativeGCost;
- if (!openSet.Contains(neighborNode))
- {
- openSet.Add(neighborNode);
- }
- }
- }
- }
- Debug.LogWarning($"❌ {routeType} path not found after {iterations} iterations (max: {maxIterations})");
- return null; // No path found
- }
- /// <summary>
- /// Gets movement cost for moving from one tile to another, considering route type preferences
- /// </summary>
- private float GetMovementCost(Vector2Int from, Vector2Int to, RouteType routeType)
- {
- var mapData = mapMaker.GetMapData();
- var toTile = mapData.GetTile(to.x, to.y);
- // Base movement cost by terrain
- float baseCost = GetTerrainMovementCost(toTile);
- // Feature modifications
- float featureCost = GetFeatureMovementCost(toTile, routeType);
- // Combine costs
- float totalCost = baseCost * featureCost;
- // Route type adjustments
- switch (routeType)
- {
- case RouteType.Standard:
- // Shortest route: minimize distance regardless of terrain difficulty
- // No terrain or road preferences - pure distance optimization
- break;
- case RouteType.RoadPreferred:
- // Fastest route: heavily favors roads and fast terrain, considers costly but fast options
- if (toTile.featureType == FeatureType.Road)
- {
- totalCost *= 0.3f; // Strong road preference (70% discount) - roads are much faster but realistic
- }
- else if (toTile.featureType == FeatureType.Tunnel)
- {
- // Tunnels are fast for fastest route (ignore resource cost for speed optimization)
- totalCost *= 0.4f; // Very fast travel through tunnels
- }
- else if (toTile.featureType == FeatureType.Ferry)
- {
- // Ferries are reasonably fast for fastest route
- totalCost *= 0.6f; // Fast water crossing
- }
- else
- {
- // Penalize non-road movement for road-preferred routes
- totalCost *= 1.5f; // 50% penalty for off-road movement
- }
- break;
- case RouteType.Special:
- // Cheapest route: avoid resource costs, but don't make travel time unrealistic
- // Moderate penalties for features that cost resources
- if (toTile.featureType == FeatureType.Ferry || toTile.featureType == FeatureType.Tunnel)
- totalCost *= 3.0f; // Significant penalty for resource-costing features (was 10x)
- // Moderate penalties for features that might trigger resource costs
- if (toTile.featureType == FeatureType.Bridge)
- totalCost *= 2.0f; // Penalty for bridges (potential tolls) (was 5x)
- // Light penalty for terrain that requires guides/special costs
- if (toTile.terrainType == TerrainType.Mountain)
- totalCost *= 2.0f; // Penalty for mountains (guide costs) (was 5x)
- // Still avoid slow terrain, but secondary to cost avoidance
- if (toTile.terrainType == TerrainType.River || toTile.terrainType == TerrainType.Lake)
- totalCost *= 1.5f; // Light penalty for water crossings (was 2x)
- // Prefer roads since they're free and fast
- if (toTile.featureType == FeatureType.Road)
- totalCost *= 0.5f; // Good road preference (50% discount)
- break;
- }
- return totalCost;
- }
- /// <summary>
- /// Gets base movement cost for different terrain types
- /// </summary>
- private float GetTerrainMovementCost(MapTile tile)
- {
- switch (tile.terrainType)
- {
- case TerrainType.Plains:
- return plainsMovementCost;
- case TerrainType.Forest:
- return forestMovementCost;
- case TerrainType.Mountain:
- return mountainMovementCost;
- case TerrainType.Ocean:
- return oceanCrossingCost; // Allow dangerous ocean crossing
- case TerrainType.Lake:
- return lakeCrossingCost; // Allow swimming across lakes
- case TerrainType.River:
- return riverCrossingCost; // Allow slow river crossing
- default:
- return plainsMovementCost;
- }
- }
- /// <summary>
- /// Gets movement cost modifications from features (roads, bridges, etc.)
- /// </summary>
- private float GetFeatureMovementCost(MapTile tile, RouteType routeType)
- {
- switch (tile.featureType)
- {
- case FeatureType.Road:
- return roadMovementCost; // Fast movement on roads
- case FeatureType.Bridge:
- return roadMovementCost; // Bridges are as fast as roads (crossing structures)
- case FeatureType.Town:
- case FeatureType.Village:
- return townMovementCost; // Very fast through settlements
- case FeatureType.Ferry:
- return ferryMovementCost; // Ferries are fast transportation (cost is in gold, not time)
- case FeatureType.Tunnel:
- return tunnelWithTorchCost; // Tunnels are fast with torch (cost is in torches, not time)
- default:
- return 1f; // No modification
- }
- }
- /// <summary>
- /// Calculates heuristic distance for A* pathfinding with route type considerations
- /// </summary>
- private float CalculateHeuristic(Vector2Int from, Vector2Int to, RouteType routeType = RouteType.Standard)
- {
- float baseHeuristic = Vector2Int.Distance(from, to) * plainsMovementCost;
- // For road-preferred routes, make the heuristic slightly more optimistic about finding roads
- if (routeType == RouteType.RoadPreferred)
- {
- // Assume we'll find roads for some of the journey
- baseHeuristic *= 0.8f; // Slightly more optimistic heuristic
- }
- return baseHeuristic;
- }
- /// <summary>
- /// Gets valid neighboring tiles for pathfinding
- /// </summary>
- private List<Vector2Int> GetNeighbors(Vector2Int position)
- {
- var neighbors = new List<Vector2Int>();
- // 8-directional movement
- for (int dx = -1; dx <= 1; dx++)
- {
- for (int dy = -1; dy <= 1; dy++)
- {
- if (dx == 0 && dy == 0) continue;
- Vector2Int neighbor = new Vector2Int(position.x + dx, position.y + dy);
- neighbors.Add(neighbor);
- }
- }
- return neighbors;
- }
- /// <summary>
- /// Reconstructs the final route from pathfinding results
- /// </summary>
- private TravelRoute ReconstructRoute(PathNode destinationNode, RouteType routeType)
- {
- var route = new TravelRoute
- {
- routeType = routeType,
- path = new List<Vector2Int>(),
- totalCost = destinationNode.gCost,
- totalDistance = 0f,
- estimatedTime = 0f,
- specialCosts = new Dictionary<string, int>()
- };
- var current = destinationNode;
- while (current != null)
- {
- route.path.Insert(0, current.position);
- current = current.parent;
- }
- // Calculate additional route information
- CalculateRouteDetails(route);
- return route;
- }
- /// <summary>
- /// Finds a more direct path that ignores some terrain penalties for route variety
- /// </summary>
- private TravelRoute FindDirectPath(Vector2Int start, Vector2Int destination)
- {
- var mapData = mapMaker.GetMapData();
- Debug.Log($"🎯 Finding direct path from {start} to {destination}");
- var openSet = new List<PathNode>();
- var closedSet = new HashSet<Vector2Int>();
- var allNodes = new Dictionary<Vector2Int, PathNode>();
- var startNode = new PathNode
- {
- position = start,
- gCost = 0,
- hCost = CalculateHeuristic(start, destination),
- parent = null
- };
- openSet.Add(startNode);
- allNodes[start] = startNode;
- int iterations = 0;
- const int maxIterations = 10000;
- while (openSet.Count > 0 && iterations < maxIterations)
- {
- iterations++;
- var currentNode = openSet.OrderBy(n => n.fCost).ThenBy(n => n.hCost).First();
- openSet.Remove(currentNode);
- closedSet.Add(currentNode.position);
- if (currentNode.position == destination)
- {
- var route = ReconstructRoute(currentNode, RouteType.Special);
- route.routeType = RouteType.Special; // Mark as direct route
- Debug.Log($"✅ Direct path found in {iterations} iterations, final cost: {currentNode.gCost:F1}");
- CalculateRouteDetails(route);
- return route;
- }
- var neighbors = GetNeighbors(currentNode.position);
- foreach (var neighbor in neighbors)
- {
- if (closedSet.Contains(neighbor) || !mapData.IsValidPosition(neighbor.x, neighbor.y))
- continue;
- // Use simplified movement cost for direct routes (less terrain penalty)
- float movementCost = GetDirectMovementCost(currentNode.position, neighbor);
- if (movementCost < 0) continue;
- float tentativeGCost = currentNode.gCost + movementCost;
- if (!allNodes.ContainsKey(neighbor))
- {
- allNodes[neighbor] = new PathNode
- {
- position = neighbor,
- gCost = float.MaxValue,
- hCost = CalculateHeuristic(neighbor, destination),
- parent = null
- };
- }
- var neighborNode = allNodes[neighbor];
- if (tentativeGCost < neighborNode.gCost)
- {
- neighborNode.parent = currentNode;
- neighborNode.gCost = tentativeGCost;
- if (!openSet.Contains(neighborNode))
- {
- openSet.Add(neighborNode);
- }
- }
- }
- }
- Debug.LogWarning($"❌ Direct path not found after {iterations} iterations");
- return null;
- }
- /// <summary>
- /// Simplified movement cost calculation for direct routes
- /// </summary>
- private float GetDirectMovementCost(Vector2Int from, Vector2Int to)
- {
- var mapData = mapMaker.GetMapData();
- var toTile = mapData.GetTile(to.x, to.y);
- // Simplified costs that emphasize directness over terrain optimization
- switch (toTile.terrainType)
- {
- case TerrainType.Plains:
- return 1.0f;
- case TerrainType.Forest:
- return 1.5f; // Less penalty than normal
- case TerrainType.Mountain:
- return 2.0f; // Much less penalty for directness
- case TerrainType.River:
- return 1.8f;
- case TerrainType.Lake:
- return 2.0f;
- case TerrainType.Ocean:
- return 4.0f; // Still expensive but possible
- default:
- return 1.0f;
- }
- }
- /// <summary>
- /// Finds the cheapest path that minimizes resource costs (gold) first, then travel time
- /// Uses the Special route type but should calculate actual travel time correctly
- /// </summary>
- private TravelRoute FindCheapestPath(Vector2Int start, Vector2Int destination)
- {
- // Use the standard A* pathfinding with Special route type (which avoids costly features)
- return FindPath(start, destination, RouteType.Special);
- }
- /// <summary>
- /// Gets the resource cost for entering a specific tile
- /// </summary>
- private float GetResourceCostForTile(Vector2Int position)
- {
- var mapData = mapMaker.GetMapData();
- var tile = mapData.GetTile(position.x, position.y);
- switch (tile.featureType)
- {
- case FeatureType.Ferry:
- return ferryBaseCost; // Direct gold cost
- case FeatureType.Tunnel:
- return tunnelTorchCost; // Torch cost (treated as gold equivalent)
- case FeatureType.Bridge:
- return 2.0f; // Potential bridge toll (small cost to prefer other routes)
- default:
- break;
- }
- // Terrain-based resource costs (for guide fees, etc.)
- switch (tile.terrainType)
- {
- case TerrainType.Mountain:
- return 3.0f; // Potential guide cost for dangerous areas
- default:
- return 0.0f; // No resource cost
- }
- }
- /// <summary>
- /// Calculates detailed information about the route (distance, time, special costs)
- /// </summary>
- private void CalculateRouteDetails(TravelRoute route)
- {
- var mapData = mapMaker.GetMapData();
- route.totalDistance = route.path.Count;
- route.estimatedTime = route.totalCost * 0.5f; // Rough time estimate
- // Check for special costs
- int bridgeCount = 0;
- int mountainTileCount = 0;
- foreach (var position in route.path)
- {
- var tile = mapData.GetTile(position.x, position.y);
- // Ferry costs
- if (tile.featureType == FeatureType.Ferry)
- {
- if (!route.specialCosts.ContainsKey("Ferry"))
- route.specialCosts["Ferry"] = ferryBaseCost;
- }
- // Tunnel costs
- else if (tile.featureType == FeatureType.Tunnel)
- {
- if (!route.specialCosts.ContainsKey("Tunnel Torch"))
- route.specialCosts["Tunnel Torch"] = tunnelTorchCost;
- }
- // Bridge tolls (for major bridges)
- else if (tile.featureType == FeatureType.Bridge)
- {
- bridgeCount++;
- }
- // Mountain passes (dangerous mountain areas need guides)
- else if (tile.terrainType == TerrainType.Mountain)
- {
- mountainTileCount++;
- }
- }
- // Add bridge toll costs (only for longer bridge crossings)
- if (bridgeCount >= 3) // Only charge for significant bridge crossings
- {
- route.specialCosts["Bridge Toll"] = bridgeTollCost;
- }
- // Add mountain guide costs (for extensive mountain travel)
- if (mountainTileCount >= 5) // Only charge for dangerous mountain passes
- {
- route.specialCosts["Mountain Guide"] = mountainPassCost;
- }
- }
- /// <summary>
- /// Calculates the total resource cost (gold equivalent) for a route
- /// </summary>
- private int GetTotalResourceCost(TravelRoute route)
- {
- if (route?.specialCosts == null) return 0;
- int totalCost = 0;
- foreach (var cost in route.specialCosts)
- {
- totalCost += cost.Value; // All costs are in gold or gold-equivalent
- }
- return totalCost;
- }
- /// <summary>
- /// Checks if two routes are essentially the same
- /// </summary>
- private bool RoutesSimilar(TravelRoute route1, TravelRoute route2)
- {
- if (route1 == null || route2 == null) return false;
- // Consider routes similar if they have >90% path overlap (more strict to allow more variations)
- var overlap = route1.path.Intersect(route2.path).Count();
- var maxLength = Mathf.Max(route1.path.Count, route2.path.Count);
- bool similar = (float)overlap / maxLength > 0.9f;
- Debug.Log($"🔍 Route similarity check: {overlap}/{maxLength} = {((float)overlap / maxLength):F2} > 0.9? {similar}");
- return similar;
- }
- /// <summary>
- /// Analyzes and logs the composition of a route for debugging
- /// </summary>
- private void AnalyzeRouteComposition(TravelRoute route)
- {
- if (route == null || route.path == null) return;
- var mapData = mapMaker.GetMapData();
- int plainsCount = 0, forestCount = 0, mountainCount = 0, roadCount = 0, riverCount = 0, lakeCount = 0;
- float totalMovementCost = 0;
- float roadBonusTotal = 0;
- foreach (var pos in route.path)
- {
- var tile = mapData.GetTile(pos.x, pos.y);
- float tileCost = GetMovementCost(pos, pos, route.routeType);
- totalMovementCost += tileCost;
- switch (tile.terrainType)
- {
- case TerrainType.Plains: plainsCount++; break;
- case TerrainType.Forest: forestCount++; break;
- case TerrainType.Mountain: mountainCount++; break;
- case TerrainType.River: riverCount++; break;
- case TerrainType.Lake: lakeCount++; break;
- }
- if (tile.featureType == FeatureType.Road)
- {
- roadCount++;
- // Calculate road bonus for this route type
- switch (route.routeType)
- {
- case RouteType.Standard:
- roadBonusTotal += GetTerrainMovementCost(tile) * (1f - 0.6f); // 40% discount
- break;
- case RouteType.RoadPreferred:
- roadBonusTotal += GetTerrainMovementCost(tile) * (1f - 0.2f); // 80% discount
- break;
- }
- }
- }
- float avgCostPerTile = totalMovementCost / route.path.Count;
- float roadPercentage = (float)roadCount / route.path.Count * 100f;
- Debug.Log($"📊 Route Analysis ({route.routeType}): {route.path.Count} tiles");
- Debug.Log($" Terrain: Plains:{plainsCount}, Forest:{forestCount}, Mountain:{mountainCount}, Rivers:{riverCount}, Lakes:{lakeCount}");
- Debug.Log($" Roads: {roadCount} tiles ({roadPercentage:F1}%), saved {roadBonusTotal:F1} cost");
- Debug.Log($" Average cost per tile: {avgCostPerTile:F2} (Total: {totalMovementCost:F1})");
- Debug.Log($" Time calculation: {route.estimatedTime:F1}h = {route.totalCost:F1} cost × 0.5 multiplier");
- // Explain why this route type might be different
- string explanation = "";
- switch (route.routeType)
- {
- case RouteType.Standard:
- explanation = "Shortest route minimizes distance and waypoints";
- break;
- case RouteType.RoadPreferred:
- explanation = $"Fastest route heavily favors roads (90% discount) - {roadPercentage:F1}% on roads";
- break;
- case RouteType.Special:
- explanation = "Cheapest/Direct route avoids special costs and takes direct paths";
- break;
- }
- Debug.Log($" Strategy: {explanation}");
- }
- /// <summary>
- /// Visualizes the selected route on the map
- /// </summary>
- private void VisualizeRoute(TravelRoute route)
- {
- if (showDebugLogs)
- {
- Debug.Log($"🎨 VisualizeRoute called with route: {route?.routeType} ({route?.path?.Count ?? 0} points)");
- }
- ClearPathVisualization();
- if (route == null || route.path.Count == 0)
- {
- if (showDebugLogs)
- {
- Debug.LogWarning("❌ Cannot visualize route: route is null or has no path points");
- }
- return;
- }
- // Create path visualization
- var pathObject = new GameObject("TravelPath");
- var lineRenderer = pathObject.AddComponent<LineRenderer>();
- // Configure line renderer with simple, reliable settings
- lineRenderer.material = pathLineMaterial ?? CreateDefaultPathMaterial();
- lineRenderer.material.color = GetRouteColor(route);
- // Force line width to 1.0 for consistent visibility
- lineRenderer.startWidth = 1.0f;
- lineRenderer.endWidth = 1.0f;
- lineRenderer.positionCount = route.path.Count;
- lineRenderer.useWorldSpace = true;
- // Simple LineRenderer settings for reliable visibility
- lineRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
- lineRenderer.receiveShadows = false;
- lineRenderer.alignment = LineAlignment.View; // Face camera
- lineRenderer.textureMode = LineTextureMode.Tile;
- lineRenderer.sortingLayerName = "Default";
- lineRenderer.sortingOrder = 50; // High sorting order but not excessive
- // Debug the width setting
- if (showDebugLogs)
- {
- Debug.Log($"🔧 Setting LineRenderer width to: {pathLineWidth} (startWidth: {lineRenderer.startWidth}, endWidth: {lineRenderer.endWidth})");
- }
- // Set path points
- for (int i = 0; i < route.path.Count; i++)
- {
- Vector3 worldPos = TileToWorldPosition(route.path[i]);
- worldPos.y = pathLineHeight; // Place above map tiles
- lineRenderer.SetPosition(i, worldPos);
- }
- currentPathVisualizations.Add(pathObject);
- if (showDebugLogs)
- {
- Debug.Log($"📍 Visualized {route.routeType} route with {route.path.Count} waypoints");
- Debug.Log($"🔧 LineRenderer config: startWidth={lineRenderer.startWidth}, endWidth={lineRenderer.endWidth}, material={lineRenderer.material?.name}, color={lineRenderer.material?.color}");
- Debug.Log($"🎯 GameObject created: {pathObject.name} at position {pathObject.transform.position}");
- }
- }
- /// <summary>
- /// Visualizes all available routes with different colors and proper layering
- /// </summary>
- private void VisualizeAllRoutes(List<TravelRoute> routes)
- {
- ClearPathVisualization();
- // Render all routes with the first one (fastest by time) selected by default
- var routesByTime = routes.OrderBy(r => r.estimatedTime).ToList();
- var defaultSelected = routesByTime.FirstOrDefault();
- for (int i = 0; i < routes.Count; i++)
- {
- var route = routes[i];
- if (route == null || route.path.Count == 0) continue;
- bool isSelected = (route == defaultSelected);
- RenderSingleRoute(route, i, isSelected);
- }
- if (showDebugLogs)
- {
- string selectedName = defaultSelected?.routeType == RouteType.RoadPreferred ? "Fastest" :
- defaultSelected?.routeType == RouteType.Standard ? "Shortest" : "Cheapest";
- Debug.Log($"🎨 Visualized {routes.Count} routes with {selectedName} as default selected (on top)");
- }
- } /// <summary>
- /// Gets the appropriate color for a route based on its type (GPS-style)
- /// </summary>
- private Color GetRouteColor(TravelRoute route)
- {
- // GPS-style color coding
- switch (route.routeType)
- {
- case RouteType.RoadPreferred: // Fastest route
- if (showDebugLogs) Debug.Log($"🎨 Route color: GREEN (fastest route)");
- return Color.green; // Green for fastest (like GPS)
- case RouteType.Standard: // Shortest route
- if (showDebugLogs) Debug.Log($"🎨 Route color: BLUE (shortest route)");
- return standardPathColor; // Blue for shortest
- case RouteType.Special: // Cheapest/Direct route
- if (showDebugLogs) Debug.Log($"🎨 Route color: YELLOW (cheapest/direct route)");
- return alternativePathColor; // Yellow for cheapest/alternative
- default:
- if (showDebugLogs) Debug.Log($"🎨 Route color: BLUE (default)");
- return standardPathColor;
- }
- }
- /// <summary>
- /// Creates a default material for path visualization
- /// </summary>
- private Material CreateDefaultPathMaterial()
- {
- // Use simple Sprites/Default shader for reliable rendering
- Shader shader = Shader.Find("Sprites/Default");
- if (shader == null)
- shader = Shader.Find("Legacy Shaders/Particles/Alpha Blended Premultiply");
- if (shader == null)
- shader = Shader.Find("Unlit/Color");
- if (shader == null)
- shader = Shader.Find("Standard");
- var material = new Material(shader ?? Shader.Find("Diffuse"));
- material.color = standardPathColor;
- if (showDebugLogs)
- {
- Debug.Log($"🎨 Created path material with shader: {shader?.name ?? "Unknown"}");
- }
- return material;
- }
- /// <summary>
- /// Clears any existing path visualization
- /// </summary>
- private void ClearPathVisualization()
- {
- foreach (var pathViz in currentPathVisualizations)
- {
- if (pathViz != null)
- {
- DestroyImmediate(pathViz);
- }
- }
- currentPathVisualizations.Clear();
- }
- /// <summary>
- /// Shows only the selected route with transparency for better marker visibility during travel
- /// </summary>
- private void ShowTravelRouteOnly(TravelRoute route)
- {
- if (route == null || route.path.Count == 0)
- {
- if (showDebugLogs)
- {
- Debug.LogWarning("❌ Cannot show travel route: route is null or has no path");
- }
- return;
- }
- // Clear all existing route visualizations
- ClearPathVisualization();
- if (showDebugLogs)
- {
- Debug.Log($"🎯 Showing only {route.routeType} route with transparency for travel");
- }
- // Create a semi-transparent visualization of the selected route
- var pathObject = new GameObject($"TravelPath_{route.routeType}_Traveling");
- var lineRenderer = pathObject.AddComponent<LineRenderer>();
- // Configure line renderer with semi-transparent material
- var baseMaterial = pathLineMaterial ?? CreateDefaultPathMaterial();
- var routeMaterial = new Material(baseMaterial);
- var routeColor = GetRouteColor(route);
- // Set to 50% transparency for better marker visibility
- routeColor.a = 0.5f;
- routeMaterial.color = routeColor;
- lineRenderer.material = routeMaterial;
- // Use a thinner line during travel to be less obtrusive
- lineRenderer.startWidth = 1.2f;
- lineRenderer.endWidth = 1.2f;
- lineRenderer.positionCount = route.path.Count;
- // Set sorting order to be below the team marker
- lineRenderer.sortingOrder = 45; // Lower than team marker
- // Set path points
- for (int i = 0; i < route.path.Count; i++)
- {
- Vector3 worldPos = TileToWorldPosition(route.path[i]);
- worldPos.y = pathLineHeight; // Keep consistent height
- lineRenderer.SetPosition(i, worldPos);
- }
- currentPathVisualizations.Add(pathObject);
- if (showDebugLogs)
- {
- Debug.Log($"✅ Travel route visualization created: {route.routeType} with 50% transparency");
- }
- }
- /// <summary>
- /// Cancels current travel planning
- /// </summary>
- public void CancelTravelPlanning()
- {
- plannedDestination = null;
- availableRoutes.Clear();
- ClearPathVisualization();
- HideTravelUI();
- if (showDebugLogs)
- {
- Debug.Log("❌ Travel planning cancelled");
- }
- }
- /// <summary>
- /// Starts travel using the specified route
- /// </summary>
- public void StartTravel(TravelRoute route)
- {
- if (route == null || isTraveling)
- {
- Debug.LogWarning("Cannot start travel: invalid route or already traveling");
- return;
- }
- // Synchronize coordinates before starting travel
- SynchronizeWithExplorationSystem();
- // Reset cancellation flag and store current route
- shouldCancelJourney = false;
- currentRoute = route;
- // Show only the selected route with transparency for better marker visibility
- ShowTravelRouteOnly(route);
- // Start the travel coroutine and store reference
- currentTravelCoroutine = StartCoroutine(ExecuteTravel(route));
- }
- /// <summary>
- /// Cancels the current journey if one is in progress
- /// </summary>
- public void CancelJourney()
- {
- if (!isTraveling)
- {
- if (showDebugLogs) Debug.Log("No journey to cancel");
- return;
- }
- if (showDebugLogs)
- {
- Debug.Log("Journey cancellation requested by player");
- }
- // Set the cancellation flag
- shouldCancelJourney = true;
- // The actual cleanup will happen in the ExecuteTravel coroutine
- }
- /// <summary>
- /// Executes the actual travel along the route
- /// </summary>
- private IEnumerator ExecuteTravel(TravelRoute route)
- {
- isTraveling = true;
- if (showDebugLogs)
- {
- Debug.Log($"🚶 Starting travel along {route.routeType} route");
- Debug.Log($"📍 Route has {route.path.Count} waypoints, estimated time: {route.estimatedTime:F1} hours");
- }
- // Ensure team marker is visible during travel (stop blinking)
- EnsureTeamMarkerVisible(true);
- // Debug: Check initial team marker state
- var initialMarker = teamPlacement.GetTeamMarker();
- if (initialMarker != null)
- {
- Debug.Log($"🔍 Initial team marker: Position={initialMarker.transform.position}, Active={initialMarker.activeInHierarchy}, WorldPos={initialMarker.transform.position}");
- var initialRenderer = initialMarker.GetComponent<Renderer>();
- if (initialRenderer != null)
- {
- Debug.Log($"🔍 Initial renderer: Enabled={initialRenderer.enabled}, Material={initialRenderer.material?.name}");
- }
- }
- else
- {
- Debug.LogError("❌ No team marker found at start of travel!");
- }
- // Deduct special costs before travel
- DeductTravelCosts(route);
- // Move team marker along the path with smooth animation
- for (int i = 1; i < route.path.Count; i++)
- {
- // Check for cancellation at the start of each step
- if (shouldCancelJourney)
- {
- if (showDebugLogs)
- {
- Debug.Log($"🛑 Journey cancelled at step {i}/{route.path.Count - 1}. Stopping at {route.path[i - 1]}");
- }
- break; // Exit the movement loop
- }
- Vector2Int nextPosition = route.path[i];
- Vector2Int currentPos = route.path[i - 1];
- if (showDebugLogs && i % 3 == 0) // Log every 3rd step
- {
- Debug.Log($"🚶 Traveling step {i}/{route.path.Count - 1} to {nextPosition} ({((float)i / (route.path.Count - 1) * 100):F0}% complete)");
- }
- // Get current and target world positions for smooth movement
- Vector3 currentWorldPos = TileToWorldPosition(currentPos);
- Vector3 targetWorldPos = TileToWorldPosition(nextPosition);
- // Animate the team marker moving smoothly between positions
- float moveTime = 0.8f; // Base time per step (slower movement)
- float stepCost = GetMovementCost(currentPos, nextPosition, route.routeType);
- // Adjust move time based on terrain difficulty
- moveTime *= Mathf.Clamp(stepCost * 0.3f, 0.5f, 3f); // Slower for difficult terrain
- // Smooth movement animation
- float elapsed = 0f;
- while (elapsed < moveTime)
- {
- // Check for cancellation during movement animation
- if (shouldCancelJourney)
- {
- if (showDebugLogs)
- {
- Debug.Log($"🛑 Journey cancelled during movement animation. Stopping at current position.");
- }
- break; // Exit the animation loop
- }
- elapsed += Time.deltaTime;
- float t = Mathf.SmoothStep(0f, 1f, elapsed / moveTime);
- // Interpolate between current and target position
- Vector3 lerpedWorldPos = Vector3.Lerp(currentWorldPos, targetWorldPos, t);
- Vector2Int lerpedTilePos = WorldToTilePosition(lerpedWorldPos);
- // Update team position smoothly (but only if it's a valid position)
- if (mapMaker.GetMapData().IsValidPosition(lerpedTilePos.x, lerpedTilePos.y))
- {
- teamPlacement.PlaceTeamAt(lerpedTilePos);
- // Ensure marker stays visible during travel
- var teamMarker = teamPlacement.GetTeamMarker();
- if (teamMarker != null)
- {
- teamMarker.SetActive(true);
- var renderer = teamMarker.GetComponent<Renderer>();
- if (renderer != null && !renderer.enabled)
- {
- renderer.enabled = true;
- if (showDebugLogs)
- {
- Debug.Log($"🔧 Re-enabled team marker renderer at {teamMarker.transform.position}");
- }
- }
- }
- }
- yield return null; // Wait one frame
- }
- // Break out of the inner animation loop if cancelled
- if (shouldCancelJourney)
- {
- break; // Exit the for loop as well
- }
- // Ensure we end up exactly at the target position (if not cancelled)
- if (!shouldCancelJourney)
- {
- teamPlacement.PlaceTeamAt(nextPosition);
- }
- }
- // Journey cleanup - whether completed or cancelled
- isTraveling = false;
- ResetTimeSpeed(); // Reset time scale to normal when travel ends
- EnsureTeamMarkerVisible(false); // Restore blinking
- ClearPathVisualization();
- HideTravelUI();
- // Synchronize coordinate systems after travel
- SynchronizeWithExplorationSystem();
- // Clear travel state
- currentTravelCoroutine = null;
- currentRoute = null;
- if (shouldCancelJourney)
- {
- if (showDebugLogs)
- {
- Debug.Log($"🛑 Journey cancelled! Team stopped at {teamPlacement.GetTeamPosition()}");
- }
- shouldCancelJourney = false; // Reset the flag
- }
- else
- {
- if (showDebugLogs)
- {
- Debug.Log($"✅ Travel completed! Arrived at {route.path.Last()}");
- }
- }
- }
- /// <summary>
- /// Synchronize travel system coordinates with exploration system
- /// </summary>
- private void SynchronizeWithExplorationSystem()
- {
- if (mapMaker != null && mapMaker.useExplorationSystem)
- {
- mapMaker.SyncTeamPosition();
- if (showDebugLogs)
- {
- Debug.Log("🔄 Synchronized team position with exploration system");
- }
- }
- }
- /// <summary>
- /// Deducts costs for travel (gold, items, etc.)
- /// </summary>
- private void DeductTravelCosts(TravelRoute route)
- {
- if (route.specialCosts.Count == 0) return;
- if (showDebugLogs)
- {
- Debug.Log($"💰 Deducting travel costs: {string.Join(", ", route.specialCosts.Select(kvp => $"{kvp.Key}: {kvp.Value}"))}");
- }
- // TODO: Implement actual resource deduction
- // This would integrate with your inventory/currency system
- foreach (var cost in route.specialCosts)
- {
- switch (cost.Key)
- {
- case "Ferry":
- // DeductGold(cost.Value);
- Debug.Log($"💰 Would deduct {cost.Value} gold for ferry");
- break;
- case "Tunnel Torch":
- // DeductTorches(cost.Value);
- Debug.Log($"🔥 Would deduct {cost.Value} torches for tunnel");
- break;
- }
- }
- }
- /// <summary>
- /// Ensures the team marker is visible during travel by controlling its blinking behavior
- /// </summary>
- private void EnsureTeamMarkerVisible(bool forceVisible)
- {
- if (teamPlacement == null)
- {
- Debug.LogWarning("❌ TeamPlacement is null in EnsureTeamMarkerVisible");
- return;
- }
- var teamMarker = teamPlacement.GetTeamMarker();
- if (teamMarker == null)
- {
- Debug.LogWarning("❌ Team marker is null - creating new marker");
- // Force creation of a new marker if it doesn't exist
- teamPlacement.PlaceTeamAt(teamPlacement.GetTeamPosition());
- teamMarker = teamPlacement.GetTeamMarker();
- if (teamMarker == null)
- {
- Debug.LogError("❌ Failed to create team marker!");
- return;
- }
- }
- var renderer = teamMarker.GetComponent<Renderer>();
- if (renderer == null)
- {
- Debug.LogWarning("❌ Team marker has no Renderer component");
- return;
- }
- Debug.Log($"🔧 Team marker status: Position={teamMarker.transform.position}, Active={teamMarker.activeInHierarchy}, RendererEnabled={renderer.enabled}");
- if (forceVisible)
- {
- // Force marker to be visible and stop blinking during travel
- teamMarker.SetActive(true);
- renderer.enabled = true;
- // Make the marker more visible by adjusting its properties
- teamMarker.transform.localScale = Vector3.one * 2f; // Make it bigger
- // Try to change material color to bright red for visibility
- var material = renderer.material;
- if (material != null)
- {
- material.color = Color.red;
- if (material.HasProperty("_BaseColor"))
- material.SetColor("_BaseColor", Color.red);
- if (material.HasProperty("_Color"))
- material.SetColor("_Color", Color.red);
- Debug.Log($"🔴 Set team marker to bright red for visibility");
- }
- // Force position to be visible above map (adjust Y for top-down view)
- var pos = teamMarker.transform.position;
- teamMarker.transform.position = new Vector3(pos.x, 2f, pos.z); // Raise above map for visibility
- // Stop any blinking coroutines in SimpleTeamPlacement using reflection
- try
- {
- var placementType = teamPlacement.GetType();
- var stopCooroutinesMethod = placementType.GetMethod("StopAllCoroutines");
- if (stopCooroutinesMethod != null)
- {
- stopCooroutinesMethod.Invoke(teamPlacement, null);
- Debug.Log("✅ Stopped blinking coroutines");
- }
- // Set isBlinking to false using reflection
- var isBlinkingField = placementType.GetField("isBlinking", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
- if (isBlinkingField != null)
- {
- isBlinkingField.SetValue(teamPlacement, false);
- Debug.Log("✅ Set isBlinking to false");
- }
- }
- catch (System.Exception e)
- {
- Debug.LogWarning($"⚠️ Reflection failed: {e.Message}");
- }
- if (showDebugLogs)
- {
- Debug.Log($"👁️ Team marker forced visible for travel at {teamMarker.transform.position}");
- }
- }
- else
- {
- // Restore normal blinking behavior after travel
- try
- {
- var placementType = teamPlacement.GetType();
- var enableBlinkingField = placementType.GetField("enableBlinking", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
- if (enableBlinkingField != null && (bool)enableBlinkingField.GetValue(teamPlacement))
- {
- // Restart blinking using reflection
- var startBlinkingMethod = placementType.GetMethod("StartBlinking", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
- if (startBlinkingMethod != null)
- {
- startBlinkingMethod.Invoke(teamPlacement, null);
- if (showDebugLogs)
- {
- Debug.Log("✨ Team marker blinking restored after travel");
- }
- }
- }
- }
- catch (System.Exception e)
- {
- Debug.LogWarning($"⚠️ Reflection failed during restore: {e.Message}");
- }
- }
- }
- /// <summary>
- /// Utility methods for coordinate conversion
- /// </summary>
- private Vector3 GetMouseWorldPosition()
- {
- if (mapCamera == null)
- {
- if (showDebugLogs) Debug.LogWarning("❌ Map camera is null, trying to find Camera.main");
- mapCamera = Camera.main;
- if (mapCamera == null) return Vector3.zero;
- }
- Ray ray = mapCamera.ScreenPointToRay(Input.mousePosition);
- if (showDebugLogs) Debug.Log($"📐 Raycast from camera: {ray.origin} direction: {ray.direction}");
- // Try raycast first (if there are colliders)
- if (Physics.Raycast(ray, out RaycastHit hit, 1000f))
- {
- if (showDebugLogs) Debug.Log($"🎯 Raycast hit: {hit.point} on {hit.collider.name}");
- return hit.point;
- }
- // Fallback: use camera plane projection at Y=0 (map level) for top-down view
- Plane mapPlane = new Plane(Vector3.up, Vector3.zero); // Y=0 plane facing up for top-down camera
- if (mapPlane.Raycast(ray, out float distance))
- {
- Vector3 point = ray.GetPoint(distance);
- if (showDebugLogs) Debug.Log($"📍 Plane intersection: {point}");
- return point;
- }
- if (showDebugLogs) Debug.LogWarning("❌ No raycast hit and no plane intersection");
- return Vector3.zero;
- }
- private Vector2Int WorldToTilePosition(Vector3 worldPos)
- {
- float tileSize = mapMaker?.mapVisualizer?.tileSize ?? 1f;
- return new Vector2Int(
- Mathf.RoundToInt(worldPos.x / tileSize),
- Mathf.RoundToInt(worldPos.z / tileSize) // Use Z coordinate for map Y in top-down view
- );
- }
- private Vector3 TileToWorldPosition(Vector2Int tilePos)
- {
- float tileSize = mapMaker?.mapVisualizer?.tileSize ?? 1f;
- return new Vector3(
- tilePos.x * tileSize,
- 0f, // Y=0 for tiles at ground level
- tilePos.y * tileSize // Map Y coordinate becomes world Z coordinate for top-down view
- );
- }
- /// <summary>
- /// Debug method to show movement cost information
- /// </summary>
- private void ShowMovementCostInfo()
- {
- Debug.Log("=== MOVEMENT COST INFO ===");
- Debug.Log("TERRAIN COSTS:");
- Debug.Log($" Plains: {plainsMovementCost}x");
- Debug.Log($" Forest: {forestMovementCost}x");
- Debug.Log($" Mountain: {mountainMovementCost}x");
- Debug.Log($" River Crossing: {riverCrossingCost}x (slow crossing)");
- Debug.Log($" Lake Swimming: {lakeCrossingCost}x (swimming)");
- Debug.Log($" Ocean Crossing: {oceanCrossingCost}x (very dangerous)");
- Debug.Log("FEATURE COSTS:");
- Debug.Log($" Road: {roadMovementCost}x");
- Debug.Log($" Town/Village: {townMovementCost}x");
- Debug.Log($" Bridge: {bridgeMovementCost}x");
- Debug.Log($" Ferry: {ferryMovementCost}x (+ {ferryBaseCost} gold)");
- Debug.Log($" Tunnel (with torch): {tunnelWithTorchCost}x (+ {tunnelTorchCost} torches)");
- Debug.Log($" Tunnel (without torch): {tunnelWithoutTorchCost}x");
- Debug.Log("CONTROLS:");
- Debug.Log(" TAB: Cycle through route options");
- Debug.Log(" ESC: Cancel travel planning (or cancel journey if traveling)");
- Debug.Log(" C: Cancel journey while traveling");
- Debug.Log(" F8: Toggle debug logs");
- Debug.Log("TRAVEL CONTROLS (while traveling):");
- Debug.Log(" >,→: Increase travel speed");
- Debug.Log(" <,←: Decrease travel speed");
- Debug.Log(" Space: Reset travel speed to normal");
- Debug.Log("========================");
- }
- /// <summary>
- /// Public getters for external systems
- /// </summary>
- public List<TravelRoute> GetAvailableRoutes() => availableRoutes;
- public Vector2Int? GetPlannedDestination() => plannedDestination;
- public bool IsTraveling() => isTraveling;
- public bool CanCancelJourney() => isTraveling && currentTravelCoroutine != null;
- public TravelRoute GetCurrentRoute() => currentRoute;
- /// <summary>
- /// Increases time speed for faster travel
- /// </summary>
- public void IncreaseTimeSpeed()
- {
- if (currentTimeSpeedIndex < timeSpeedOptions.Length - 1)
- {
- currentTimeSpeedIndex++;
- timeMultiplier = timeSpeedOptions[currentTimeSpeedIndex];
- Time.timeScale = timeMultiplier;
- Debug.Log($"⏩ Time speed increased to {timeMultiplier}x");
- }
- }
- /// <summary>
- /// Decreases time speed
- /// </summary>
- public void DecreaseTimeSpeed()
- {
- if (currentTimeSpeedIndex > 0)
- {
- currentTimeSpeedIndex--;
- timeMultiplier = timeSpeedOptions[currentTimeSpeedIndex];
- Time.timeScale = timeMultiplier;
- Debug.Log($"⏪ Time speed decreased to {timeMultiplier}x");
- }
- }
- /// <summary>
- /// Resets time speed to normal
- /// </summary>
- public void ResetTimeSpeed()
- {
- currentTimeSpeedIndex = 0;
- timeMultiplier = timeSpeedOptions[0];
- Time.timeScale = timeMultiplier;
- Debug.Log($"⏸️ Time speed reset to {timeMultiplier}x");
- }
- /// <summary>
- /// Gets current time speed multiplier
- /// </summary>
- public float GetTimeSpeed() => timeMultiplier;
- /// <summary>
- /// Methods for UI integration
- /// </summary>
- private bool IsTravelUIBlocking()
- {
- // Check both travel UI and combat popup for blocking input
- bool travelUIBlocking = travelUI != null && travelUI.IsUIBlocking();
- bool combatPopupBlocking = combatEventPopup != null && combatEventPopup.IsVisible;
- return travelUIBlocking || combatPopupBlocking;
- }
- private void ShowTravelUIIfAvailable(Vector2Int destination, List<TravelRoute> routes)
- {
- // Direct call instead of reflection since TravelUI is now available
- if (travelUI != null)
- {
- travelUI.ShowTravelUI(destination, routes);
- }
- else
- {
- Debug.LogWarning("⚠️ TravelUI not found, cannot show travel interface");
- }
- }
- public void PlanTravelWithPreferences(Vector2Int destination, bool avoidFerries, bool avoidMountainPasses, bool preferRoads)
- {
- // Store preferences and recalculate routes
- if (showDebugLogs)
- {
- Debug.Log($"🔄 Recalculating routes with preferences: AvoidFerries={avoidFerries}, AvoidMountains={avoidMountainPasses}, PreferRoads={preferRoads}");
- }
- // For now, just recalculate the same routes
- // In a full implementation, these preferences would modify the pathfinding algorithm
- PlanTravelTo(destination);
- }
- /// <summary>
- /// Forces recalculation of routes and refreshes the UI - useful for debugging or preference changes
- /// </summary>
- [ContextMenu("DEBUG: Recalculate Routes")]
- public void ForceRecalculateRoutes()
- {
- if (plannedDestination.HasValue)
- {
- Debug.Log($"🔄 TeamTravelSystem: Force recalculating routes to {plannedDestination.Value}");
- Vector2Int currentPosition = teamPlacement.GetTeamPosition();
- // Recalculate routes
- CalculateRoutes(currentPosition, plannedDestination.Value);
- // Refresh visualization
- if (availableRoutes.Count > 0)
- {
- VisualizeAllRoutes(availableRoutes);
- }
- // Refresh UI if visible
- if (travelUI != null && travelUI.IsVisible)
- {
- Debug.Log("🔄 TeamTravelSystem: Refreshing UI with recalculated routes");
- travelUI.RefreshWithNewRoutes(availableRoutes);
- }
- Debug.Log($"✅ TeamTravelSystem: Routes recalculated - {availableRoutes.Count} routes available");
- }
- else
- {
- Debug.LogWarning("⚠️ TeamTravelSystem: No planned destination to recalculate routes for");
- }
- }
- public void VisualizeSpecificRoute(TravelRoute route)
- {
- var callId = System.Guid.NewGuid().ToString("N")[..8]; // 8-character unique ID
- if (showDebugLogs)
- {
- Debug.Log($"🎯 [{callId}] VisualizeSpecificRoute called with route: {route?.routeType}, availableRoutes count: {availableRoutes?.Count ?? 0}");
- }
- if (route == null)
- {
- Debug.LogError($"❌ [{callId}] Cannot visualize null route!");
- return;
- }
- // If availableRoutes is empty, this might be because routes were cleared during UI interaction
- // In this case, we can still visualize the single route that was passed to us
- if (availableRoutes == null || availableRoutes.Count == 0)
- {
- Debug.LogWarning($"⚠️ [{callId}] availableRoutes is empty, visualizing single route {route.routeType}");
- // Temporarily add this route to the list so the visualization system can work
- if (availableRoutes == null) availableRoutes = new List<TravelRoute>();
- availableRoutes.Add(route);
- // Visualize just this route
- ClearPathVisualization();
- RenderSingleRoute(route, 0, true); // Render as selected
- if (showDebugLogs)
- {
- Debug.Log($"🎨 [{callId}] Visualized single {route.routeType} route");
- }
- return;
- }
- // Normal path when we have available routes
- // Verify the route is actually in our available routes
- bool routeExists = availableRoutes.Contains(route);
- if (!routeExists)
- {
- Debug.LogWarning($"⚠️ [{callId}] Selected route {route.routeType} is not in availableRoutes list!");
- // Try to find a matching route by type
- var matchingRoute = availableRoutes.FirstOrDefault(r => r?.routeType == route.routeType);
- if (matchingRoute != null)
- {
- Debug.Log($"🔄 [{callId}] Found matching route by type, using that instead");
- route = matchingRoute;
- }
- else
- {
- Debug.LogError($"❌ [{callId}] No matching route found, cannot visualize!");
- return;
- }
- }
- // Instead of clearing all routes, just highlight the selected one
- HighlightSpecificRoute(route);
- if (showDebugLogs)
- {
- Debug.Log($"🎨 [{callId}] Highlighted {route.routeType} route while keeping all routes visible");
- }
- }
- /// <summary>
- /// Highlights a specific route while keeping all routes visible (selected route on top)
- /// </summary>
- private void HighlightSpecificRoute(TravelRoute selectedRoute)
- {
- if (showDebugLogs)
- {
- Debug.Log($"🔍 HighlightSpecificRoute called with {selectedRoute?.routeType}, availableRoutes count: {availableRoutes.Count}");
- for (int i = 0; i < availableRoutes.Count; i++)
- {
- Debug.Log($" Route {i}: {availableRoutes[i]?.routeType} with {availableRoutes[i]?.path?.Count ?? 0} waypoints");
- }
- }
- // Validate that we have routes to work with
- if (availableRoutes == null || availableRoutes.Count == 0)
- {
- Debug.LogError("❌ HighlightSpecificRoute: No available routes to render!");
- return;
- }
- if (selectedRoute == null)
- {
- Debug.LogError("❌ HighlightSpecificRoute: Selected route is null!");
- return;
- }
- // Clear and re-render all routes with proper transparency and layering
- ClearPathVisualization();
- // Create a copy of routes to avoid any modification issues
- var routesToRender = new List<TravelRoute>(availableRoutes);
- if (showDebugLogs)
- {
- Debug.Log($"🎨 About to render {routesToRender.Count} routes");
- }
- // First render non-selected routes with transparency
- for (int i = 0; i < routesToRender.Count; i++)
- {
- var route = routesToRender[i];
- if (route == null)
- {
- Debug.LogWarning($"⚠️ Route {i} is null, skipping");
- continue;
- }
- if (route.path == null || route.path.Count == 0)
- {
- Debug.LogWarning($"⚠️ Route {i} ({route.routeType}) has no path, skipping");
- continue;
- }
- bool isSelected = (route == selectedRoute);
- if (showDebugLogs)
- {
- Debug.Log($"🎨 Rendering route {i}: {route.routeType}, selected: {isSelected}, pathCount: {route.path.Count}");
- }
- // All routes are rendered, but with different emphasis
- RenderSingleRoute(route, i, isSelected);
- }
- if (showDebugLogs)
- {
- string routeName = selectedRoute?.routeType == RouteType.RoadPreferred ? "Fastest" :
- selectedRoute?.routeType == RouteType.Standard ? "Shortest" : "Cheapest";
- Debug.Log($"🔝 Highlighted {routeName} route - rendered {currentPathVisualizations.Count} route visualizations");
- }
- }
- /// <summary>
- /// Renders a single route with specified layering
- /// </summary>
- private void RenderSingleRoute(TravelRoute route, int layerIndex, bool isSelected)
- {
- if (route == null || route.path.Count == 0)
- {
- if (showDebugLogs)
- {
- Debug.LogWarning($"⚠️ RenderSingleRoute: Cannot render route - route is null: {route == null}, path count: {route?.path?.Count ?? 0}");
- }
- return;
- }
- if (showDebugLogs)
- {
- Debug.Log($"🎨 RenderSingleRoute: Creating visualization for {route.routeType}, selected: {isSelected}, layerIndex: {layerIndex}");
- }
- // Create path visualization
- var pathObject = new GameObject($"TravelPath_{route.routeType}_{(isSelected ? "Selected" : "Normal")}");
- var lineRenderer = pathObject.AddComponent<LineRenderer>();
- if (showDebugLogs)
- {
- Debug.Log($"🔧 Created GameObject: {pathObject.name} with LineRenderer");
- }
- // Configure line renderer with a new material instance
- var baseMaterial = pathLineMaterial ?? CreateDefaultPathMaterial();
- var routeMaterial = new Material(baseMaterial);
- var routeColor = GetRouteColor(route);
- // Selected route gets full opacity, non-selected get good visibility
- if (isSelected)
- {
- routeColor.a = 1.0f; // Full opacity for selected
- }
- else
- {
- routeColor.a = 0.6f; // Good visibility for non-selected (was 0.8f)
- }
- routeMaterial.color = routeColor;
- lineRenderer.material = routeMaterial;
- // Selected route gets thicker line, non-selected get thinner
- float lineWidth = 1.0f;
- int sortingOrder = 50;
- switch (route.routeType)
- {
- case RouteType.RoadPreferred: // Fastest
- lineWidth = isSelected ? 2.2f : 1.4f; // More dramatic difference
- sortingOrder = isSelected ? 60 : 53;
- break;
- case RouteType.Standard: // Shortest
- lineWidth = isSelected ? 2.0f : 1.2f; // More dramatic difference
- sortingOrder = isSelected ? 59 : 52;
- break;
- case RouteType.Special: // Cheapest
- lineWidth = isSelected ? 1.8f : 1.0f; // More dramatic difference
- sortingOrder = isSelected ? 58 : 51;
- break;
- }
- lineRenderer.startWidth = lineWidth;
- lineRenderer.endWidth = lineWidth;
- lineRenderer.positionCount = route.path.Count;
- lineRenderer.useWorldSpace = true;
- // Enhanced settings with proper layering
- lineRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
- lineRenderer.receiveShadows = false;
- lineRenderer.alignment = LineAlignment.View;
- lineRenderer.sortingLayerName = "Default";
- lineRenderer.sortingOrder = sortingOrder;
- lineRenderer.textureMode = LineTextureMode.Tile;
- // Selected route gets priority Y-offset (higher above map)
- float zOffset = pathLineHeight + (isSelected ? 1.0f : layerIndex * 0.2f);
- // Set path points
- for (int j = 0; j < route.path.Count; j++)
- {
- // Convert map coordinates to world coordinates properly for top-down view
- Vector3 worldPos = TileToWorldPosition(route.path[j]);
- // Add slight offset for non-selected routes to prevent exact overlap
- if (!isSelected)
- {
- float offsetAmount = layerIndex * 0.02f;
- worldPos.x += Mathf.Sin(j + layerIndex) * offsetAmount;
- worldPos.z += Mathf.Cos(j + layerIndex) * offsetAmount; // Use Z for map Y offset
- }
- worldPos.y = zOffset; // Use Y for height/depth positioning
- lineRenderer.SetPosition(j, worldPos);
- }
- currentPathVisualizations.Add(pathObject);
- if (showDebugLogs)
- {
- string routeName = route.routeType == RouteType.RoadPreferred ? "Fastest" :
- route.routeType == RouteType.Standard ? "Shortest" : "Cheapest";
- Debug.Log($"✅ Successfully rendered {routeName} route: selected={isSelected}, width={lineWidth}, sorting={sortingOrder}, z-offset={zOffset}");
- Debug.Log($"📊 Total visualizations now: {currentPathVisualizations.Count}");
- }
- }
- public void HideTravelUI()
- {
- // Direct call instead of reflection since TravelUI is now available
- if (travelUI != null)
- {
- travelUI.HideTravelPanel();
- }
- }
- }
- /// <summary>
- /// Represents different types of routes with GPS-style optimization goals
- /// </summary>
- public enum RouteType
- {
- Standard, // Shortest path (minimal distance/waypoints)
- RoadPreferred, // Fastest path (heavily prefers roads and fast terrain)
- Special // Cheapest/Direct path (avoids special costs, takes more direct routes)
- }
- /// <summary>
- /// Represents a complete travel route with all associated costs and information
- /// </summary>
- [System.Serializable]
- public class TravelRoute
- {
- public RouteType routeType;
- public List<Vector2Int> path;
- public float totalCost;
- public float totalDistance;
- public float estimatedTime;
- public Dictionary<string, int> specialCosts; // Special costs like ferry fees, torch usage
- }
- /// <summary>
- /// Node used for A* pathfinding
- /// </summary>
- public class PathNode
- {
- public Vector2Int position;
- public float gCost; // Distance from starting node
- public float hCost; // Heuristic distance to target
- public float fCost => gCost + hCost;
- public PathNode parent;
- }
|