using UnityEngine; using TD.Core; #if UNITY_EDITOR using UnityEditor; #endif namespace TD.Levels { /// /// Authoring volume defining the playable map area — the bounds the builder can move within /// and the camera can pan to. Has no gameplay payload (no owner, no placement validity, no /// spawner data, no leak target). Multiple instances are allowed; their union defines the /// "map area" as referenced in the per-tile data layering spec. /// /// /// Map area = where the player's attention can go. /// Gameplay area = union of PlayerZone, Spawner, LeakExit, Goal volumes (where rules apply). /// Buffer area = map area minus gameplay area (visible terrain, no gameplay, no building). /// /// Coverage rule: every gameplay volume's tiles must be a subset of the map area. The bake /// validates this in Phase 5 (P5-12) and fails with a hard error if any gameplay volume /// pokes outside the map area. /// /// Drawn as a thicker outline only (no fill) so it reads as a boundary marker rather than /// as a translucent overlay on top of the gameplay volumes. The "always show" toggle on /// LevelAuthoring defaults to true for this volume type because the map boundary is /// generally useful to see during all authoring, not just when this volume is selected. /// public class MapAreaVolume : VolumeBase { // Map area draws BELOW player zones (which sit at 0.05). Just above the LevelAuthoring // map-bounds line (0.01) so it doesn't z-fight with that wireframe. private const float FillYLevel = 0.02f; protected override bool GetAlwaysShowToggle(LevelAuthoring authoring) { return authoring.alwaysShowMapArea; } private void OnDrawGizmosSelected() { DrawGizmosCore(); } private void OnDrawGizmos() { if (ShouldDrawAlwaysOn()) { DrawGizmosCore(); } } private void DrawGizmosCore() { Color baseColor = PlayerColors.MapArea; DrawThickRectangularOutline(baseColor, yLevel: FillYLevel); #if UNITY_EDITOR var col = Collider; if (col != null) { Vector3 labelPos = new Vector3(col.bounds.center.x, FillYLevel + 0.1f, col.bounds.center.z); Handles.Label(labelPos, "Map Area"); } #endif } // Draws the volume's tight tile rectangle as a thicker outline. Unity's Gizmos.DrawLine // has no width parameter, so we approximate thickness by drawing the rectangle three // times with small lateral offsets. The offsets are in tile units; 0.04 reads as a // visibly thicker line at typical scene-view zoom without looking like multiple lines. private void DrawThickRectangularOutline(Color outlineColor, float yLevel) { if (!TryGetTightTileRect(out Vector2Int minTile, out Vector2Int maxTile)) return; const float thickness = 0.04f; float tileSize = GridCoordinates.TILE_SIZE; // Three concentric rectangles: the original edge, slightly inset, slightly outset. DrawOutlineAtInset(minTile, maxTile, tileSize, yLevel, 0f, outlineColor); DrawOutlineAtInset(minTile, maxTile, tileSize, yLevel, +thickness, outlineColor); DrawOutlineAtInset(minTile, maxTile, tileSize, yLevel, -thickness, outlineColor); } // Tile (x, y) spans world XZ [x, x+1] (edge-aligned). The outline corners are at the // tile rect's outer edges; `inset` shifts them outward (positive) or inward (negative) // to draw the concentric thickness rectangles. private static void DrawOutlineAtInset(Vector2Int minTile, Vector2Int maxTile, float tileSize, float yLevel, float inset, Color color) { float minX = minTile.x * tileSize - inset; float maxX = (maxTile.x + 1) * tileSize + inset; float minZ = minTile.y * tileSize - inset; float maxZ = (maxTile.y + 1) * tileSize + inset; Vector3 sw = new Vector3(minX, yLevel, minZ); Vector3 se = new Vector3(maxX, yLevel, minZ); Vector3 ne = new Vector3(maxX, yLevel, maxZ); Vector3 nw = new Vector3(minX, yLevel, maxZ); Color prev = Gizmos.color; Gizmos.color = color; Gizmos.DrawLine(sw, se); Gizmos.DrawLine(se, ne); Gizmos.DrawLine(ne, nw); Gizmos.DrawLine(nw, sw); Gizmos.color = prev; } } }