// Assets/_Project/Scripts/Gameplay/BuildRangeIndicator.cs
using UnityEngine;
using UnityEngine.Rendering.Universal;
namespace TD.Gameplay
{
///
/// Visualizes a builder's build range as a decal projector circle on the ground.
/// Visible only to the owning client, and only while placement mode is active.
///
///
/// Sits as a child of the GameObject. The
/// renders a circular texture onto whatever ground
/// geometry is below — flat plane or sloped terrain alike, no special handling needed.
///
/// Owner-only. Non-owning clients should not see other players' build
/// range indicators (would be visual clutter). The decal projector is force-disabled
/// for non-owners on spawn.
///
/// Toggling. The indicator is only visible when the local player is in
/// placement mode. It checks TowerPlacementController.IsPlacing each frame
/// and toggles the projector accordingly. When sized correctly the projector size
/// matches buildRange * 2 (diameter).
///
public class BuildRangeIndicator : MonoBehaviour
{
[Tooltip("DecalProjector child component to drive. Auto-found in Awake if empty.")]
[SerializeField] private DecalProjector projector;
[Tooltip("Vertical thickness of the decal projector's projection volume. Should " +
"exceed your map's vertical range so the decal projects onto terrain at any height.")]
[SerializeField] private float projectionDepth = 50f;
// Cached references resolved lazily.
private Builder cachedBuilder;
private TowerPlacementController cachedPlacementController;
// ----- Lifecycle --------------------------------------------------
private void Awake()
{
if (projector == null) projector = GetComponentInChildren();
if (projector == null)
{
Debug.LogError("[BuildRangeIndicator] No DecalProjector found. Add one as a " +
"child of this GameObject.");
enabled = false;
return;
}
// Start hidden; visibility is updated each frame.
projector.enabled = false;
}
private void Start()
{
cachedBuilder = GetComponentInParent();
if (cachedBuilder == null)
{
Debug.LogError("[BuildRangeIndicator] No Builder found in parents. " +
"Disabling indicator.");
enabled = false;
return;
}
// Hide for non-owners — other players don't see your range indicator.
if (!cachedBuilder.IsOwner)
{
enabled = false;
if (projector != null) projector.enabled = false;
return;
}
// Size the projector to match the builder's range (diameter = 2 * range).
// Modern URP DecalProjector exposes width/height/pivot as separate properties
// rather than a single Vector3 size. Width and Height are the ground-plane extents
// of the projection; pivot.z is the volume's depth offset (0 = volume centered
// on the projector position, projecting equally above and below).
float diameter = cachedBuilder.BuildRange * 2f;
// The DecalProjector exposes a `size` Vector3 on its API even though the
// inspector splits it into Width/Height/ProjectionDepth — assignment is still
// valid in current URP. We use it here to set all three at once.
projector.size = new Vector3(diameter, diameter, projectionDepth);
projector.pivot = new Vector3(0f, 0f, 0f);
// Center the projection volume on the builder. The decal projector projects
// along its local +Z axis by default; rotate to project downward (look down).
//
// CRITICAL: rotate the PROJECTOR's transform, not this component's transform.
// BuildRangeIndicator lives on the Builder root (so it can find Builder via
// GetComponentInParent), but `transform` here is the Builder's transform —
// rotating it tips the cylinder onto its side. The projector lives on a child
// GameObject; that's the one that needs the 90° X rotation.
projector.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
projector.transform.localPosition = Vector3.zero;
}
private void Update()
{
if (cachedBuilder == null) return;
bool shouldShow = IsLocalPlayerPlacing();
if (projector.enabled != shouldShow)
projector.enabled = shouldShow;
}
// ----- Helpers ----------------------------------------------------
private bool IsLocalPlayerPlacing()
{
if (cachedPlacementController == null)
{
cachedPlacementController =
UnityEngine.Object.FindAnyObjectByType();
if (cachedPlacementController == null) return false;
}
return cachedPlacementController.IsPlacing;
}
}
}