250 lines
No EOL
12 KiB
C#
250 lines
No EOL
12 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using TD.Core;
|
|
|
|
#if UNITY_EDITOR
|
|
using UnityEditor;
|
|
#endif
|
|
|
|
namespace TD.Levels
|
|
{
|
|
/// <summary>
|
|
/// Scene-level coordinator for level authoring. One per scene, attached to an empty
|
|
/// GameObject named <c>_LevelAuthoring</c> whose subtree contains all the volumes for the map.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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
|
|
/// <see cref="targetAsset"/> 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.
|
|
/// </remarks>
|
|
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;
|
|
|
|
[Tooltip("If true, MapAreaVolume gizmos draw whether or not the volume is selected. " +
|
|
"Defaults to true — the map boundary is generally useful to see while authoring " +
|
|
"the rest of the map, not just when the MapAreaVolume itself is selected.")]
|
|
public bool alwaysShowMapArea = true;
|
|
|
|
// -------------------------------------------------------------------
|
|
// 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<VolumeBase>(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<BoxCollider>() : 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<PlayerZoneVolume>(includeInactive: false);
|
|
if (zones == null || zones.Length == 0) return;
|
|
|
|
// Build per-owner tile sets.
|
|
var perOwner = new Dictionary<PlayerSlot, HashSet<Vector2Int>>();
|
|
for (int i = 0; i < zones.Length; i++)
|
|
{
|
|
var z = zones[i];
|
|
if (z == null) continue;
|
|
|
|
var col = z.GetComponent<BoxCollider>();
|
|
if (col == null) continue;
|
|
|
|
if (!perOwner.TryGetValue(z.owner, out var set))
|
|
{
|
|
set = new HashSet<Vector2Int>();
|
|
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;
|
|
}
|
|
}
|
|
} |