using UnityEngine; using TD.Core; #if UNITY_EDITOR using UnityEditor; #endif namespace TD.Levels { /// /// Authoring volume marking where enemies spawn into a player's zone. Spawner tiles are /// in the baked grid (no tower placement allowed) /// but remain walkable so enemies can leave the spawner. /// /// /// A zone may have multiple spawners; disambiguates them. /// Player 5 in the 9-player Wintermaul map is the canonical multi-spawner zone. /// /// The spawner declares an owning player via rather than relying on /// spatial containment within a PlayerZoneVolume. The bake validates (with a soft warning) /// that the spawner's tiles fall inside its declared owner's zone. /// public class SpawnerVolume : VolumeBase { [Tooltip("Which player's zone owns this spawner. Explicit — not inferred from spatial overlap.")] public PlayerSlot owner = PlayerSlot.Player1; [Tooltip("Disambiguator for zones with multiple spawners. Should be 0 for single-spawner zones, " + "and contiguous starting from 0 for multi-spawner zones (e.g., 0 and 1).")] public int spawnerIdInZone = 0; [Tooltip("Direction enemies face when they spawn. Used for visual orientation and may bias " + "initial enemy movement direction.")] public Direction spawnFacing = Direction.South; [Tooltip("Whether tiles in this volume are buildable. Defaults to Invalid (no placement on spawners).")] public PlacementValidity placementValidity = PlacementValidity.Invalid; // Spawners draw above player zones so the player zone color reads as background. private const float FillYLevel = 0.06f; private const float ArrowYLevel = 0.07f; private const float ArrowLength = 1.5f; // 1.5 tiles per gizmo design protected override bool GetAlwaysShowToggle(LevelAuthoring authoring) { return authoring.alwaysShowSpawners; } private void OnDrawGizmosSelected() { DrawGizmosCore(); } private void OnDrawGizmos() { if (ShouldDrawAlwaysOn()) { DrawGizmosCore(); } } private void DrawGizmosCore() { Color baseColor = PlayerColors.Get(owner); DrawTileCoverageFill(baseColor, alpha: 0.40f, yLevel: FillYLevel); DrawRectangularOutline(baseColor, yLevel: FillYLevel); // Direction arrow originates from the volume's center and extends 1.5 tiles in the // declared facing direction, rendered in opaque owner color. var col = Collider; if (col != null) { Vector3 arrowOrigin = new Vector3(col.bounds.center.x, ArrowYLevel, col.bounds.center.z); Vector3 arrowDir = DirectionToWorld(spawnFacing); DrawArrow(arrowOrigin, arrowDir, ArrowLength, baseColor); } #if UNITY_EDITOR // Label conditional on multi-spawner status: // - Single-spawner zone: "Player N Spawn" // - Multi-spawner zone: "Player N Spawn 0", "Player N Spawn 1", etc. // Multi-spawner detection scans other SpawnerVolumes in the scene with the same owner. // Same posture as the leak-exit target lookup: cheap on realistic maps, can be cached // if it ever shows up as an editor-perf issue. if (col != null) { Vector3 labelPos = new Vector3(col.bounds.center.x, ArrowYLevel + 0.1f, col.bounds.center.z); string labelText = ZoneHasMultipleSpawners() ? $"{FormatPlayerName(owner)} Spawn {spawnerIdInZone}" : $"{FormatPlayerName(owner)} Spawn"; Handles.Label(labelPos, labelText); } #endif } /// /// Returns true if more than one active SpawnerVolume in the scene shares this spawner's /// . Used to decide whether to suffix the gizmo label with the spawner ID. /// private bool ZoneHasMultipleSpawners() { var spawners = Object.FindObjectsByType(FindObjectsInactive.Exclude); if (spawners == null) return false; int count = 0; for (int i = 0; i < spawners.Length; i++) { if (spawners[i] == null) continue; if (spawners[i].owner == owner) { count++; if (count > 1) return true; } } return false; } } }