292 lines
13 KiB
C#
292 lines
13 KiB
C#
// Assets/_Project/Scripts/Editor/Levels/MapTemplate.cs
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
namespace TD.Levels.Editor
|
|
{
|
|
/// <summary>
|
|
/// Editor-only settings asset that drives <c>TD/New Map…</c>. Holds the two core prefabs
|
|
/// produced by <see cref="MapCoreExtractor"/> plus every default the new-map generator needs,
|
|
/// so tuning the starter layout is an inspector edit rather than a code change.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Lives in the Editor assembly deliberately — nothing at runtime reads it, and keeping the
|
|
/// type editor-only stops it from being pulled into a player build.
|
|
///
|
|
/// The asset is created on demand at <see cref="AssetPath"/>. If it goes missing, both
|
|
/// <c>TD/New Map…</c> and the extractor recreate it with defaults; only the prefab references
|
|
/// and captured terrain settings are lost, and re-running the extractor restores those.
|
|
/// </remarks>
|
|
public class MapTemplate : ScriptableObject
|
|
{
|
|
public const string AssetPath = "Assets/_Project/Settings/MapTemplate.asset";
|
|
|
|
// -------------------------------------------------------------------
|
|
// Core prefabs. Populated by MapCoreExtractor; consumed by NewMapBuilder.
|
|
// -------------------------------------------------------------------
|
|
|
|
[Header("Core Prefabs (built by TD/Map Pipeline/Build Core Prefabs)")]
|
|
[Tooltip("Every map-independent, non-networked service collapsed onto one GameObject " +
|
|
"(plus HUD / DebugConsole / CameraRig / light children). Instantiated as a LINKED " +
|
|
"prefab instance so edits here propagate to every map.")]
|
|
public GameObject matchServicesPrefab;
|
|
|
|
[Tooltip("The six scene-placed NetworkBehaviours, parented under one root. Instantiated " +
|
|
"and immediately UNPACKED so the scene contains plain GameObjects — network " +
|
|
"topology stays byte-identical to hand-authored maps.")]
|
|
public GameObject matchNetworkPrefab;
|
|
|
|
// -------------------------------------------------------------------
|
|
// Output locations.
|
|
// -------------------------------------------------------------------
|
|
|
|
[Header("Output Locations")]
|
|
[Tooltip("Folder new map scenes, their LevelData, and their TerrainData are written to. " +
|
|
"Drag the folder itself here — an asset reference follows renames and moves, " +
|
|
"which a path string does not.")]
|
|
public DefaultAsset sceneFolderAsset;
|
|
|
|
[Tooltip("Scene holding the MapRegistry that the lobby's map browser reads. New maps are " +
|
|
"appended to its Maps array automatically.")]
|
|
public SceneAsset mainMenuScene;
|
|
|
|
// Legacy path strings, superseded by the asset references above. Kept so an existing
|
|
// template migrates itself (see MigratePathsToAssets) and as a last resort if a reference
|
|
// is cleared. Hidden so there's exactly one obvious place to edit.
|
|
[HideInInspector] public string sceneFolder = "Assets/_Project/Scenes/Levels";
|
|
[HideInInspector] public string mainMenuScenePath = "Assets/_Project/Scenes/UI/MainMenu.unity";
|
|
|
|
/// <summary>
|
|
/// Folder new maps are written to. Prefers <see cref="sceneFolderAsset"/>; falls back to the
|
|
/// legacy path string if the reference is unset or doesn't point at a folder.
|
|
/// </summary>
|
|
public string SceneFolderPath
|
|
{
|
|
get
|
|
{
|
|
if (sceneFolderAsset != null)
|
|
{
|
|
string path = AssetDatabase.GetAssetPath(sceneFolderAsset);
|
|
if (AssetDatabase.IsValidFolder(path)) return path;
|
|
|
|
Debug.LogWarning($"[MapPipeline] MapTemplate's Scene Folder Asset points at " +
|
|
$"'{path}', which is not a folder. Falling back to '{sceneFolder}'.");
|
|
}
|
|
return sceneFolder;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scene containing the MapRegistry. Prefers <see cref="mainMenuScene"/>; falls back to the
|
|
/// legacy path string. Returns empty if neither resolves.
|
|
/// </summary>
|
|
public string MainMenuScenePath
|
|
{
|
|
get
|
|
{
|
|
if (mainMenuScene != null)
|
|
{
|
|
string path = AssetDatabase.GetAssetPath(mainMenuScene);
|
|
if (!string.IsNullOrEmpty(path)) return path;
|
|
}
|
|
return mainMenuScenePath ?? string.Empty;
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Terrain defaults. Captured from the reference scene by the extractor.
|
|
// -------------------------------------------------------------------
|
|
|
|
[Header("Terrain Defaults")]
|
|
[Tooltip("Material applied to new terrain. Captured from the reference scene's Terrain.")]
|
|
public Material terrainMaterial;
|
|
|
|
[Tooltip("Terrain layers (splat textures) applied to new terrain.")]
|
|
public TerrainLayer[] terrainLayers;
|
|
|
|
[Tooltip("Heightmap resolution. Must be 2^n + 1 (129, 257, 513, 1025…).")]
|
|
public int terrainHeightmapResolution = 513;
|
|
|
|
[Tooltip("Total vertical range of the terrain in world units.")]
|
|
public float terrainHeight = 60f;
|
|
|
|
[Tooltip("How far the terrain extends past the map bounds on each side, in tiles. Gives " +
|
|
"the camera something to look at beyond the playable area.")]
|
|
public int terrainSkirtTiles = 20;
|
|
|
|
[Tooltip("Terrain is created flattened to this height above its own base, and its transform " +
|
|
"is dropped by the same amount — so the flat surface lands exactly on the buildable " +
|
|
"plane (Y=0) while leaving headroom to carve DOWN as well as up. Set 0 to match the " +
|
|
"older 9Player setup (surface at the very bottom of the height range).")]
|
|
public float terrainCarveDepth = 5f;
|
|
|
|
// -------------------------------------------------------------------
|
|
// Starter layout. All values in tiles.
|
|
// -------------------------------------------------------------------
|
|
|
|
[Header("Starter Layout (tiles)")]
|
|
[Tooltip("Default map width for the New Map dialog.")]
|
|
public int defaultMapWidth = 60;
|
|
|
|
[Tooltip("Default map height for the New Map dialog.")]
|
|
public int defaultMapHeight = 70;
|
|
|
|
[Tooltip("Gap between the map edge (MapAreaVolume) and the gameplay volumes. The map area " +
|
|
"must fully contain every gameplay volume, and the buffer gives the camera room " +
|
|
"to pan past the playfield.")]
|
|
public int playAreaMargin = 4;
|
|
|
|
[Tooltip("Depth (north-south) of the GoalVolume.")]
|
|
public int goalDepth = 5;
|
|
|
|
[Tooltip("Width (east-west) of the GoalVolume.")]
|
|
public int goalWidth = 10;
|
|
|
|
[Tooltip("Depth of each SpawnerVolume, measured from the south edge of its owner's band.")]
|
|
public int spawnerDepth = 3;
|
|
|
|
[Tooltip("Width of each SpawnerVolume.")]
|
|
public int spawnerWidth = 10;
|
|
|
|
[Tooltip("Width of the one-tile-deep LeakExitVolume gate between adjacent player bands.")]
|
|
public int leakGateWidth = 8;
|
|
|
|
[Tooltip("Smallest acceptable north-south depth for a player band. The New Map dialog " +
|
|
"refuses to generate a layout thinner than this and tells you the map height needed.")]
|
|
public int minBandDepth = 8;
|
|
|
|
// -------------------------------------------------------------------
|
|
// Loading
|
|
// -------------------------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Returns the template asset, creating it with defaults if it doesn't exist yet.
|
|
/// Never returns null.
|
|
/// </summary>
|
|
public static MapTemplate LoadOrCreate()
|
|
{
|
|
var existing = AssetDatabase.LoadAssetAtPath<MapTemplate>(AssetPath);
|
|
if (existing != null)
|
|
{
|
|
existing.MigratePathsToAssets();
|
|
return existing;
|
|
}
|
|
|
|
EnsureFolder(System.IO.Path.GetDirectoryName(AssetPath).Replace("\\", "/"));
|
|
|
|
var created = CreateInstance<MapTemplate>();
|
|
AssetDatabase.CreateAsset(created, AssetPath);
|
|
created.MigratePathsToAssets();
|
|
AssetDatabase.SaveAssets();
|
|
Debug.Log($"[MapPipeline] Created default map template at '{AssetPath}'.");
|
|
return created;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Upgrades a template that still stores locations as path strings, resolving each to an
|
|
/// asset reference. Idempotent and cheap — safe to call on every load.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If the recorded path no longer resolves (the folder was renamed out from under it, which
|
|
/// is exactly the failure the asset references exist to prevent), falls back to searching
|
|
/// the project for a uniquely-named match. An ambiguous match is left unresolved on purpose:
|
|
/// guessing which of several same-named scenes holds the MapRegistry would be worse than
|
|
/// asking.
|
|
/// </remarks>
|
|
private void MigratePathsToAssets()
|
|
{
|
|
bool changed = false;
|
|
|
|
if (sceneFolderAsset == null && !string.IsNullOrEmpty(sceneFolder)
|
|
&& AssetDatabase.IsValidFolder(sceneFolder))
|
|
{
|
|
sceneFolderAsset = AssetDatabase.LoadAssetAtPath<DefaultAsset>(sceneFolder);
|
|
changed |= sceneFolderAsset != null;
|
|
}
|
|
|
|
if (mainMenuScene == null && !string.IsNullOrEmpty(mainMenuScenePath))
|
|
{
|
|
var scene = AssetDatabase.LoadAssetAtPath<SceneAsset>(mainMenuScenePath);
|
|
|
|
if (scene == null)
|
|
{
|
|
string wanted = System.IO.Path.GetFileNameWithoutExtension(mainMenuScenePath);
|
|
scene = FindUniqueSceneNamed(wanted);
|
|
if (scene != null)
|
|
{
|
|
Debug.Log($"[MapPipeline] MapTemplate's recorded Main Menu path " +
|
|
$"'{mainMenuScenePath}' no longer exists; relinked to " +
|
|
$"'{AssetDatabase.GetAssetPath(scene)}'.");
|
|
}
|
|
}
|
|
|
|
if (scene != null)
|
|
{
|
|
mainMenuScene = scene;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (changed)
|
|
{
|
|
EditorUtility.SetDirty(this);
|
|
AssetDatabase.SaveAssets();
|
|
}
|
|
}
|
|
|
|
// Returns the one SceneAsset whose file name matches exactly, or null if there are zero or
|
|
// several. Name search only — the caller has already failed to resolve by path.
|
|
private static SceneAsset FindUniqueSceneNamed(string sceneName)
|
|
{
|
|
if (string.IsNullOrEmpty(sceneName)) return null;
|
|
|
|
string[] guids = AssetDatabase.FindAssets($"{sceneName} t:SceneAsset");
|
|
SceneAsset first = null;
|
|
int matches = 0;
|
|
|
|
foreach (string guid in guids)
|
|
{
|
|
string path = AssetDatabase.GUIDToAssetPath(guid);
|
|
if (!string.Equals(System.IO.Path.GetFileNameWithoutExtension(path), sceneName,
|
|
System.StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
continue; // FindAssets does substring matching; we want an exact file name.
|
|
}
|
|
|
|
matches++;
|
|
if (first == null) first = AssetDatabase.LoadAssetAtPath<SceneAsset>(path);
|
|
}
|
|
|
|
if (matches > 1)
|
|
{
|
|
Debug.LogWarning($"[MapPipeline] Found {matches} scenes named '{sceneName}'. " +
|
|
"Assign Main Menu Scene on MapTemplate manually.");
|
|
return null;
|
|
}
|
|
|
|
return first;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates <paramref name="folder"/> and any missing ancestors. Path is asset-relative and
|
|
/// uses forward slashes ("Assets/_Project/Prefabs/MatchCore"). No-op if it already exists.
|
|
/// </summary>
|
|
public static void EnsureFolder(string folder)
|
|
{
|
|
if (string.IsNullOrEmpty(folder) || AssetDatabase.IsValidFolder(folder)) return;
|
|
|
|
string[] parts = folder.Split('/');
|
|
// parts[0] is always "Assets" for a valid asset path; walk down creating as needed.
|
|
string running = parts[0];
|
|
for (int i = 1; i < parts.Length; i++)
|
|
{
|
|
string next = running + "/" + parts[i];
|
|
if (!AssetDatabase.IsValidFolder(next))
|
|
{
|
|
AssetDatabase.CreateFolder(running, parts[i]);
|
|
}
|
|
running = next;
|
|
}
|
|
}
|
|
}
|
|
}
|