UnityTowerDefense/Assets/_Project/Scripts/Editor/Levels/NewMapWindow.cs

205 lines
9.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Assets/_Project/Scripts/Editor/Levels/NewMapWindow.cs
using UnityEditor;
using UnityEngine;
using TD.Core;
namespace TD.Levels.Editor
{
/// <summary>
/// The <c>TD/New Map…</c> dialog. Collects the handful of decisions only a designer can make,
/// validates the resulting layout live, and hands off to <see cref="NewMapBuilder"/>.
/// </summary>
public class NewMapWindow : EditorWindow
{
// Mirrors LevelBakePipeline's accepted set — anything else is a hard bake error (P2-15),
// so there's no point offering it here.
private static readonly int[] PlayerCountValues = { 1, 2, 3, 4, 5, 9 };
private static readonly string[] PlayerCountLabels =
{ "1 player", "2 players", "3 players", "4 players", "5 players", "9 players" };
private const string AuthorPrefKey = "TD.MapPipeline.LastAuthor";
private readonly NewMapBuilder.Request request = new NewMapBuilder.Request();
private bool initialized;
private Vector2 scroll;
[MenuItem("TD/New Map...", false, 100)]
public static void Open()
{
var window = GetWindow<NewMapWindow>(utility: true, title: "New Map", focus: true);
window.minSize = new Vector2(420f, 460f);
window.Show();
}
private void Initialize()
{
if (initialized) return;
initialized = true;
var template = MapTemplate.LoadOrCreate();
request.MapWidth = template.defaultMapWidth;
request.MapHeight = template.defaultMapHeight;
request.Author = EditorPrefs.GetString(AuthorPrefKey, "");
}
private void OnGUI()
{
Initialize();
var template = MapTemplate.LoadOrCreate();
scroll = EditorGUILayout.BeginScrollView(scroll);
bool prefabsReady = template.matchServicesPrefab != null
&& template.matchNetworkPrefab != null;
if (!prefabsReady)
{
EditorGUILayout.HelpBox(
"Core prefabs haven't been built yet.\n\n" +
"Open the 9Player scene, then run:\n" +
"TD → Map Pipeline → Build Core Prefabs From Open Scene",
MessageType.Error);
EditorGUILayout.Space(6);
}
EditorGUILayout.LabelField("Identity", EditorStyles.boldLabel);
request.MapName = EditorGUILayout.TextField(
new GUIContent("Map Name",
"Becomes the scene file name, the LevelData asset name, and the string NGO " +
"uses to load the scene. Letters, digits and underscores only."),
request.MapName);
request.Author = EditorGUILayout.TextField("Author", request.Author);
EditorGUILayout.LabelField(new GUIContent("Description",
"Shown in the lobby's map browser. Empty produces a bake warning."));
request.Description = EditorGUILayout.TextArea(
request.Description, GUILayout.Height(38f));
EditorGUILayout.Space(8);
EditorGUILayout.LabelField("Shape", EditorStyles.boldLabel);
request.PlayerCount = EditorGUILayout.IntPopup(
"Players", request.PlayerCount, PlayerCountLabels, PlayerCountValues);
request.MapWidth = Mathf.Max(8, EditorGUILayout.IntField(
new GUIContent("Width (tiles)", "1 tile = 1 world unit."), request.MapWidth));
request.MapHeight = Mathf.Max(8, EditorGUILayout.IntField(
new GUIContent("Height (tiles)", "1 tile = 1 world unit."), request.MapHeight));
request.BuildAccess = (BuildAccess)EditorGUILayout.EnumPopup(
new GUIContent("Build Access",
"OwnerOnly: each player builds only in their own zone.\n" +
"Shared: everyone builds anywhere on the map's buildable area.\n\n" +
"Changeable later on _LevelAuthoring — it just needs a re-bake."),
request.BuildAccess);
if (request.BuildAccess == BuildAccess.Shared)
{
EditorGUILayout.HelpBox(
"Shared: every band forms one cooperative maze. Zones still define " +
"territory for leak attribution, camera start and builder spawn. Towers stay " +
"owned by whoever paid — only that player can sell or upgrade them.",
MessageType.None);
}
EditorGUILayout.Space(8);
EditorGUILayout.LabelField("On Create", EditorStyles.boldLabel);
request.CreateTerrain = EditorGUILayout.Toggle(
new GUIContent("Create Terrain",
"Adds a Terrain sized to the map plus a skirt, on the TerrainGeometry layer, " +
"flattened onto the buildable plane."),
request.CreateTerrain);
request.AddToBuildSettings = EditorGUILayout.Toggle(
new GUIContent("Add to Build Settings",
"Required — NGO cannot load a scene that isn't in the build list."),
request.AddToBuildSettings);
request.AddToMapRegistry = EditorGUILayout.Toggle(
new GUIContent("Add to MapRegistry",
"Opens the MainMenu scene, appends the LevelData to the lobby's map browser, " +
"and saves it."),
request.AddToMapRegistry);
request.BakeImmediately = EditorGUILayout.Toggle(
new GUIContent("Bake immediately",
"Runs the bake on the generated starter layout so the map is valid and " +
"playable the moment it's created."),
request.BakeImmediately);
// Registry wiring is the one step that reaches into a scene the designer isn't looking
// at, so a broken reference should be visible BEFORE Create, not in the summary after.
if (request.AddToMapRegistry && !System.IO.File.Exists(template.MainMenuScenePath))
{
EditorGUILayout.HelpBox(
"Main Menu Scene isn't resolving, so the new map won't be added to the lobby's " +
"map browser. Assign it on MapTemplate, or add the LevelData to MapRegistry.Maps " +
"by hand afterwards.",
MessageType.Warning);
}
EditorGUILayout.Space(8);
DrawLayoutPreview(template);
EditorGUILayout.EndScrollView();
EditorGUILayout.Space(6);
bool canCreate = prefabsReady &&
NewMapBuilder.TryComputeLayout(
template, request.MapWidth, request.MapHeight,
request.PlayerCount, request.BuildAccess, out _, out _);
using (new EditorGUI.DisabledScope(!canCreate))
{
if (GUILayout.Button("Create Map", GUILayout.Height(30f)))
{
CreateMap();
}
}
}
private void DrawLayoutPreview(MapTemplate template)
{
EditorGUILayout.LabelField("Starter Layout", EditorStyles.boldLabel);
if (!NewMapBuilder.TryComputeLayout(template, request.MapWidth, request.MapHeight,
request.PlayerCount, request.BuildAccess,
out var layout, out string error))
{
EditorGUILayout.HelpBox(error, MessageType.Error);
return;
}
string routing = layout.Leaks.Length > 0
? $"through {layout.Leaks.Length} one-tile leak gate(s) " +
$"{Mathf.Min(template.leakGateWidth, layout.Zones[0].width)} tiles wide"
: "across directly-adjacent band borders (no leak gates)";
EditorGUILayout.HelpBox(
$"{request.PlayerCount} full-width band(s), {layout.BandDepth} tiles deep each, " +
$"stacked south to north.\n" +
$"Each band gets a spawner at its south edge; enemies travel north " +
$"{routing} to the goal.\n\n" +
$"Grid will bake to {layout.MapArea.width}×{layout.MapArea.height} with its " +
$"south-west corner at tile (0, 0).",
MessageType.Info);
EditorGUILayout.LabelField(
"Tune the starter proportions on the MapTemplate asset:",
EditorStyles.miniLabel);
if (GUILayout.Button("Select MapTemplate", EditorStyles.miniButton))
{
Selection.activeObject = template;
EditorGUIUtility.PingObject(template);
}
}
private void CreateMap()
{
EditorPrefs.SetString(AuthorPrefKey, request.Author ?? "");
if (NewMapBuilder.Create(request, out string error))
{
Close();
return;
}
EditorUtility.DisplayDialog("Could not create map", error, "OK");
}
}
}