Adding tons of new functionality
Decals, ghost textures, placement functionality, builder stub ins, a new camera system, and more.
This commit is contained in:
parent
56dc775c68
commit
a63cce53e2
54 changed files with 4817 additions and 238 deletions
547
Assets/_Project/Scripts/Gameplay/TowerPlacementManager.cs
Normal file
547
Assets/_Project/Scripts/Gameplay/TowerPlacementManager.cs
Normal file
|
|
@ -0,0 +1,547 @@
|
|||
// Assets/_Project/Scripts/Gameplay/TowerPlacementManager.cs
|
||||
using System.Collections.Generic;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using TD.Core;
|
||||
using TD.Levels;
|
||||
using TD.Towers;
|
||||
|
||||
namespace TD.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-authoritative manager for tower placement requests. Receives placement
|
||||
/// requests from clients via RPC, validates them in order, and either spawns the
|
||||
/// tower or rejects the request with a reason code.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>Queue-based processing.</b> Incoming RPCs enqueue a
|
||||
/// <see cref="PlacementRequest"/> rather than validating inline. Each server
|
||||
/// Update drains up to <see cref="RequestsPerFrame"/> requests. At 60 fps this
|
||||
/// gives ~180 validations/second, comfortably above the worst-case 90/second
|
||||
/// (9 players × 10 placements/second). Queuing keeps the server frame budget
|
||||
/// predictable regardless of burst traffic.</para>
|
||||
///
|
||||
/// <para><b>Server-only logic.</b> All validation and mutation runs on the server.
|
||||
/// Clients learn about accepted placements when the <see cref="TowerInstance"/>
|
||||
/// NetworkObject spawns (NGO replicates it automatically). Clients learn about
|
||||
/// rejections via <see cref="PlacementRejectedRpc"/>.</para>
|
||||
///
|
||||
/// <para><b>Validation order:</b>
|
||||
/// <list type="number">
|
||||
/// <item>Ownership — every footprint tile must be owned by the requesting player.</item>
|
||||
/// <item>Placement state — every footprint tile must be <c>Buildable</c> and unoccupied.</item>
|
||||
/// <item>Gold — the placing player must have enough gold.</item>
|
||||
/// <item>Path — a BFS confirms every spawner in the placing player's zone still reaches
|
||||
/// an exit after the footprint is stamped as non-walkable.</item>
|
||||
/// </list></para>
|
||||
///
|
||||
/// <para><b>Path-check BFS.</b> The server temporarily stamps the footprint,
|
||||
/// runs BFS per spawner, then un-stamps if the check fails. This is O(tiles in zone)
|
||||
/// per spawner per request — acceptable for low-frequency gameplay actions and the
|
||||
/// queue-rate-limited processing model.</para>
|
||||
///
|
||||
/// <para><b>Builder range check.</b> Deliberately omitted in Path B. The builder
|
||||
/// system does not exist yet. When Path D is implemented, add a range check between
|
||||
/// steps 2 and 3 above, gated on the requesting player's Builder position.</para>
|
||||
/// </remarks>
|
||||
public class TowerPlacementManager : NetworkBehaviour
|
||||
{
|
||||
// ----- Singleton --------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// The active TowerPlacementManager. Null before the scene loads or after it unloads.
|
||||
/// Always null-check before use. Only meaningful on the server; clients route all
|
||||
/// placement through RPC.
|
||||
/// </summary>
|
||||
public static TowerPlacementManager Instance { get; private set; }
|
||||
|
||||
// ----- Inspector --------------------------------------------------
|
||||
|
||||
[Tooltip("Maximum number of placement requests processed per server Update tick. " +
|
||||
"At 60 fps, 3 requests/frame = 180 validations/second, well above the " +
|
||||
"worst-case 90/second (9 players × 10 placements/second).")]
|
||||
[SerializeField] private int requestsPerFrame = 3;
|
||||
|
||||
[Tooltip("Tower definitions available in this match, indexed by TowerTypeId. " +
|
||||
"Populate this with every TowerDefinition asset the current race roster " +
|
||||
"contains. Index 0 is reserved; valid IDs start at 1. " +
|
||||
"(Temporary: will be driven by RaceDefinition once Path E is complete.)")]
|
||||
[SerializeField] private TowerDefinition[] towerDefinitions = new TowerDefinition[0];
|
||||
|
||||
// ----- Internal request queue -------------------------------------
|
||||
|
||||
private struct PlacementRequest
|
||||
{
|
||||
public ulong SenderClientId;
|
||||
public Vector2Int Anchor; // SW corner of the footprint, in world-tile coords.
|
||||
public int TowerTypeId; // Index into towerDefinitions[].
|
||||
}
|
||||
|
||||
private readonly Queue<PlacementRequest> pendingRequests = new Queue<PlacementRequest>();
|
||||
|
||||
// Reusable scratch collections for BFS — allocated once and cleared per
|
||||
// check to avoid per-frame GC pressure. Only used on the server.
|
||||
private readonly Queue<Vector2Int> bfsQueue = new Queue<Vector2Int>();
|
||||
private readonly HashSet<Vector2Int> bfsVisited = new HashSet<Vector2Int>();
|
||||
|
||||
// ----- Lifecycle --------------------------------------------------
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
if (IsServer)
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Debug.LogError("[TowerPlacementManager] Multiple instances detected. " +
|
||||
"Only one TowerPlacementManager should exist per scene.");
|
||||
}
|
||||
Instance = this;
|
||||
Debug.Log("[TowerPlacementManager] Server ready.");
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
|
||||
// ----- Server Update — queue drain --------------------------------
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
int processed = 0;
|
||||
while (pendingRequests.Count > 0 && processed < requestsPerFrame)
|
||||
{
|
||||
ProcessRequest(pendingRequests.Dequeue());
|
||||
processed++;
|
||||
}
|
||||
}
|
||||
|
||||
// ----- RPC: client → server (placement request) -------------------
|
||||
|
||||
/// <summary>
|
||||
/// Client entry point. Call this on the local client to request placing a tower.
|
||||
/// The server will validate and either spawn the tower (visible to all clients)
|
||||
/// or call back with <see cref="PlacementRejectedRpc"/>.
|
||||
/// </summary>
|
||||
/// <param name="anchorX">X component of the footprint anchor tile (world-tile coords).</param>
|
||||
/// <param name="anchorY">Y component of the footprint anchor tile (world-tile coords).</param>
|
||||
/// <param name="towerTypeId">Index into the server's towerDefinitions array.</param>
|
||||
[Rpc(SendTo.Server)]
|
||||
public void RequestPlaceTowerRpc(int anchorX, int anchorY, int towerTypeId,
|
||||
RpcParams rpcParams = default)
|
||||
{
|
||||
pendingRequests.Enqueue(new PlacementRequest
|
||||
{
|
||||
SenderClientId = rpcParams.Receive.SenderClientId,
|
||||
Anchor = new Vector2Int(anchorX, anchorY),
|
||||
TowerTypeId = towerTypeId,
|
||||
});
|
||||
}
|
||||
|
||||
// ----- RPC: server → client (rejection notification) --------------
|
||||
|
||||
/// <summary>
|
||||
/// Sent by the server to the placing client when a placement request is rejected.
|
||||
/// The client uses the reason to display a feedback message to the player.
|
||||
/// </summary>
|
||||
[Rpc(SendTo.SpecifiedInParams)]
|
||||
private void PlacementRejectedRpc(PlacementRejectionReason reason,
|
||||
RpcParams rpcParams = default)
|
||||
{
|
||||
// This executes on the target client.
|
||||
Debug.Log($"[TowerPlacementManager] Placement rejected: {reason}");
|
||||
|
||||
// TowerPlacementController listens for this via the static event below.
|
||||
OnPlacementRejected?.Invoke(reason);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fired on the local client when the server rejects this client's placement request.
|
||||
/// <see cref="TowerPlacementController"/> subscribes to this to display rejection
|
||||
/// feedback messages.
|
||||
/// </summary>
|
||||
public static event System.Action<PlacementRejectionReason> OnPlacementRejected;
|
||||
|
||||
// ----- Core validation and placement logic ------------------------
|
||||
|
||||
private void ProcessRequest(PlacementRequest req)
|
||||
{
|
||||
// Resolve the TowerDefinition first — needed by every subsequent check.
|
||||
if (!TryGetDefinition(req.TowerTypeId, out TowerDefinition def))
|
||||
{
|
||||
Reject(req, PlacementRejectionReason.InvalidTowerType);
|
||||
return;
|
||||
}
|
||||
|
||||
var loader = LevelLoader.Instance;
|
||||
if (loader == null || !loader.IsLoaded)
|
||||
{
|
||||
Reject(req, PlacementRejectionReason.ServerError);
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect the footprint tiles once; used by all subsequent checks.
|
||||
var footprint = new List<Vector2Int>(def.FootprintSize.x * def.FootprintSize.y);
|
||||
foreach (var tile in GridCoordinates.GetFootprintTiles(req.Anchor, def.FootprintSize))
|
||||
footprint.Add(tile);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Check 1: Ownership
|
||||
// Every footprint tile must be owned by the placing player's zone.
|
||||
// ------------------------------------------------------------------
|
||||
PlayerSlot placingSlot = ClientIdToPlayerSlot(req.SenderClientId);
|
||||
if (placingSlot == PlayerSlot.None)
|
||||
{
|
||||
Reject(req, PlacementRejectionReason.ServerError);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var tile in footprint)
|
||||
{
|
||||
if (loader.GetOwner(tile) != placingSlot)
|
||||
{
|
||||
Reject(req, PlacementRejectionReason.WrongOwner);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Check 2: Placement state + occupancy
|
||||
// Every footprint tile must be Buildable and not already occupied.
|
||||
// ------------------------------------------------------------------
|
||||
foreach (var tile in footprint)
|
||||
{
|
||||
if (loader.GetPlacement(tile) != PlacementState.Buildable)
|
||||
{
|
||||
Reject(req, PlacementRejectionReason.TileNotBuildable);
|
||||
return;
|
||||
}
|
||||
if (loader.IsOccupied(tile))
|
||||
{
|
||||
Reject(req, PlacementRejectionReason.TileOccupied);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Check 3: Build range
|
||||
// Tower must be within the placing player's builder's build range.
|
||||
// Cheap to check; runs before gold and path so we don't burn cycles
|
||||
// on out-of-range placements.
|
||||
// ------------------------------------------------------------------
|
||||
var builder = Builder.GetForClient(req.SenderClientId);
|
||||
if (builder == null)
|
||||
{
|
||||
// No builder spawned for this client — server-side error, not a player-facing one.
|
||||
Reject(req, PlacementRejectionReason.ServerError);
|
||||
return;
|
||||
}
|
||||
if (!builder.IsTileWithinBuildRange(req.Anchor, def.FootprintSize))
|
||||
{
|
||||
Reject(req, PlacementRejectionReason.OutOfRange);
|
||||
return;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Check 4: Gold
|
||||
// Validate before the path check — cheaper, and no point running BFS
|
||||
// if the player can't afford the tower.
|
||||
// ------------------------------------------------------------------
|
||||
var goldManager = PlayerGoldManager.GetForClient(req.SenderClientId);
|
||||
if (goldManager == null || goldManager.CurrentGold < def.GoldCost)
|
||||
{
|
||||
Reject(req, PlacementRejectionReason.InsufficientGold);
|
||||
return;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Check 5: Path validity
|
||||
// Temporarily stamp the footprint, run BFS per spawner in the placing
|
||||
// player's zone, then un-stamp if any spawner loses its exit route.
|
||||
// ------------------------------------------------------------------
|
||||
StampFootprint(loader, footprint, walkable: false, occupied: true);
|
||||
|
||||
bool pathValid = CheckPathValidity(loader, placingSlot);
|
||||
|
||||
if (!pathValid)
|
||||
{
|
||||
// Un-stamp — the placement is rejected, grid stays as it was.
|
||||
StampFootprint(loader, footprint, walkable: true, occupied: false);
|
||||
Reject(req, PlacementRejectionReason.BlocksPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// All checks passed — commit the placement.
|
||||
// The footprint stamp is already applied (walkable=false, occupied=true).
|
||||
// Deduct gold and spawn the tower NetworkObject.
|
||||
// ------------------------------------------------------------------
|
||||
goldManager.DeductGold(def.GoldCost);
|
||||
|
||||
SpawnTower(def, req.Anchor, placingSlot);
|
||||
|
||||
Debug.Log($"[TowerPlacementManager] Placed '{def.DisplayName}' for " +
|
||||
$"client {req.SenderClientId} ({placingSlot}) at anchor {req.Anchor}.");
|
||||
}
|
||||
|
||||
// ----- Path-validity BFS ------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if every spawner in <paramref name="placingSlot"/>'s zone can still
|
||||
/// reach an exit tile (any leak exit tile OR any goal tile) via walkable tiles,
|
||||
/// given the current state of <c>LevelLoader</c>'s runtime walkability grid.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Mirrors bake-time P5-4. Runs a BFS per spawner against the runtime walkability
|
||||
/// grid. Reuses <see cref="bfsQueue"/> and <see cref="bfsVisited"/> scratch
|
||||
/// collections (cleared between BFS runs) to avoid GC allocation per call.
|
||||
/// </remarks>
|
||||
private bool CheckPathValidity(LevelLoader loader, PlayerSlot slot)
|
||||
{
|
||||
var levelData = loader.LevelData;
|
||||
|
||||
// Find the PlayerZoneData for this slot.
|
||||
PlayerZoneData zoneData = null;
|
||||
foreach (var zone in levelData.PlayerZones)
|
||||
{
|
||||
if (zone.Owner == slot) { zoneData = zone; break; }
|
||||
}
|
||||
if (zoneData == null || zoneData.Spawners == null || zoneData.Spawners.Length == 0)
|
||||
{
|
||||
// No spawners means nothing to block — treat as valid.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Build the exit tile set: union of all leak exit tiles and all goal tiles.
|
||||
// This is built fresh per call because it doesn't change within a match
|
||||
// (tiles never move), but the allocation cost is small and correctness
|
||||
// is more important than micro-optimization here.
|
||||
var exitTiles = BuildExitTileSet(levelData, slot);
|
||||
if (exitTiles.Count == 0)
|
||||
{
|
||||
// Zone has no exits at all — this would have been caught at bake time (P5-8).
|
||||
// Treat as valid so a bake-side error doesn't cause all placements to fail.
|
||||
Debug.LogWarning($"[TowerPlacementManager] Zone {slot} has no exit tiles. " +
|
||||
$"This should have been caught at bake time (P5-8).");
|
||||
return true;
|
||||
}
|
||||
|
||||
// BFS per spawner: each spawner's tile area is the BFS seed set.
|
||||
foreach (var spawner in zoneData.Spawners)
|
||||
{
|
||||
if (!SpawnerCanReachExit(loader, spawner, exitTiles))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the set of tiles that count as "exits" for the given player zone:
|
||||
/// all tiles of every LeakExit FROM this zone, plus all goal tiles.
|
||||
/// </summary>
|
||||
private HashSet<Vector2Int> BuildExitTileSet(LevelData levelData, PlayerSlot slot)
|
||||
{
|
||||
var exits = new HashSet<Vector2Int>();
|
||||
|
||||
// Leak exit tiles for this zone.
|
||||
foreach (var zone in levelData.PlayerZones)
|
||||
{
|
||||
if (zone.Owner != slot) continue;
|
||||
if (zone.LeakExits == null) continue;
|
||||
foreach (var leak in zone.LeakExits)
|
||||
foreach (var tile in leak.TileArea)
|
||||
exits.Add(tile);
|
||||
}
|
||||
|
||||
// Goal tiles (all goals count as exits for the final defender zone).
|
||||
if (levelData.Goals != null)
|
||||
foreach (var goal in levelData.Goals)
|
||||
foreach (var tile in goal.TileArea)
|
||||
exits.Add(tile);
|
||||
|
||||
return exits;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BFS from <paramref name="spawner"/>'s tile area. Returns true if any exit tile
|
||||
/// is reachable via walkable tiles. Uses the shared scratch queue and visited set.
|
||||
/// </summary>
|
||||
private bool SpawnerCanReachExit(LevelLoader loader, SpawnerData spawner,
|
||||
HashSet<Vector2Int> exitTiles)
|
||||
{
|
||||
bfsQueue.Clear();
|
||||
bfsVisited.Clear();
|
||||
|
||||
// Seed the BFS with the spawner's full tile area (not just its center tile),
|
||||
// matching bake-time P5-4 exactly.
|
||||
foreach (var tile in spawner.TileArea)
|
||||
{
|
||||
if (bfsVisited.Add(tile))
|
||||
bfsQueue.Enqueue(tile);
|
||||
}
|
||||
|
||||
while (bfsQueue.Count > 0)
|
||||
{
|
||||
var current = bfsQueue.Dequeue();
|
||||
|
||||
if (exitTiles.Contains(current))
|
||||
return true;
|
||||
|
||||
foreach (var neighbor in GridCoordinates.GetNeighbors(current))
|
||||
{
|
||||
if (bfsVisited.Contains(neighbor)) continue;
|
||||
if (!loader.IsWalkable(neighbor)) continue;
|
||||
bfsVisited.Add(neighbor);
|
||||
bfsQueue.Enqueue(neighbor);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ----- Helpers ----------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Stamps or un-stamps all tiles in <paramref name="footprint"/> on both the
|
||||
/// walkability and occupancy grids simultaneously. Always update both together.
|
||||
/// </summary>
|
||||
private static void StampFootprint(LevelLoader loader, List<Vector2Int> footprint,
|
||||
bool walkable, bool occupied)
|
||||
{
|
||||
foreach (var tile in footprint)
|
||||
{
|
||||
loader.SetWalkable(tile, walkable);
|
||||
loader.SetOccupied(tile, occupied);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns the tower NetworkObject at the footprint center and records the
|
||||
/// placing player's slot on the <see cref="TowerInstance"/> component.
|
||||
/// </summary>
|
||||
private void SpawnTower(TowerDefinition def, Vector2Int anchor, PlayerSlot owner)
|
||||
{
|
||||
if (def.TowerPrefab == null)
|
||||
{
|
||||
Debug.LogError($"[TowerPlacementManager] TowerDefinition '{def.DisplayName}' " +
|
||||
$"has no TowerPrefab assigned. Cannot spawn.");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 spawnPos = GridCoordinates.GetFootprintCenterWorld(anchor, def.FootprintSize);
|
||||
// Towers sit on the buildable plane (Y=0); raise slightly so the mesh base
|
||||
// sits flush rather than half-clipped. A cube of scale 1 needs +0.5 on Y.
|
||||
spawnPos.y = 0.5f;
|
||||
|
||||
var go = Instantiate(def.TowerPrefab, spawnPos, Quaternion.identity);
|
||||
var instance = go.GetComponent<TowerInstance>();
|
||||
if (instance == null)
|
||||
{
|
||||
Debug.LogError($"[TowerPlacementManager] TowerPrefab '{def.TowerPrefab.name}' " +
|
||||
$"is missing a TowerInstance component.");
|
||||
Destroy(go);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set data before network spawn so TowerInstance.OnNetworkSpawn sees it.
|
||||
instance.InitializeServer(def, anchor, owner);
|
||||
|
||||
var netObj = go.GetComponent<NetworkObject>();
|
||||
if (netObj == null)
|
||||
{
|
||||
Debug.LogError($"[TowerPlacementManager] TowerPrefab '{def.TowerPrefab.name}' " +
|
||||
$"is missing a NetworkObject component.");
|
||||
Destroy(go);
|
||||
return;
|
||||
}
|
||||
netObj.Spawn(destroyWithScene: true);
|
||||
}
|
||||
|
||||
// ----- Utility ----------------------------------------------------
|
||||
|
||||
private static bool TryGetDefinition(int typeId, out TowerDefinition def)
|
||||
{
|
||||
def = null;
|
||||
// typeId 0 is reserved; valid IDs start at 1.
|
||||
if (typeId <= 0) return false;
|
||||
// Instance check for the static helper path — callers that have a
|
||||
// direct reference use the instance array directly.
|
||||
if (Instance == null) return false;
|
||||
if (typeId >= Instance.towerDefinitions.Length) return false;
|
||||
def = Instance.towerDefinitions[typeId];
|
||||
return def != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a client ID to the PlayerSlot assigned to that client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// STUB: Currently uses a trivial mapping where client 0 = Player1, client 1 = Player2,
|
||||
/// etc. This will be replaced when MatchState / PlayerMatchState is implemented and
|
||||
/// carries the authoritative client-to-slot assignment.
|
||||
/// </remarks>
|
||||
private static PlayerSlot ClientIdToPlayerSlot(ulong clientId)
|
||||
{
|
||||
// NGO client IDs start at 0 (host). PlayerSlot values start at 1.
|
||||
// Cast is safe for up to 9 players; beyond that returns None.
|
||||
byte slotByte = (byte)(clientId + 1);
|
||||
if (slotByte < 1 || slotByte > 9) return PlayerSlot.None;
|
||||
return (PlayerSlot)slotByte;
|
||||
}
|
||||
|
||||
private void Reject(PlacementRequest req, PlacementRejectionReason reason)
|
||||
{
|
||||
Debug.Log($"[TowerPlacementManager] Rejected request from client " +
|
||||
$"{req.SenderClientId} at anchor {req.Anchor}: {reason}");
|
||||
|
||||
// Send the rejection RPC back to only the requesting client.
|
||||
PlacementRejectedRpc(reason,
|
||||
new RpcParams
|
||||
{
|
||||
Send = new RpcSendParams
|
||||
{
|
||||
Target = RpcTarget.Single(req.SenderClientId, RpcTargetUse.Temp)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reason codes sent to the client when the server rejects a placement request.
|
||||
/// Used by <see cref="TowerPlacementController"/> to display the appropriate
|
||||
/// feedback message to the player.
|
||||
/// </summary>
|
||||
public enum PlacementRejectionReason
|
||||
{
|
||||
/// <summary>One or more footprint tiles belong to a different player's zone.</summary>
|
||||
WrongOwner,
|
||||
|
||||
/// <summary>One or more footprint tiles are not in a Buildable state
|
||||
/// (they are Restricted or Outside the map).</summary>
|
||||
TileNotBuildable,
|
||||
|
||||
/// <summary>One or more footprint tiles are already occupied by an existing tower.</summary>
|
||||
TileOccupied,
|
||||
|
||||
/// <summary>The placing player does not have enough gold.</summary>
|
||||
InsufficientGold,
|
||||
|
||||
/// <summary>The placing player's builder is too far from the requested location.</summary>
|
||||
OutOfRange,
|
||||
|
||||
/// <summary>Placing this tower would block all valid paths from at least one
|
||||
/// spawner to its exit. The maze must remain passable.</summary>
|
||||
BlocksPath,
|
||||
|
||||
/// <summary>The requested tower type ID is not in the server's definition list.</summary>
|
||||
InvalidTowerType,
|
||||
|
||||
/// <summary>An unexpected server-side error occurred (e.g., LevelLoader not loaded,
|
||||
/// client not mapped to a PlayerSlot). Check server logs.</summary>
|
||||
ServerError,
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue