205 lines
9.1 KiB
C#
205 lines
9.1 KiB
C#
// 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");
|
||
}
|
||
}
|
||
}
|