// Assets/_Project/Scripts/Editor/Levels/NewMapWindow.cs using UnityEditor; using UnityEngine; using TD.Core; namespace TD.Levels.Editor { /// /// The TD/New Map… dialog. Collects the handful of decisions only a designer can make, /// validates the resulting layout live, and hands off to . /// 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(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"); } } }