Adding tons of new functionality

Decals, ghost textures, placement functionality, builder stub ins, a new camera system,  and more.
This commit is contained in:
Matt F 2026-05-04 00:01:30 -07:00
parent 56dc775c68
commit a63cce53e2
54 changed files with 4817 additions and 238 deletions

View file

@ -0,0 +1,273 @@
// Assets/_Project/Scripts/Gameplay/TowerInstance.cs
using Unity.Collections;
using Unity.Netcode;
using UnityEngine;
using TD.Core;
using TD.Towers;
namespace TD.Gameplay
{
/// <summary>
/// Per-tower runtime component. Lives on the tower's NetworkObject prefab root.
///
/// Responsibilities:
/// <list type="bullet">
/// <item>Hold the network-replicated identity of this tower: which
/// <see cref="TowerDefinition"/> it is and which <see cref="PlayerSlot"/> owns it.</item>
/// <item>On <see cref="OnNetworkSpawn"/>, stamp the tower's footprint into
/// <see cref="LevelLoader"/> on every client so local grids stay in sync
/// with the server-authoritative state.</item>
/// <item>Apply the owner's player color to the tower mesh, so towers are
/// visually distinct by zone during testing.</item>
/// </list>
/// </remarks>
/// <remarks>
/// <para><b>Grid stamping split.</b> The server stamps the footprint in
/// <c>TowerPlacementManager.ProcessRequest</c> (before <c>NetworkObject.Spawn</c>)
/// so the path-validity check in the same frame sees the updated grid. Non-host
/// clients stamp in <see cref="OnNetworkSpawn"/> when NGO replicates the
/// NetworkObject to them. The server's <see cref="OnNetworkSpawn"/> also runs,
/// but by then the footprint is already stamped — <see cref="SetWalkable"/> and
/// <see cref="SetOccupied"/> are idempotent writes, so double-stamping is safe.</para>
///
/// <para><b>Definition reference replication.</b> TowerDefinition assets live in the
/// project on all clients. We replicate the asset by name via a
/// <see cref="NetworkVariable{T}"/> holding a <c>FixedString64Bytes</c>, then look
/// up the asset locally. This avoids serializing the full ScriptableObject over the
/// network. The lookup uses a <see cref="TowerRegistry"/> singleton that must be
/// present in the scene. (Temporary: will be driven by RaceDefinition in Path E.)</para>
///
/// <para><b>Combat.</b> No combat logic here yet. Combat fields live stubbed on
/// <see cref="TowerDefinition"/>; they will be consumed by a future
/// <c>TowerCombat</c> component added to the same prefab.</para>
/// </remarks>
[RequireComponent(typeof(NetworkObject))]
public class TowerInstance : NetworkBehaviour
{
// ----- Networked state ------------------------------------------------
// The name of the TowerDefinition asset for this tower. Replicated so all
// clients can look up the full definition locally without sending the whole
// ScriptableObject over the wire.
private readonly NetworkVariable<FixedString64Bytes> definitionName =
new NetworkVariable<FixedString64Bytes>(
default,
readPerm: NetworkVariableReadPermission.Everyone,
writePerm: NetworkVariableWritePermission.Server);
// The footprint anchor (SW corner, world-tile coords). Replicated so
// clients can stamp the correct tiles in OnNetworkSpawn.
private readonly NetworkVariable<Vector2Int> anchorTile =
new NetworkVariable<Vector2Int>(
default,
readPerm: NetworkVariableReadPermission.Everyone,
writePerm: NetworkVariableWritePermission.Server);
// The PlayerSlot that placed this tower. Replicated for the HUD context
// panel and for view-selection by non-owning clients.
private readonly NetworkVariable<PlayerSlot> ownerSlot =
new NetworkVariable<PlayerSlot>(
PlayerSlot.None,
readPerm: NetworkVariableReadPermission.Everyone,
writePerm: NetworkVariableWritePermission.Server);
// ----- Local resolved state -------------------------------------------
// Resolved on every client in OnNetworkSpawn from definitionName.
// Null if the lookup fails (missing TowerRegistry or unknown name).
private TowerDefinition resolvedDefinition;
// ----- Pre-spawn initialization data ----------------------------------
//
// Set by InitializeServer (called by TowerPlacementManager BEFORE Spawn).
// Read by the server's OnNetworkSpawn to populate the NetworkVariables.
//
// Why this two-step dance: NGO 2.x disallows writing NetworkVariables
// before NetworkObject.Spawn() — those writes produce warnings and may
// not replicate reliably. The supported pattern is to set NVs inside
// OnNetworkSpawn on the server; NGO captures those writes and includes
// them in the initial sync message sent to clients, so every client
// sees correct values on its very first OnNetworkSpawn callback.
private TowerDefinition pendingDefinition;
private Vector2Int pendingAnchor;
private PlayerSlot pendingOwner = PlayerSlot.None;
private bool hasPendingInit;
// ----- Public accessors -----------------------------------------------
/// <summary>The TowerDefinition for this tower, resolved locally. Null until
/// <see cref="OnNetworkSpawn"/> runs and the definition lookup succeeds.</summary>
public TowerDefinition Definition => resolvedDefinition;
/// <summary>The PlayerSlot that placed this tower.</summary>
public PlayerSlot Owner => ownerSlot.Value;
/// <summary>The footprint anchor tile (SW corner, world-tile coords).</summary>
public Vector2Int AnchorTile => anchorTile.Value;
// ----- Server-only initialization -------------------------------------
/// <summary>
/// Called by <c>TowerPlacementManager</c> on the server immediately after
/// instantiation and before <c>NetworkObject.Spawn</c>. Stores the data that
/// the server's <see cref="OnNetworkSpawn"/> will copy into the
/// NetworkVariables. NetworkVariables themselves are NOT written here —
/// see the comment on the pending-init fields above for why.
/// </summary>
public void InitializeServer(TowerDefinition def, Vector2Int anchor, PlayerSlot owner)
{
var nm = NetworkManager.Singleton;
if (nm == null || !nm.IsServer)
{
Debug.LogError("[TowerInstance] InitializeServer called when not running " +
"as a server. This must only be called by " +
"TowerPlacementManager on the server.");
return;
}
pendingDefinition = def;
pendingAnchor = anchor;
pendingOwner = owner;
hasPendingInit = true;
// Cache the resolved definition on the server immediately — clients
// will resolve via the registry lookup once definitionName arrives.
resolvedDefinition = def;
}
// ----- NGO lifecycle --------------------------------------------------
public override void OnNetworkSpawn()
{
// Server-only step: now that the NetworkObject is fully spawned,
// write the pending init values to the NetworkVariables. These writes
// will be captured in the initial sync message sent to clients, so
// every client sees correct values on its very first OnNetworkSpawn.
if (IsServer && hasPendingInit)
{
definitionName.Value = new FixedString64Bytes(pendingDefinition.name);
anchorTile.Value = pendingAnchor;
ownerSlot.Value = pendingOwner;
// Clear the pending data — it's now committed to NetworkVariables.
hasPendingInit = false;
}
// Resolve the TowerDefinition from the (now-available) replicated name.
// On the server this is already set by InitializeServer; the lookup is
// redundant but harmless and keeps the code path uniform.
ResolveDefinition();
// Stamp the footprint into the local LevelLoader grids.
// The server already stamped in TowerPlacementManager before Spawn(),
// but SetWalkable/SetOccupied are idempotent — double-stamping is safe.
StampFootprint(walkable: false, occupied: true);
// Apply owner color to the mesh renderer.
ApplyOwnerColor();
if (resolvedDefinition != null)
{
Debug.Log($"[TowerInstance] Spawned '{resolvedDefinition.DisplayName}' " +
$"for {ownerSlot.Value} at anchor {anchorTile.Value}. " +
$"IsServer={IsServer}");
}
}
public override void OnNetworkDespawn()
{
// Un-stamp the footprint when the tower is destroyed (sold, wave end, etc.)
// so the tiles become walkable and buildable again.
StampFootprint(walkable: true, occupied: false);
}
// ----- Private helpers ------------------------------------------------
private void ResolveDefinition()
{
// Already resolved (server path via InitializeServer).
if (resolvedDefinition != null) return;
string defName = definitionName.Value.ToString();
if (string.IsNullOrEmpty(defName))
{
Debug.LogError($"[TowerInstance] NetworkObject {NetworkObjectId}: " +
$"definitionName is empty. Cannot resolve TowerDefinition.");
return;
}
var registry = TowerRegistry.Instance;
if (registry == null)
{
Debug.LogError($"[TowerInstance] NetworkObject {NetworkObjectId}: " +
$"No TowerRegistry found in scene. Cannot resolve '{defName}'.");
return;
}
resolvedDefinition = registry.Get(defName);
if (resolvedDefinition == null)
{
Debug.LogError($"[TowerInstance] NetworkObject {NetworkObjectId}: " +
$"TowerRegistry does not contain a definition named '{defName}'.");
}
}
private void StampFootprint(bool walkable, bool occupied)
{
var loader = LevelLoader.Instance;
if (loader == null || !loader.IsLoaded)
{
Debug.LogWarning($"[TowerInstance] NetworkObject {NetworkObjectId}: " +
$"LevelLoader not available during footprint stamp. " +
$"Grids may be out of sync.");
return;
}
// Determine footprint size from the resolved definition, falling back to
// 2×2 if the definition hasn't resolved yet (shouldn't happen, but defensive).
Vector2Int footprintSize = resolvedDefinition != null
? resolvedDefinition.FootprintSize
: new Vector2Int(2, 2);
foreach (var tile in GridCoordinates.GetFootprintTiles(anchorTile.Value, footprintSize))
{
loader.SetWalkable(tile, walkable);
loader.SetOccupied(tile, occupied);
}
}
// Reused per-instance across color updates to avoid per-call GC allocation.
// MaterialPropertyBlock is not thread-safe but all rendering runs on the
// main thread, so a single instance per TowerInstance is fine.
private MaterialPropertyBlock colorPropertyBlock;
private static readonly int ColorPropertyId = Shader.PropertyToID("_Color");
// URP Lit uses _BaseColor, not _Color. Writing both ensures the tint applies
// regardless of which shader the prefab uses; unknown property writes are
// silently ignored.
private static readonly int BaseColorPropertyId = Shader.PropertyToID("_BaseColor");
private void ApplyOwnerColor()
{
Color ownerColor = PlayerColors.Get(ownerSlot.Value);
ownerColor.a = 1f;
// 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.
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)
{
Debug.LogWarning($"[TowerInstance] NetworkObject {NetworkObjectId}: " +
$"No MeshRenderers found for owner color tinting.");
}
}
}
}