Major updates to the HUD and selectable objects

This commit is contained in:
Matt F 2026-05-11 23:57:35 -07:00
parent 5bc757b385
commit c100db52e5
23 changed files with 1615 additions and 614 deletions

View file

@ -43,8 +43,18 @@ namespace TD.Gameplay
/// <c>TowerCombat</c> component added to the same prefab.</para>
/// </remarks>
[RequireComponent(typeof(NetworkObject))]
public class TowerInstance : NetworkBehaviour, IMinimapEntity
public class TowerInstance : NetworkBehaviour, IMinimapEntity, ISelectable
{
// ----- Inspector --------------------------------------------------
[Header("Visuals")]
[Tooltip("Mesh renderers tinted with the owner's player color. " +
"Drag in only the tower body's renderers — exclude anything " +
"that has its own color rules (selection rings, range " +
"indicators, FX). If left empty, the tower is NOT tinted " +
"and the prefab's baked materials show through.")]
[SerializeField] private MeshRenderer[] tintedRenderers;
// ----- Networked state ------------------------------------------------
// The name of the TowerDefinition asset for this tower. Replicated so all
@ -107,6 +117,40 @@ namespace TD.Gameplay
/// <summary>The footprint anchor tile (SW corner, world-tile coords).</summary>
public Vector2Int AnchorTile => anchorTile.Value;
// ----- ISelectable ----------------------------------------------------
// Absolute world-unit margin that the selection ring extends beyond the
// tower's footprint edges. Tuned for visibility — the tower body sits ON
// the ring at ground level, so only the area outside the footprint is
// actually rendered. Too small (was 0.15) and the ring is invisible under
// anything taller than a paving stone. 0.5 gives a half-tile-wide visible
// band around the tower at any footprint size.
private const float SelectionRingPadding = 0.5f;
/// <summary>Display name shown in the HUD portrait when this tower is selected.</summary>
public string DisplayName =>
resolvedDefinition != null ? resolvedDefinition.DisplayName : "Tower";
public SelectableKind Kind => SelectableKind.Tower;
public Transform SelectionTransform => transform;
// Ring radius derived from footprint: max axis * 0.5 * tile size, plus a
// small padding so the ring is visible outside the tower's edges. Falls
// back to 1×1 if the definition hasn't resolved yet (transient, harmless —
// the HUD won't allow selection until OnNetworkSpawn finishes anyway).
public float SelectionRadius
{
get
{
Vector2Int fp = resolvedDefinition != null
? resolvedDefinition.FootprintSize
: new Vector2Int(1, 1);
return Mathf.Max(fp.x, fp.y) * 0.5f * GridCoordinates.TILE_SIZE
+ SelectionRingPadding;
}
}
// ----- Server-only initialization -------------------------------------
/// <summary>
@ -171,6 +215,20 @@ namespace TD.Gameplay
// Register for minimap rendering.
MinimapEntityRegistry.Register(this);
// Selection auto-transfer: if a BuildSiteVisual at our anchor is the
// active local selection, the player was watching this tower complete —
// hand selection off to the new TowerInstance so the HUD/visualizer
// transition smoothly. Server's completion order (spawn THEN despawn)
// means we get here BEFORE the BuildSiteVisual's OnNetworkDespawn,
// so the old reference is still valid and selected.
var selState = SelectionState.Instance;
if (selState != null
&& selState.SelectedObject is BuildSiteVisual bsv
&& bsv.Anchor == anchorTile.Value)
{
selState.Select(this);
}
if (resolvedDefinition != null)
{
Debug.Log($"[TowerInstance] Spawned '{resolvedDefinition.DisplayName}' " +
@ -186,6 +244,13 @@ namespace TD.Gameplay
StampFootprint(walkable: true, occupied: false);
MinimapEntityRegistry.Deregister(this);
// Clear local selection if THIS tower was selected. Without this,
// SelectionState (and any subscriber holding our reference — HUD,
// SelectionVisualizer) keeps pointing at a soon-to-be-destroyed Unity
// object and throws MissingReferenceException on the next access.
if (SelectionState.Instance != null && SelectionState.Instance.IsSelected(this))
SelectionState.Instance.Clear();
}
// ----- IMinimapEntity -------------------------------------------------
@ -285,19 +350,20 @@ namespace TD.Gameplay
// MaterialPropertyBlock sets per-renderer properties without allocating
// a new Material object. Safe to reuse across calls on the same instance.
// All Unity standard/URP shaders expose _Color or _BaseColor, so no shader changes needed.
// All Unity standard/URP shaders expose _Color or _BaseColor, so writing
// both lets the tint apply regardless of which shader the prefab uses.
colorPropertyBlock ??= new MaterialPropertyBlock();
colorPropertyBlock.SetColor(ColorPropertyId, ownerColor);
colorPropertyBlock.SetColor(BaseColorPropertyId, ownerColor);
var renderers = GetComponentsInChildren<MeshRenderer>();
foreach (var rend in renderers)
rend.SetPropertyBlock(colorPropertyBlock);
if (renderers.Length == 0)
// Tint only the renderers explicitly listed in the inspector. Avoids
// accidentally re-coloring decorative children, FX, etc. (Mirrors
// Builder.tintedRenderers — same rationale.)
if (tintedRenderers == null) return;
foreach (var rend in tintedRenderers)
{
Debug.LogWarning($"[TowerInstance] NetworkObject {NetworkObjectId}: " +
$"No MeshRenderers found for owner color tinting.");
if (rend == null) continue;
rend.SetPropertyBlock(colorPropertyBlock);
}
}
}