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;
}
}
}