using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
///
/// New comprehensive travel system that handles movement costs, pathfinding, and visual planning.
/// Integrates with SimpleTeamPlacement and MapMaker2 for seamless map-based travel.
///
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 pathLineZOffset = -2f; // Moved further forward to appear above roads
[Header("Debug")]
public bool showDebugLogs = false;
public bool showMovementCosts = false;
// References
private SimpleTeamPlacement teamPlacement;
private MapMaker2 mapMaker;
private Camera mapCamera;
private TravelUI travelUI;
// Travel state
private Vector2Int? plannedDestination;
private List availableRoutes = new List();
private List currentPathVisualizations = new List(); // 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();
mapMaker = FindFirstObjectByType();
mapCamera = Camera.main;
travelUI = FindFirstObjectByType();
// 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
}
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")}");
}
}
///
/// Plans travel to the specified destination and shows available routes
///
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");
}
}
}
///
/// Calculates different route options including fastest, shortest, and cheapest routes
///
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}");
}
}
///
/// Finds the best path between two points using A* pathfinding with different route preferences
///
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();
var closedSet = new HashSet();
var allNodes = new Dictionary();
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
}
///
/// Gets movement cost for moving from one tile to another, considering route type preferences
///
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;
}
///
/// Gets base movement cost for different terrain types
///
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;
}
}
///
/// Gets movement cost modifications from features (roads, bridges, etc.)
///
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
}
}
///
/// Calculates heuristic distance for A* pathfinding with route type considerations
///
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;
}
///
/// Gets valid neighboring tiles for pathfinding
///
private List GetNeighbors(Vector2Int position)
{
var neighbors = new List();
// 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;
}
///
/// Reconstructs the final route from pathfinding results
///
private TravelRoute ReconstructRoute(PathNode destinationNode, RouteType routeType)
{
var route = new TravelRoute
{
routeType = routeType,
path = new List(),
totalCost = destinationNode.gCost,
totalDistance = 0f,
estimatedTime = 0f,
specialCosts = new Dictionary()
};
var current = destinationNode;
while (current != null)
{
route.path.Insert(0, current.position);
current = current.parent;
}
// Calculate additional route information
CalculateRouteDetails(route);
return route;
}
///
/// Finds a more direct path that ignores some terrain penalties for route variety
///
private TravelRoute FindDirectPath(Vector2Int start, Vector2Int destination)
{
var mapData = mapMaker.GetMapData();
Debug.Log($"π― Finding direct path from {start} to {destination}");
var openSet = new List();
var closedSet = new HashSet();
var allNodes = new Dictionary();
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;
}
///
/// Simplified movement cost calculation for direct routes
///
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;
}
}
///
/// 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
///
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);
}
///
/// Gets the resource cost for entering a specific tile
///
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
}
}
///
/// Calculates detailed information about the route (distance, time, special costs)
///
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;
}
}
///
/// Calculates the total resource cost (gold equivalent) for a route
///
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;
}
///
/// Checks if two routes are essentially the same
///
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;
}
///
/// Analyzes and logs the composition of a route for debugging
///
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}");
}
///
/// Visualizes the selected route on the map
///
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();
// 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.z = pathLineZOffset; // Place in front of map
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}");
}
}
///
/// Visualizes all available routes with different colors and proper layering
///
private void VisualizeAllRoutes(List 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)");
}
} ///
/// Gets the appropriate color for a route based on its type (GPS-style)
///
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;
}
}
///
/// Creates a default material for path visualization
///
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;
}
///
/// Clears any existing path visualization
///
private void ClearPathVisualization()
{
foreach (var pathViz in currentPathVisualizations)
{
if (pathViz != null)
{
DestroyImmediate(pathViz);
}
}
currentPathVisualizations.Clear();
}
///
/// Cancels current travel planning
///
public void CancelTravelPlanning()
{
plannedDestination = null;
availableRoutes.Clear();
ClearPathVisualization();
HideTravelUI();
if (showDebugLogs)
{
Debug.Log("β Travel planning cancelled");
}
}
///
/// Starts travel using the specified route
///
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;
// Start the travel coroutine and store reference
currentTravelCoroutine = StartCoroutine(ExecuteTravel(route));
}
///
/// Cancels the current journey if one is in progress
///
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
}
///
/// Executes the actual travel along the route
///
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();
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();
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()}");
}
}
}
///
/// Synchronize travel system coordinates with exploration system
///
private void SynchronizeWithExplorationSystem()
{
if (mapMaker != null && mapMaker.useExplorationSystem)
{
mapMaker.SyncTeamPosition();
if (showDebugLogs)
{
Debug.Log("π Synchronized team position with exploration system");
}
}
}
///
/// Deducts costs for travel (gold, items, etc.)
///
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;
}
}
}
///
/// Ensures the team marker is visible during travel by controlling its blinking behavior
///
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();
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 in front of everything
var pos = teamMarker.transform.position;
teamMarker.transform.position = new Vector3(pos.x, pos.y, -5f);
// 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}");
}
}
}
///
/// Utility methods for coordinate conversion
///
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 Z=0 (map level)
Plane mapPlane = new Plane(Vector3.back, Vector3.zero); // Z=0 plane facing forward
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.y / tileSize)
);
}
private Vector3 TileToWorldPosition(Vector2Int tilePos)
{
float tileSize = mapMaker?.mapVisualizer?.tileSize ?? 1f;
return new Vector3(
tilePos.x * tileSize,
tilePos.y * tileSize,
0f
);
}
///
/// Debug method to show movement cost information
///
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("========================");
}
///
/// Public getters for external systems
///
public List GetAvailableRoutes() => availableRoutes;
public Vector2Int? GetPlannedDestination() => plannedDestination;
public bool IsTraveling() => isTraveling;
public bool CanCancelJourney() => isTraveling && currentTravelCoroutine != null;
public TravelRoute GetCurrentRoute() => currentRoute;
///
/// Increases time speed for faster travel
///
public void IncreaseTimeSpeed()
{
if (currentTimeSpeedIndex < timeSpeedOptions.Length - 1)
{
currentTimeSpeedIndex++;
timeMultiplier = timeSpeedOptions[currentTimeSpeedIndex];
Time.timeScale = timeMultiplier;
Debug.Log($"β© Time speed increased to {timeMultiplier}x");
}
}
///
/// Decreases time speed
///
public void DecreaseTimeSpeed()
{
if (currentTimeSpeedIndex > 0)
{
currentTimeSpeedIndex--;
timeMultiplier = timeSpeedOptions[currentTimeSpeedIndex];
Time.timeScale = timeMultiplier;
Debug.Log($"βͺ Time speed decreased to {timeMultiplier}x");
}
}
///
/// Resets time speed to normal
///
public void ResetTimeSpeed()
{
currentTimeSpeedIndex = 0;
timeMultiplier = timeSpeedOptions[0];
Time.timeScale = timeMultiplier;
Debug.Log($"βΈοΈ Time speed reset to {timeMultiplier}x");
}
///
/// Gets current time speed multiplier
///
public float GetTimeSpeed() => timeMultiplier;
///
/// Methods for UI integration
///
private bool IsTravelUIBlocking()
{
// Direct check instead of reflection since TravelUI is now available
return travelUI != null && travelUI.IsUIBlocking();
}
private void ShowTravelUIIfAvailable(Vector2Int destination, List 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);
}
///
/// Forces recalculation of routes and refreshes the UI - useful for debugging or preference changes
///
[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();
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");
}
}
///
/// Highlights a specific route while keeping all routes visible (selected route on top)
///
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(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");
}
}
///
/// Renders a single route with specified layering
///
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();
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 Z-offset (closer to camera)
float zOffset = pathLineZOffset - (isSelected ? 1.0f : layerIndex * 0.2f);
// Set path points
for (int j = 0; j < route.path.Count; j++)
{
Vector3 worldPos = new Vector3(route.path[j].x, route.path[j].y, 0);
// 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.y += Mathf.Cos(j + layerIndex) * offsetAmount;
}
worldPos.z = zOffset;
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();
}
}
}
///
/// Represents different types of routes with GPS-style optimization goals
///
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)
}
///
/// Represents a complete travel route with all associated costs and information
///
[System.Serializable]
public class TravelRoute
{
public RouteType routeType;
public List path;
public float totalCost;
public float totalDistance;
public float estimatedTime;
public Dictionary specialCosts; // Special costs like ferry fees, torch usage
}
///
/// Node used for A* pathfinding
///
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;
}