Major pipeline changes to map building, introducing 3player map, and adding Shared build mode to level authoring options

This commit is contained in:
Matt F 2026-08-04 22:50:33 -07:00
parent 749c9203de
commit 2b1aee3e03
37 changed files with 5418 additions and 30 deletions

View file

@ -0,0 +1,205 @@
// 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");
}
}
}