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 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 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(); combatEventPopup = 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 } // 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")}"); } } /// /// 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; // Calculate actual travel time based on real movement costs, not pathfinding costs float actualTravelTime = 0f; for (int i = 0; i < route.path.Count; i++) { var position = route.path[i]; var tile = mapData.GetTile(position.x, position.y); // Use actual movement speeds for time calculation float tileTime = GetActualMovementTime(tile); actualTravelTime += tileTime; } route.estimatedTime = actualTravelTime; // 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; } /// /// Gets the actual movement time for a tile (used for time calculations, not pathfinding) /// private float GetActualMovementTime(MapTile tile) { // Special features that completely replace terrain costs switch (tile.featureType) { case FeatureType.Ferry: // Ferry completely replaces terrain cost - you're not swimming, you're riding! return ferryMovementCost; // 0.7f - direct ferry speed case FeatureType.Tunnel: // Tunnel completely replaces terrain cost return tunnelWithTorchCost; // 0.8f - tunnel with torch speed case FeatureType.Road: // Roads significantly speed up movement but don't completely replace terrain return GetTerrainMovementCost(tile) * roadMovementCost; // e.g., 1.0f * 0.5f = 0.5f case FeatureType.Bridge: // Bridges provide moderate speedup over terrain return GetTerrainMovementCost(tile) * bridgeMovementCost; // e.g., 1.0f * 0.6f = 0.6f case FeatureType.Town: case FeatureType.Village: // Towns are very fast to move through return townMovementCost; // 0.1f - direct town speed default: // No special feature, use base terrain cost return GetTerrainMovementCost(tile); } } /// /// 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 = calculated from actual tile movement speeds"); // 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.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}"); } } /// /// 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(); } /// /// Shows only the selected route with transparency for better marker visibility during travel /// 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(); // 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"); } } /// /// 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; // Show only the selected route with transparency for better marker visibility ShowTravelRouteOnly(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 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}"); } } } /// /// 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 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 ); } /// /// 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() { // 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 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"); } } [ContextMenu("DEBUG: Analyze Route Times")] public void DebugAnalyzeRouteTimes() { Debug.Log("πŸ§ͺ Analyzing route time calculations..."); if (availableRoutes == null || availableRoutes.Count == 0) { Debug.LogWarning("⚠️ No routes available. Plan a route first!"); return; } foreach (var route in availableRoutes) { Debug.Log($"\nπŸ“Š === {route.routeType} Route Analysis ==="); Debug.Log($" Path length: {route.path.Count} tiles"); Debug.Log($" Pathfinding cost: {route.totalCost:F2}"); Debug.Log($" Estimated time: {route.estimatedTime:F2}h"); // Count different terrain and feature types int ferryTiles = 0, roadTiles = 0, plainsTiles = 0, lakeTiles = 0, forestTiles = 0, tunnelTiles = 0; float ferryTime = 0f, roadTime = 0f, plainsTime = 0f, lakeTime = 0f, forestTime = 0f, tunnelTime = 0f; var mapData = mapMaker.GetMapData(); foreach (var pos in route.path) { var tile = mapData.GetTile(pos.x, pos.y); float tileTime = GetActualMovementTime(tile); if (tile.featureType == FeatureType.Ferry) { ferryTiles++; ferryTime += tileTime; } else if (tile.featureType == FeatureType.Tunnel) { tunnelTiles++; tunnelTime += tileTime; } else if (tile.featureType == FeatureType.Road) { roadTiles++; roadTime += tileTime; } else if (tile.terrainType == TerrainType.Plains) { plainsTiles++; plainsTime += tileTime; } else if (tile.terrainType == TerrainType.Lake) { lakeTiles++; lakeTime += tileTime; } else if (tile.terrainType == TerrainType.Forest) { forestTiles++; forestTime += tileTime; } } Debug.Log($" Time Breakdown:"); if (ferryTiles > 0) Debug.Log($" 🚒 Ferry: {ferryTiles} tiles = {ferryTime:F2}h"); if (tunnelTiles > 0) Debug.Log($" πŸ•³οΈ Tunnel: {tunnelTiles} tiles = {tunnelTime:F2}h"); if (roadTiles > 0) Debug.Log($" πŸ›£οΈ Roads: {roadTiles} tiles = {roadTime:F2}h"); if (plainsTiles > 0) Debug.Log($" 🌾 Plains: {plainsTiles} tiles = {plainsTime:F2}h"); if (lakeTiles > 0) Debug.Log($" 🏊 Lake swimming: {lakeTiles} tiles = {lakeTime:F2}h"); if (forestTiles > 0) Debug.Log($" 🌲 Forest: {forestTiles} tiles = {forestTime:F2}h"); if (route.specialCosts.Count > 0) { Debug.Log($" πŸ’° Special costs: {string.Join(", ", route.specialCosts.Select(c => $"{c.Key}: {c.Value}"))}"); } } Debug.Log("\nπŸ”§ Movement Speed Reference:"); Debug.Log($" Plains: {plainsMovementCost}f per tile"); Debug.Log($" Forest: {forestMovementCost}f per tile"); Debug.Log($" Mountains: {mountainMovementCost}f per tile"); Debug.Log($" Lake crossing (swimming): {lakeCrossingCost}f per tile"); Debug.Log($" Ferry: {ferryMovementCost}f per tile (should be much faster!)"); Debug.Log($" Tunnel (with torch): {tunnelWithTorchCost}f per tile"); Debug.Log($" Roads: {roadMovementCost}f per tile"); // Calculate how much faster ferry should be vs swimming float ferrySpeedAdvantage = lakeCrossingCost / ferryMovementCost; Debug.Log($" ⚑ Ferry is {ferrySpeedAdvantage:F1}x faster than swimming across lakes"); // Calculate how much faster tunnels are vs mountain climbing float tunnelSpeedAdvantage = mountainMovementCost / tunnelWithTorchCost; Debug.Log($" ⚑ Tunnels are {tunnelSpeedAdvantage:F1}x faster than climbing over mountains"); } 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 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(); } } } /// /// 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; }