using System; using System.Collections.Generic; using System.Linq; using Sirenix.OdinInspector; using UnityEngine; using UnityEngine.Rendering; using VOID.Generic.Dict; using VOID.Generic.Objects.Abstract.Events; using VOID.Generic.Objects.Unit; using VOID.Generic.Objects.Unit.Events; using VOID.Generic.Player.Input; using VOID.Generic.Player.Util; using VOID.UserInterface; using VOID.Generic.Util; namespace VOID.Generic.Player { [DefaultExecutionOrder(-1)] public class PathVisualizer : MonoBehaviour { public static PathVisualizer current { get; private set; } // Configuration [SerializeField] private float redrawTimer = 0.1f; [SerializeField] private float lineSize = 0.05f; [SerializeField] private float verticalOffset = 0.1f; [SerializeField, Required] private Material reachableMaterial; [SerializeField, Required] private Material unreachableMaterial; [SerializeField, Required] private Material dangerousMaterial; [SerializeField, Required] private Material neutralMaterial; // References private UnitObject _activeUnit; private readonly List _pathLineRenderers = new(); private bool _autoRedraw; private float _lastRedrawTimer; private void Awake() { current = this; } private void Start() { PlayerController.current.OnActiveUnitChange += OnActiveUnitChange; PlayerInputManager.current.OnForceMoveStart += ShouldAutoRedraw; PlayerInputManager.current.OnForceMoveEnd += ShouldAutoRedraw; } private void FixedUpdate() { if (_autoRedraw == false) return; if (GameUI.current.isCursorOverUI) { HidePath(); return; } _lastRedrawTimer += Time.fixedDeltaTime; if (_lastRedrawTimer < redrawTimer) return; _lastRedrawTimer = 0f; _activeUnit.unitNavAgent.CalculatePathTo(SelectUtil.SelectPosition()); UpdatePath(_activeUnit.unitNavAgent.GetCorrectedPath().ToArray()); } private void ShouldAutoRedraw() { // No player's active unit! if (!_activeUnit) { _autoRedraw = false; return; } // Unit is moving on drawn path - don't draw another one if (_activeUnit.unitState.isMoving) { _autoRedraw = false; return; } // Is something forcing path drawing? var isForcedMove = PlayerInputManager.current.isForceMove; var isActiveUnitTurn = _activeUnit.basicState.turnManager && _activeUnit.basicState.turnManager.currentUnitTurn == _activeUnit; if (!isForcedMove && !isActiveUnitTurn) { _autoRedraw = false; HidePath(); return; } // Is unit casting ability? if (_activeUnit.unitState.isCasting) { _autoRedraw = false; HidePath(); return; } _autoRedraw = true; } private void AddLineRenderers(int needed) { // Add missing LineRenderers to current poll for (var i = 0; i < needed - _pathLineRenderers.Count; i++) { var lineRendererGameObject = new GameObject($"Path Renderer - part {_pathLineRenderers.Count+1}"); lineRendererGameObject.transform.parent = gameObject.transform; var lineRenderer = lineRendererGameObject.AddComponent(); lineRenderer.enabled = false; lineRenderer.startWidth = lineSize; lineRenderer.endWidth = lineSize; lineRenderer.shadowCastingMode = ShadowCastingMode.Off; lineRenderer.numCapVertices = 3; lineRenderer.numCornerVertices = 3; _pathLineRenderers.Add(lineRenderer); } } private void UpdateLineRenderers(List pathTypes, List paths) { AddLineRenderers(paths.Count); for (var i = 0; i < paths.Count; i++) { var lineRenderer = _pathLineRenderers[i]; var pathType = pathTypes[i]; var path = paths[i]; var material = pathType switch { PathType.Reachable => reachableMaterial, PathType.UnReachable => unreachableMaterial, PathType.Dangerous => dangerousMaterial, PathType.Neutral => neutralMaterial, _ => throw new ArgumentOutOfRangeException(nameof(pathType), pathType, null) }; var offsetPath = path.Select(point => point + new Vector3(0, verticalOffset, 0)).ToArray(); lineRenderer.positionCount = path.Length; lineRenderer.SetPositions(offsetPath); lineRenderer.sharedMaterial = material; lineRenderer.enabled = true; } } private void OnActiveUnitChange(UnitObject unitObject) { if (_activeUnit) { _activeUnit.unitEvents.onMoveStart.RemoveListener(OnUnitMoveStart); _activeUnit.unitEvents.onMoveEnd.RemoveListener(OnUnitMoveEnd); _activeUnit.basicEvents.onTurnSelfStart.RemoveListener(OnTurnSelfStart); _activeUnit.basicEvents.onTurnSelfEnd.RemoveListener(OnTurnSelfEnd); _activeUnit.basicEvents.onTurnQueueEnter.RemoveListener(OnTurnQueueEnter); _activeUnit.basicEvents.onTurnQueueLeave.RemoveListener(OnTurnQueueLeave); _activeUnit.unitEvents.onStartCasting.RemoveListener(OnStartCasting); _activeUnit.unitEvents.onEndCasting.RemoveListener(OnEndCasting); } HidePath(); ShouldAutoRedraw(); _activeUnit = unitObject; _activeUnit.unitEvents.onMoveStart.AddListener(OnUnitMoveStart); _activeUnit.unitEvents.onMoveEnd.AddListener(OnUnitMoveEnd); _activeUnit.basicEvents.onTurnSelfStart.AddListener(OnTurnSelfStart); _activeUnit.basicEvents.onTurnSelfEnd.AddListener(OnTurnSelfEnd); _activeUnit.basicEvents.onTurnQueueEnter.AddListener(OnTurnQueueEnter); _activeUnit.basicEvents.onTurnQueueLeave.AddListener(OnTurnQueueLeave); _activeUnit.unitEvents.onStartCasting.AddListener(OnStartCasting); _activeUnit.unitEvents.onEndCasting.AddListener(OnEndCasting); } private void OnUnitMoveStart(MoveEvent moveEvent) { _autoRedraw = false; UpdatePath(moveEvent.pathCorrected.path.ToArray()); } private void OnUnitMoveEnd(MoveEvent moveEvent) { HidePath(); ShouldAutoRedraw(); } private void OnTurnSelfStart(TurnEvent turnEvent) { ShouldAutoRedraw(); } private void OnTurnSelfEnd(TurnEvent turnEvent) { ShouldAutoRedraw(); } private void OnTurnQueueEnter(TurnEvent turnEvent) { ShouldAutoRedraw(); } private void OnTurnQueueLeave(TurnEvent turnEvent) { ShouldAutoRedraw(); } private void OnStartCasting(CastingEvent castingEvent) { ShouldAutoRedraw(); } private void OnEndCasting(CastingEvent castingEvent) { ShouldAutoRedraw(); } private void UpdatePath(Vector3[] path) { HidePath(); Vector3[] reachablePath; Vector3[] unreachablePath; if (_activeUnit.basicState.turnManager) { // Inside combat - some part of path can be unreachable (not enough move points) var movePoints = _activeUnit.unitData.currentResources.movePoints; NavMeshPathUtil.SplitPath(path, movePoints, out reachablePath, out unreachablePath); } else { // Outside combat - whole path is reachable reachablePath = path; unreachablePath = null; } NavMeshPathUtil.SplitReachablePath(reachablePath, out var pathTypes, out var paths); if (unreachablePath is not null) { pathTypes.Add(PathType.UnReachable); paths.Add(unreachablePath); } UpdateLineRenderers(pathTypes, paths); } private void HidePath() { _pathLineRenderers.ForEach(lineRenderer => lineRenderer.enabled = false); } } }