// Assets/_Project/Scripts/Combat/TowerRangeIndicator.cs using UnityEngine; using UnityEngine.Rendering.Universal; using TD.Gameplay; namespace TD.Combat { /// /// Displays a translucent decal circle representing this tower's attack range /// when the local player selects the tower. /// /// /// Visual only. No networking — selection is a local UI concept. /// All clients independently show the range indicator for whatever tower /// they have selected. /// /// Prefab setup: /// /// Add this component to the tower prefab root (alongside TowerInstance). /// Add a child GameObject named "RangeIndicator". /// Add a DecalProjector to that child and assign it to /// (or leave it unassigned — auto-found /// via GetComponentInChildren in Start). /// Assign a translucent range-circle material to the DecalProjector. /// /// /// Sizing: The projector diameter is set once in Start from /// TowerDefinition.Range. Towers are static, so no per-frame resize is needed. /// /// Subscription timing: Follows the same deferred-subscribe pattern as /// — retries until /// is available, then stops polling. /// public class TowerRangeIndicator : MonoBehaviour { [Tooltip("DecalProjector used to render the range circle on the ground. " + "Auto-found via GetComponentInChildren if left empty.")] [SerializeField] private DecalProjector rangeProjector; [Tooltip("Vertical extent of the decal projection volume in world units. " + "Must be tall enough to project onto terrain at any height in your map.")] [SerializeField] private float projectionDepth = 50f; private TowerInstance towerInstance; private bool subscribed; // ----- Lifecycle --------------------------------------------------- private void Start() { towerInstance = GetComponent(); if (towerInstance == null) { Debug.LogError("[TowerRangeIndicator] No TowerInstance found on this " + "GameObject. TowerRangeIndicator must sit on the same " + "prefab root as TowerInstance."); enabled = false; return; } if (rangeProjector == null) rangeProjector = GetComponentInChildren(); if (rangeProjector == null) { Debug.LogError("[TowerRangeIndicator] No DecalProjector found. " + "Add one as a child of this GameObject and assign it " + "to the rangeProjector field."); enabled = false; return; } // TowerInstance resolves its Definition in OnNetworkSpawn, which runs // before Start, so Definition is guaranteed available here. float range = towerInstance.Definition != null ? towerInstance.Definition.Range : 0f; float diameter = range * 2f; rangeProjector.size = new Vector3(diameter, diameter, projectionDepth); rangeProjector.pivot = Vector3.zero; // DecalProjector projects along its local +Z axis; rotate 90° around X // to project downward onto the ground plane. rangeProjector.transform.localRotation = Quaternion.Euler(90f, 0f, 0f); rangeProjector.transform.localPosition = Vector3.zero; rangeProjector.enabled = false; TrySubscribe(); } private void OnDestroy() { Unsubscribe(); } private void Update() { // Retry subscription each frame until SelectionState is ready. // Once subscribed the branch short-circuits immediately. if (!subscribed) TrySubscribe(); } // ----- Subscription ----------------------------------------------- private void TrySubscribe() { if (subscribed) return; var sel = SelectionState.Instance; if (sel == null) return; sel.OnSelectionChanged += HandleSelectionChanged; subscribed = true; // Catch whatever is already selected before we subscribed. HandleSelectionChanged(sel.SelectedObject); } private void Unsubscribe() { if (!subscribed) return; var sel = SelectionState.Instance; if (sel != null) sel.OnSelectionChanged -= HandleSelectionChanged; subscribed = false; } // ----- Selection handler ------------------------------------------ private void HandleSelectionChanged(ISelectable newSelection) { if (rangeProjector == null) return; // Show only when THIS tower's TowerInstance is the selected object. rangeProjector.enabled = (object)newSelection == (object)towerInstance; } } }