Major pipeline changes to map building, introducing 3player map, and adding Shared build mode to level authoring options

This commit is contained in:
Matt F 2026-08-04 22:50:33 -07:00
parent 749c9203de
commit 2b1aee3e03
37 changed files with 5418 additions and 30 deletions

View file

@ -217,8 +217,10 @@ namespace TD.Gameplay
footprint.Add(tile);
// ------------------------------------------------------------------
// Check 1: Ownership
// Every footprint tile must be owned by the placing player's zone.
// Check 1: Build permission
// On an OwnerOnly map every footprint tile must be owned by the placing
// player's zone; on a Shared map any zone's tile will do. LevelLoader owns
// that rule so the client's ghost preview can't disagree with us.
// ------------------------------------------------------------------
PlayerSlot placingSlot = ClientIdToPlayerSlot(req.SenderClientId);
if (placingSlot == PlayerSlot.None)
@ -229,7 +231,7 @@ namespace TD.Gameplay
foreach (var tile in footprint)
{
if (loader.GetOwner(tile) != placingSlot)
if (!loader.HasBuildPermission(tile, placingSlot))
{
Reject(req, PlacementRejectionReason.WrongOwner);
return;
@ -533,37 +535,67 @@ namespace TD.Gameplay
// ----- 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.
/// Returns true if the map's mazes are still solvable with the candidate footprint in
/// place — every spawner can still reach an exit tile (any leak exit from its own zone,
/// OR any goal tile) via walkable tiles, given <c>LevelLoader</c>'s runtime 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.
/// <para>Mirrors bake-time P5-4. Reuses <see cref="bfsQueue"/> and <see cref="bfsVisited"/>
/// scratch collections (cleared between BFS runs) to avoid GC allocation per call.</para>
///
/// <para><b>Scope depends on the map's build rule.</b> Under
/// <see cref="BuildAccess.OwnerOnly"/> a player can only build inside their own zone, so
/// their footprint can only ever strand their OWN spawners — checking
/// <paramref name="placingSlot"/>'s zone alone is both sufficient and cheapest. Under
/// <see cref="BuildAccess.Shared"/> anyone can wall off anyone's lane, so every zone has to
/// be checked or a player could strand a teammate's spawner and leave its enemies with no
/// route at all.</para>
/// </remarks>
private bool CheckPathValidity(LevelLoader loader, PlayerSlot slot,
private bool CheckPathValidity(LevelLoader loader, PlayerSlot placingSlot,
HashSet<Vector2Int> virtualBlocked = null)
{
var levelData = loader.LevelData;
if (levelData?.PlayerZones == null) return true;
if (loader.BuildAccess == BuildAccess.Shared)
{
foreach (var zone in levelData.PlayerZones)
{
if (!ZoneSpawnersCanReachExit(loader, levelData, zone, virtualBlocked))
return false;
}
return true;
}
// Find the PlayerZoneData for this slot.
PlayerZoneData zoneData = null;
foreach (var zone in levelData.PlayerZones)
{
if (zone.Owner == slot) { zoneData = zone; break; }
if (zone.Owner == placingSlot)
return ZoneSpawnersCanReachExit(loader, levelData, zone, virtualBlocked);
}
if (zoneData == null || zoneData.Spawners == null || zoneData.Spawners.Length == 0)
// Placing player has no zone — nothing of theirs to strand.
return true;
}
/// <summary>
/// Runs the reachability BFS for every spawner in one zone. Returns true if they can all
/// still reach an exit (or the zone has no spawners to strand).
/// </summary>
private bool ZoneSpawnersCanReachExit(LevelLoader loader, LevelData levelData,
PlayerZoneData zoneData,
HashSet<Vector2Int> virtualBlocked)
{
if (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.
var exitTiles = BuildExitTileSet(levelData, slot);
// Build the exit tile set: this zone's leak exits, plus all goal tiles.
var exitTiles = BuildExitTileSet(levelData, zoneData.Owner);
if (exitTiles.Count == 0)
{
Debug.LogWarning($"[TowerPlacementManager] Zone {slot} has no exit tiles. " +
Debug.LogWarning($"[TowerPlacementManager] Zone {zoneData.Owner} has no exit tiles. " +
$"This should have been caught at bake time (P5-8).");
return true;
}