AStarHeightmapGrid / Assets / PlatypusIdeas / AirPath / Runtime / Modes / PathfindingModeBase.cs
PathfindingModeBase.cs
Raw
using System;
using System.Collections.Generic;
using UnityEngine;

namespace PlatypusIdeas.AirPath.Runtime.Modes {
    
    public abstract class PathfindingModeBase : IPathfindingMode {
        protected PathfindingModeContext Context;
        protected bool _isActive;
        protected List<Vector2Int> _coloredCells = new();
        
        public event Action<PathRequest> OnPathRequested;
        public event Action OnClearPath;
        
        public abstract string ModeName { get; }
        public bool IsActive => _isActive;
        
        protected readonly Color StartColor = Color.green;
        protected readonly Color EndColor = Color.red;
        protected readonly Color WarningColor = new(1f, 0.5f, 0f); 
        
        public void Initialize(PathfindingModeContext context) {
            Context = context;
            ValidateContext();
        }
        
        public virtual void OnActivate() {
            _isActive = true;
            ClearVisualization();
        }
        
        public virtual void OnDeactivate() {
            _isActive = false;
            ClearVisualization();
        }
        
        public abstract void UpdateMode();
        
        public virtual void Cleanup() {
            ClearVisualization();
            _coloredCells.Clear();
        }
        
        public abstract void DrawDebugVisualization();
        public abstract string GetStatusInfo();
        
        // Protected methods for event invocation (allows derived classes to trigger events)
        
        /// <summary>
        /// Invokes the OnPathRequested event - accessible to derived classes
        /// </summary>
        protected void InvokePathRequest(PathRequest request) {
            OnPathRequested?.Invoke(request);
        }
        
        /// <summary>
        /// Invokes the OnClearPath event - accessible to derived classes
        /// </summary>
        protected void InvokeClearPath() {
            OnClearPath?.Invoke();
        }
        
        // Enhanced helper methods with boundary safety
        
        /// <summary>
        /// Request path with automatic boundary clamping
        /// </summary>
        protected void RequestPath(Vector2Int start, Vector2Int end) {
            // Clamp positions to valid grid bounds
            var clampedStart = ClampToValidGridPosition(start);
            var clampedEnd = ClampToValidGridPosition(end);
            
            // Log if positions were clamped
            if (clampedStart != start || clampedEnd != end) {
                LogBoundaryClamp(start, clampedStart, "start");
                LogBoundaryClamp(end, clampedEnd, "end");
            }
            
            var request = new PathRequest(clampedStart, clampedEnd, Context.BirdHeightOffset);
            InvokePathRequest(request);
        }
        
        /// <summary>
        /// Request path with boundary checking and optional warning visualization
        /// </summary>
        protected void RequestPathSafe(Vector2Int start, Vector2Int end, bool visualizeWarning = true) {
            bool startWasClamped = false;
            bool endWasClamped = false;
            
            var clampedStart = start;
            var clampedEnd = end;
            
            if (!IsValidGridPosition(start)) {
                clampedStart = ClampToValidGridPosition(start);
                startWasClamped = true;
                
                if (visualizeWarning) {
                    SetCellColor(clampedStart, WarningColor);
                }
            }
            
            if (!IsValidGridPosition(end)) {
                clampedEnd = ClampToValidGridPosition(end);
                endWasClamped = true;
                
                if (visualizeWarning) {
                    SetCellColor(clampedEnd, WarningColor);
                }
            }
            
            if (startWasClamped || endWasClamped) {
                var warningMsg = $"Path positions were clamped to grid bounds: ";
                if (startWasClamped) warningMsg += $"Start ({start}→{clampedStart}) ";
                if (endWasClamped) warningMsg += $"End ({end}→{clampedEnd})";
                
                Debug.LogWarning($"[{ModeName}] {warningMsg}");
                UpdateInstructionText("Warning: Position was outside grid bounds and was clamped!");
            }
            
            var request = new PathRequest(clampedStart, clampedEnd, Context.BirdHeightOffset);
            InvokePathRequest(request);
        }
        
        protected void ClearCurrentPath() {
            InvokeClearPath();
        }
        
        /// <summary>
        /// Convert world position to grid position with automatic clamping
        /// </summary>
        protected Vector2Int WorldToGridPosition(Vector3 worldPos) {
            return Context.PathfindingService.WorldToGridPosition(worldPos);
        }
        
        /// <summary>
        /// Convert world position to grid position with out-of-bounds check
        /// </summary>
        protected Vector2Int WorldToGridPositionSafe(Vector3 worldPos, out bool wasOutOfBounds) {
            return Context.PathfindingService.WorldToGridPositionSafe(worldPos, out wasOutOfBounds);
        }
        
        protected Vector3 GridToWorldPosition(Vector2Int gridPos, float yOffset = 0) {
            return Context.PathfindingService.GridToWorldPosition(gridPos, yOffset);
        }
        
        protected bool IsValidGridPosition(Vector2Int pos) {
            return Context.PathfindingService.IsValidGridPosition(pos);
        }
        
        
        /// <summary>
        /// Clamp a grid position to valid bounds
        /// </summary>
        protected Vector2Int ClampToValidGridPosition(Vector2Int pos) {
            return Context.PathfindingService.ClampToValidGridPosition(pos);
        }
        
        protected void SetCellColor(Vector2Int gridPos, Color color) {
            // Ensure position is valid before setting color
            var clampedPos = ClampToValidGridPosition(gridPos);

            if (!Context.TerrainController) return;
            Context.TerrainController.SetColor(clampedPos, color);
            if (!_coloredCells.Contains(clampedPos)) {
                _coloredCells.Add(clampedPos);
            }
        }
        
        protected void ClearVisualization() {
            if (Context?.TerrainController && Application.isPlaying) {
                foreach (var cell in _coloredCells) {
                    try {
                        Context.TerrainController.ResetCellColor(cell);
                    } catch (MissingReferenceException) {
                    }
                }
            }
            _coloredCells.Clear();
        }
        
        protected void UpdateInstructionText(string text) {
            Context?.UpdateInstructionText?.Invoke(text);
        }
        
        protected Vector3 GetAverageBirdPosition() {
            Debug.Log("<color=yellow>[PathfindingModeBase] GetAverageBirdPosition called</color>");
            Debug.Log($"<color=yellow>[PathfindingModeBase] Context is null? {Context == null}</color>");
    
            if (Context != null) {
                Debug.Log($"<color=yellow>[PathfindingModeBase] Context.GetAverageBirdPosition is null? {Context.GetAverageBirdPosition == null}</color>");
        
                if (Context.GetAverageBirdPosition != null) {
                    Debug.Log("<color=yellow>[PathfindingModeBase] About to invoke callback...</color>");
                    var result = Context.GetAverageBirdPosition.Invoke();
                    Debug.Log($"<color=yellow>[PathfindingModeBase] Callback returned: {result}</color>");
                    return result;
                }
            }
    
            Debug.LogWarning("<color=red>[PathfindingModeBase] Returning Vector3.zero (fallback)</color>");
            return Vector3.zero;
        }
        
        protected bool IsPathActive() {
            return Context?.IsPathActive?.Invoke() ?? false;
        }
        
        private void ValidateContext() {
            if (Context == null) {
                throw new InvalidOperationException($"[{ModeName}] Context is null");
            }
            
            if (!Context.IsValid()) {
                var missingComponents = new List<string>();
                if (!Context.MainCamera) missingComponents.Add("MainCamera");
                if (!Context.TerrainController) missingComponents.Add("TerrainInfo");
                if (!Context.PathfindingService) missingComponents.Add("PathfindingService");
                
                var errorDetails = missingComponents.Count > 0 
                    ? $"Missing: {string.Join(", ", missingComponents)}" 
                    : "Unknown validation error";
                    
                throw new InvalidOperationException($"[{ModeName}] Context is invalid - {errorDetails}");
            }
        }
        
        /// <summary>
        /// Helper for ray casting from camera with boundary check
        /// </summary>
        protected bool GetTerrainHitFromScreenPoint(Vector2 screenPoint, out RaycastHit hit) {
            if (!Context.MainCamera) {
                hit = default;
                return false;
            }
            
            var ray = Context.MainCamera.ScreenPointToRay(screenPoint);

            if (!Physics.Raycast(ray, out hit)) return false;
            WorldToGridPositionSafe(hit.point, out bool wasOutOfBounds);
                
            if (wasOutOfBounds && Context.ShowClampWarnings) {
                Debug.Log($"[{ModeName}] Click was outside grid bounds. Position will be clamped.");
            }
                
            return true;

        }
        
        /// <summary>
        /// Log boundary clamping for debugging
        /// </summary>
        private void LogBoundaryClamp(Vector2Int original, Vector2Int clamped, string positionName) {
            if (original != clamped && Context.ShowClampWarnings) {
                Debug.Log($"[{ModeName}] {positionName} position clamped from {original} to {clamped}");
            }
        }
    }
}