using System.Collections.Generic; using UnityEngine; using TD.Core; #if UNITY_EDITOR using UnityEditor; #endif namespace TD.Levels { /// /// Scene-level coordinator for level authoring. One per scene, attached to an empty /// GameObject named _LevelAuthoring whose subtree contains all the volumes for the map. /// /// /// LevelAuthoring is the entry point for the bake operation: the custom inspector exposes a /// "Bake LevelData" button that triggers the seven-phase algorithm, writing into the /// ScriptableObject. /// /// This class also owns the per-volume-type "always show gizmos" toggles. Each volume walks /// its parent hierarchy to find this LevelAuthoring and reads the relevant toggle. /// /// In the current session the bake methods are stubs — only the field schema and gizmo /// rendering are implemented. The full bake script is the next session's work. /// public class LevelAuthoring : MonoBehaviour { // ------------------------------------------------------------------- // Bake target and lobby-facing metadata. // ------------------------------------------------------------------- [Header("Bake Target")] [Tooltip("The ScriptableObject this scene bakes into. Must be set before baking.")] public LevelData targetAsset; [Header("Map Metadata")] [Tooltip("Display name for the lobby UI.")] public string mapName = ""; [Tooltip("Number of players this map is designed for. Used by the lobby to filter maps " + "and by bake validation. Allowed values: 1, 2, 3, 4, 5, 9.")] public int playerCount = 1; [Tooltip("Expected number of GoalVolume instances. Bake validates the count matches.")] public int expectedGoalCount = 1; [Tooltip("Optional description for the lobby UI. Empty triggers a soft warning at bake time.")] [TextArea(2, 5)] public string mapDescription = ""; [Tooltip("Optional author/credit text. Empty triggers a soft warning at bake time.")] public string author = ""; // ------------------------------------------------------------------- // Gizmo visibility toggles. Each volume type reads its own toggle to decide whether to // draw always-on or only-when-selected. // ------------------------------------------------------------------- [Header("Gizmo Visibility (Always-Show Toggles)")] [Tooltip("If true, PlayerZoneVolume gizmos draw whether or not the volume is selected.")] public bool alwaysShowPlayerZones = false; [Tooltip("If true, SpawnerVolume gizmos draw whether or not the volume is selected.")] public bool alwaysShowSpawners = false; [Tooltip("If true, LeakExitVolume gizmos draw whether or not the volume is selected.")] public bool alwaysShowLeakExits = false; [Tooltip("If true, GoalVolume gizmos draw whether or not the volume is selected.")] public bool alwaysShowGoals = false; // ------------------------------------------------------------------- // Bake API // ------------------------------------------------------------------- // The bake operation is implemented in TD.Levels.Editor.LevelBakePipeline (in the Editor // assembly). It cannot be exposed as a method on this runtime class because the runtime // assembly cannot reference types in the Editor assembly. The custom inspector's "Bake // LevelData" button invokes the pipeline directly. // ------------------------------------------------------------------- // Map-level gizmos: origin marker, map bounding rect, combined player zone outlines. // Always draw when LevelAuthoring is in the scene (no per-toggle gating for these). // ------------------------------------------------------------------- private const float OriginMarkerY = 0.005f; private const float MapBoundsY = 0.01f; private const float CombinedZoneOutlineY = 0.03f; private const float OriginMarkerSize = 0.4f; private void OnDrawGizmos() { DrawOriginMarker(); DrawMapBoundingRect(); DrawCombinedPlayerZoneOutlines(); } private void DrawOriginMarker() { Color prev = Gizmos.color; Gizmos.color = Color.white; // A small "+" cross at world (0,0) plus a sphere for visibility. Vector3 origin = new Vector3(0f, OriginMarkerY, 0f); Gizmos.DrawSphere(origin, 0.1f); Gizmos.DrawLine(origin + new Vector3(-OriginMarkerSize, 0f, 0f), origin + new Vector3(OriginMarkerSize, 0f, 0f)); Gizmos.DrawLine(origin + new Vector3(0f, 0f, -OriginMarkerSize), origin + new Vector3(0f, 0f, OriginMarkerSize)); Gizmos.color = prev; #if UNITY_EDITOR Handles.Label(origin + new Vector3(0.15f, 0.05f, 0.15f), "World Origin (0,0)"); #endif } private void DrawMapBoundingRect() { // Find every VolumeBase in this LevelAuthoring's subtree (matches the bake's scoped scan) // and union their tile bounding rectangles. var volumes = GetComponentsInChildren(includeInactive: false); if (volumes == null || volumes.Length == 0) return; bool initialized = false; Vector2Int minTile = Vector2Int.zero; Vector2Int maxTile = Vector2Int.zero; for (int i = 0; i < volumes.Length; i++) { var col = volumes[i] != null ? volumes[i].GetComponent() : null; if (col == null) continue; if (!VolumeBase.TryGetTightTileRect(col.bounds, out Vector2Int vMin, out Vector2Int vMax)) { continue; // volume covers no tiles (e.g., bounds don't intersect Y=0) } if (!initialized) { minTile = vMin; maxTile = vMax; initialized = true; } else { if (vMin.x < minTile.x) minTile.x = vMin.x; if (vMin.y < minTile.y) minTile.y = vMin.y; if (vMax.x > maxTile.x) maxTile.x = vMax.x; if (vMax.y > maxTile.y) maxTile.y = vMax.y; } } if (!initialized) return; // Convert tile-corner indices to world-space corners. Tiles are center-based with // TILE_SIZE = 1, so a tile at (x,y) spans world XZ from (x-0.5, y-0.5) to (x+0.5, y+0.5). float halfTile = GridCoordinates.TILE_SIZE * 0.5f; Vector3 sw = new Vector3(minTile.x - halfTile, MapBoundsY, minTile.y - halfTile); Vector3 se = new Vector3(maxTile.x + halfTile, MapBoundsY, minTile.y - halfTile); Vector3 ne = new Vector3(maxTile.x + halfTile, MapBoundsY, maxTile.y + halfTile); Vector3 nw = new Vector3(minTile.x - halfTile, MapBoundsY, maxTile.y + halfTile); Color prev = Gizmos.color; Gizmos.color = new Color(1f, 1f, 1f, 0.6f); // muted white Gizmos.DrawLine(sw, se); Gizmos.DrawLine(se, ne); Gizmos.DrawLine(ne, nw); Gizmos.DrawLine(nw, sw); // Map SW corner marker — a small filled square so it reads distinctly from the // world-origin sphere+cross when both are visible. Sized slightly above the bounds // line so it doesn't z-fight. Vector3 swMarkerCenter = new Vector3(sw.x, MapBoundsY + 0.005f, sw.z); Gizmos.color = new Color(1f, 0.85f, 0.2f, 0.9f); // amber, distinct from origin's white Gizmos.DrawCube(swMarkerCenter, new Vector3(0.25f, 0.01f, 0.25f)); Gizmos.color = prev; #if UNITY_EDITOR // Place label slightly inside the map bounds (NE of the corner) so it doesn't get // covered by the world-origin label when the map is aligned. Handles.Label(swMarkerCenter + new Vector3(0.2f, 0.05f, 0.2f), $"Map SW: tile ({minTile.x}, {minTile.y})"); #endif } private void DrawCombinedPlayerZoneOutlines() { // Group all PlayerZoneVolumes in this subtree by owner, then for each owner compute // the union of their tile coverage and draw the perimeter using the general // (non-rectangular) algorithm. This visualizes L-shaped multi-volume zones as a // single unified outline. var zones = GetComponentsInChildren(includeInactive: false); if (zones == null || zones.Length == 0) return; // Build per-owner tile sets. var perOwner = new Dictionary>(); for (int i = 0; i < zones.Length; i++) { var z = zones[i]; if (z == null) continue; var col = z.GetComponent(); if (col == null) continue; if (!perOwner.TryGetValue(z.owner, out var set)) { set = new HashSet(); perOwner[z.owner] = set; } // Use the shared rasterizer so this stays in lock-step with what the bake will see. VolumeBase.RasterizeBoundsToTiles(col.bounds, t => set.Add(t)); } // For each owner, draw perimeter using the general algorithm. float halfTile = GridCoordinates.TILE_SIZE * 0.5f; Color prev = Gizmos.color; foreach (var kv in perOwner) { Color outlineColor = PlayerColors.Get(kv.Key); Gizmos.color = outlineColor; foreach (var tile in kv.Value) { // World-space corners of this tile. Vector3 sw = new Vector3(tile.x - halfTile, CombinedZoneOutlineY, tile.y - halfTile); Vector3 se = new Vector3(tile.x + halfTile, CombinedZoneOutlineY, tile.y - halfTile); Vector3 ne = new Vector3(tile.x + halfTile, CombinedZoneOutlineY, tile.y + halfTile); Vector3 nw = new Vector3(tile.x - halfTile, CombinedZoneOutlineY, tile.y + halfTile); // For each of the four edges, draw it ONLY if the neighbor across that edge // is not in the covered set (i.e., the edge is on the perimeter). if (!kv.Value.Contains(new Vector2Int(tile.x, tile.y - 1))) Gizmos.DrawLine(sw, se); // south edge if (!kv.Value.Contains(new Vector2Int(tile.x + 1, tile.y))) Gizmos.DrawLine(se, ne); // east edge if (!kv.Value.Contains(new Vector2Int(tile.x, tile.y + 1))) Gizmos.DrawLine(ne, nw); // north edge if (!kv.Value.Contains(new Vector2Int(tile.x - 1, tile.y))) Gizmos.DrawLine(nw, sw); // west edge } } Gizmos.color = prev; } } }