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

@ -74,15 +74,30 @@ namespace TD.Gameplay
// The mutable walkability grid. Initialized from LevelData.WalkabilityGrid
// and mutated by tower placement at runtime. Stays in lockstep with the
// baked grid until towers are placed (none yet, since tower placement
// isn't implemented).
// baked grid until towers are placed.
//
// This is intentionally NOT exposed through a property yet -- consumers
// will query through IsWalkable(Vector2Int) instead, hiding the array
// indexing. When tower placement needs to mutate it, we'll expose a
// SetWalkable method then. Easier to add than to take away.
// Consumers query through IsWalkable(Vector2Int) and mutate through
// SetWalkable(Vector2Int, bool), which hide the flat-array indexing.
//
// NOTE: This grid will move to MatchState when that NetworkBehaviour is
// implemented, so server-authoritative mutation is co-located with other
// match-wide state. For now it lives here because no consumer needed it
// to live elsewhere.
private bool[] runtimeWalkability;
// Per-tile tower occupancy. True when a tower's footprint covers this tile.
// Distinct from runtimeWalkability because:
// (a) spawner and leak-exit tiles are walkable but can never be occupied
// by a tower — tracking them separately avoids ambiguity.
// (b) the placement ghost needs to detect tile conflicts independently
// of walkability (a tile is walkable until the tower is placed,
// but the ghost should turn red as soon as a prior placement
// reserves it).
//
// Initialized to all-false on Awake. Mutated via SetOccupied; queried
// via IsOccupied. Will move to MatchState alongside runtimeWalkability.
private bool[] runtimeOccupied;
// The buildable-plane GameObject we instantiated as our child.
// Cached for inspector debugging and future destruction.
private GameObject buildablePlaneGO;
@ -113,6 +128,7 @@ namespace TD.Gameplay
}
InitializeRuntimeWalkability();
InitializeRuntimeOccupied();
SpawnBuildablePlane();
IsLoaded = true;
@ -190,6 +206,13 @@ namespace TD.Gameplay
level.WalkabilityGrid, runtimeWalkability, level.WalkabilityGrid.Length);
}
private void InitializeRuntimeOccupied()
{
// All tiles start unoccupied. bool[] default-initializes to false,
// so Array.Clear is redundant but makes intent explicit.
runtimeOccupied = new bool[level.WalkabilityGrid.Length];
}
private void SpawnBuildablePlane()
{
// Compute the world-space center and size of the grid.
@ -314,6 +337,59 @@ namespace TD.Gameplay
return level.OwnerGrid[idx];
}
/// <summary>
/// True if <paramref name="tile"/> is currently occupied by a placed tower footprint.
/// Returns false for out-of-bounds tiles. Unlike <see cref="IsWalkable"/>, this grid
/// starts all-false and only becomes true when a tower is successfully placed.
/// </summary>
/// <remarks>
/// The placement ghost uses this (alongside <see cref="GetPlacement"/> and
/// <see cref="GetOwner"/>) to decide whether to render as valid (white) or invalid (red).
/// The server uses it as part of the tile-availability check before path validation.
/// </remarks>
public bool IsOccupied(Vector2Int tile)
{
if (!TryFlatIndex(tile, out int idx)) return false;
return runtimeOccupied[idx];
}
// ----- Runtime mutators -------------------------------------------
//
// Called by TowerPlacementManager (server-side) when a tower placement
// is accepted. Both mutators must be called for every tile in the tower's
// footprint so the two grids stay in sync.
//
// These are not RPCs — LevelLoader is a plain MonoBehaviour, not a
// NetworkBehaviour. The server calls these directly after authoritative
// validation; clients learn about the change when the TowerInstance
// NetworkObject spawns and its Start/OnNetworkSpawn stamps its own
// footprint locally.
/// <summary>
/// Sets the runtime walkability of <paramref name="tile"/>. Called by
/// <c>TowerPlacementManager</c> on the server when a tower is accepted (pass
/// <c>false</c>) and when a tower is sold/destroyed (pass <c>true</c>).
/// No-ops silently for out-of-bounds tiles.
/// </summary>
public void SetWalkable(Vector2Int tile, bool walkable)
{
if (!TryFlatIndex(tile, out int idx)) return;
runtimeWalkability[idx] = walkable;
}
/// <summary>
/// Sets the runtime occupancy of <paramref name="tile"/>. Called alongside
/// <see cref="SetWalkable"/> — always update both grids together so they
/// stay in sync. Pass <c>true</c> when a tower is placed, <c>false</c> when
/// it is sold or destroyed.
/// No-ops silently for out-of-bounds tiles.
/// </summary>
public void SetOccupied(Vector2Int tile, bool occupied)
{
if (!TryFlatIndex(tile, out int idx)) return;
runtimeOccupied[idx] = occupied;
}
// Translates world-tile coordinates to a flat-array index, returning
// false if the tile is out of bounds. Used by all query methods.
private bool TryFlatIndex(Vector2Int tile, out int idx)
@ -332,9 +408,9 @@ namespace TD.Gameplay
// Gizmos run in both edit mode and play mode. In edit mode we use the
// baked walkability/owner grids from the LevelData asset directly,
// because runtimeWalkability hasn't been initialized yet. In play mode
// we use runtimeWalkability so the visualization reflects any future
// tower stamps. Owner and placement grids are immutable, so we read
// them from the asset in both modes.
// we use runtimeWalkability so the visualization reflects tower stamps.
// Owner and placement grids are immutable, so we read them from the
// asset in both modes.
private void OnDrawGizmos()
{