using System.Collections.Generic;
using TMPro;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using BaseGames.UI;
using BaseGames.UI.Menus;
using BaseGames.World.Map;
using BaseGames.Localization;
namespace BaseGames.Editor.UI
{
///
/// 地图 UI 脚手架(对照 )。
/// 在当前活动场景生成完整的地图 UI 层级与预制,并按规范绑定引用:
///
/// - Cell / Pin / ExitConnector 预制 + MapPinConfig 占位资产
/// - HUD 下小地图(MinimapHUD + MinimapInputHandler)
/// - Map Canvas 下全屏地图(MapPanel + MapInputHandler,PanelStack 管理)
/// - 传送确认框(ConfirmDialogController)+ MapTeleportConfirmController(接 MapPanel)
/// - 登记 UIManager._panels[Map],绑定 EVT_MapOpen
///
/// 执行路径:BaseGames ▸ Scene ▸ Setup ▸ Scaffold Map UI
/// 占位为纯色块/空 Sprite,美术后续替换;运行依赖 [GameManagers] 下的
/// MapManager / MapPlayerTracker / MapPinManager / TeleportService 已存在(另由 Persistent 脚手架搭建)。
///
public static class MapUIScaffoldWizard
{
private const string UiPrefabDir = "Assets/_Game/Prefabs/UI";
private const string MapDataDir = "Assets/_Game/Data/Map";
[MenuItem("BaseGames/Scene/Setup/Scaffold Map UI", priority = 204)]
public static void ScaffoldMapUI()
{
var report = new List();
Undo.SetCurrentGroupName("Scaffold Map UI");
int undoGroup = Undo.GetCurrentGroup();
// ── 共享资产:Cell / Pin / Exit 预制 + PinConfig ──────────────────
MapPinConfigSO pinConfig = EnsurePinConfig(report);
GameObject cellPrefab = EnsureCellPrefab(report);
GameObject pinPrefab = EnsureSimpleImagePrefab("UI_Map_Pin",
new Color32(0xF0, 0xC0, 0x40, 0xFF), new Vector2(14, 14), report);
GameObject exitPrefab = EnsureSimpleImagePrefab("UI_Map_ExitConnector",
new Color32(0xC0, 0xC0, 0xC0, 0xCC), new Vector2(8, 16), report);
Sprite playerDotSprite = null; // 占位用纯色,留空即可
// ── 小地图(HUD Canvas 下)────────────────────────────────────────
GameObject hudCanvas = FindHudCanvas();
if (hudCanvas == null)
report.Add("未找到 HUD Canvas,请先执行 BaseGames/Scene/Setup/Scaffold HUD Canvas;本次跳过小地图/区域名搭建。");
else
{
// 按需求不搭建右上角小地图,只保留全屏大地图;保留进入区域时的区域名横幅。
BuildRegionBanner(hudCanvas, report);
report.Add("已跳过小地图(MinimapHUD)搭建:按需求只保留全屏大地图。");
}
// ── 全屏地图(独立 Map Canvas,PanelStack 管理)───────────────────
GameObject mapPanelRoot = BuildFullMap(cellPrefab, exitPrefab, pinPrefab, pinConfig, report);
// ── 传送确认框 + 控制器 ───────────────────────────────────────────
BuildTeleportConfirm(mapPanelRoot, report);
// ── 登记 UIManager._panels[Map] + 绑定 EVT_MapOpen ────────────────
RegisterMapPanelWithUIManager(mapPanelRoot, report);
Undo.CollapseUndoOperations(undoGroup);
AssetDatabase.SaveAssets();
MarkDirtyAndLog("Map UI 脚手架", mapPanelRoot != null ? mapPanelRoot : hudCanvas, report);
}
// ─────────────────────────────────────────────────────────────────────
// 预制 / 资产
// ─────────────────────────────────────────────────────────────────────
/// 创建(或复用)MapRoomCellUI 预制:_bg 为 Raycast Target(可点击传送),其余子图不挡射线。
private static GameObject EnsureCellPrefab(List report)
{
string path = $"{UiPrefabDir}/UI_Map_RoomCell.prefab";
var existing = AssetDatabase.LoadAssetAtPath(path);
if (existing != null) return existing;
EnsureFolder(UiPrefabDir);
var root = new GameObject("UI_Map_RoomCell", typeof(RectTransform));
((RectTransform)root.transform).sizeDelta = new Vector2(32, 32);
// 背景(可见 + 可点击)
var bg = MakeImage(root.transform, "BG", new Color(1f, 1f, 1f, 1f), new Vector2(32, 32), raycast: true);
// 轮廓(RawImage,默认禁用)
var outlineGo = MakeChild(root.transform, "Outline", new Vector2(32, 32));
var outline = outlineGo.AddComponent();
outline.raycastTarget = false; outline.enabled = false;
// 图标 / 高亮 / 雾 / 传送标记(均不挡射线,运行时按需启用)
var icon = MakeImage(root.transform, "Icon", new Color(1, 1, 1, 1), new Vector2(20, 20), raycast: false); icon.enabled = false;
var highlight = MakeImage(root.transform, "Highlight", new Color(1f, 0.9f, 0.2f, 1f),new Vector2(34, 34), raycast: false); highlight.enabled = false;
var fog = MakeImage(root.transform, "Fog", new Color(0, 0, 0, 0.85f), new Vector2(32, 32), raycast: false); fog.enabled = false;
var teleport = MakeImage(root.transform, "TeleportMarker", new Color(0.3f, 0.8f, 1f, 1f), new Vector2(12, 12), raycast: false); teleport.enabled = false;
var cell = root.AddComponent();
AssignRef(cell, "_bg", bg);
AssignRef(cell, "_icon", icon);
AssignRef(cell, "_outlineImage", outline);
AssignRef(cell, "_highlight", highlight);
AssignRef(cell, "_fogOverlay", fog);
AssignRef(cell, "_teleportMarker",teleport);
var prefab = PrefabUtility.SaveAsPrefabAsset(root, path);
Object.DestroyImmediate(root);
report.Add($"已创建 Cell 预制:{path}(占位纯色,美术可替换)。");
return prefab;
}
private static GameObject EnsureSimpleImagePrefab(string name, Color color, Vector2 size, List report)
{
string path = $"{UiPrefabDir}/{name}.prefab";
var existing = AssetDatabase.LoadAssetAtPath(path);
if (existing != null) return existing;
EnsureFolder(UiPrefabDir);
var go = new GameObject(name, typeof(RectTransform));
((RectTransform)go.transform).sizeDelta = size;
var img = go.AddComponent();
img.color = color;
img.raycastTarget = false;
var prefab = PrefabUtility.SaveAsPrefabAsset(go, path);
Object.DestroyImmediate(go);
report.Add($"已创建预制:{path}(占位纯色,美术可替换)。");
return prefab;
}
private static MapPinConfigSO EnsurePinConfig(List report)
{
string path = $"{MapDataDir}/MapPinConfig.asset";
var existing = AssetDatabase.LoadAssetAtPath(path);
if (existing != null) return existing;
EnsureFolder(MapDataDir);
var so = ScriptableObject.CreateInstance();
AssetDatabase.CreateAsset(so, path);
report.Add($"已创建占位 MapPinConfig:{path}(_entries 为空,请配置 PinType→Sprite 映射)。");
return so;
}
// ─────────────────────────────────────────────────────────────────────
// 小地图(HUD)
// ─────────────────────────────────────────────────────────────────────
private static void BuildMinimap(GameObject hudCanvas, GameObject cellPrefab,
GameObject pinPrefab, MapPinConfigSO pinConfig, List report)
{
Transform hudRoot = hudCanvas.transform.Find("HUDRoot") ?? hudCanvas.transform;
var minimapGo = GetOrCreateChild(hudRoot, "Minimap").gameObject;
var mmRect = minimapGo.GetComponent() ?? minimapGo.AddComponent();
AnchorTopRight(mmRect, new Vector2(180, 180), new Vector2(-16, -16));
// 边框底图(可见,便于定位;美术可替换/移除)
var frame = minimapGo.GetComponent() ?? minimapGo.AddComponent();
frame.color = new Color(0f, 0f, 0f, 0.35f);
frame.raycastTarget = false;
// 带 RectMask2D 的内容容器(cell 在此平移)
var viewportGo = GetOrCreateChild(minimapGo.transform, "Viewport").gameObject;
var vpRect = viewportGo.GetComponent() ?? viewportGo.AddComponent();
StretchFill(vpRect, 6f);
if (viewportGo.GetComponent() == null) viewportGo.AddComponent();
// 玩家圆点(在容器内)
var playerDot = MakeImage(viewportGo.transform, "PlayerDot", new Color(1f, 0.25f, 0.25f, 1f), new Vector2(8, 8), raycast: false);
var hud = GetOrAddComponent(minimapGo);
var input = GetOrAddComponent(minimapGo);
AssignRef(hud, "_cellPrefab", cellPrefab.GetComponent());
AssignRef(hud, "_cellContainer",vpRect);
AssignRef(hud, "_playerDot", playerDot);
AssignRef(hud, "_pinPrefab", pinPrefab.GetComponent());
AssignRef(hud, "_pinConfig", pinConfig);
AssignMapIcons(hud, report);
AssignAsset(input, "_inputReader", report, true, "InputReader");
report.Add("Minimap 已搭建于 HUDRoot(右上角占位框)。美术可调整位置/尺寸/边框。");
}
/// 进入新区域时屏幕中央渐显区域名横幅(RegionNameDisplay,挂 HUDRoot)。
private static void BuildRegionBanner(GameObject hudCanvas, List report)
{
Transform hudRoot = hudCanvas.transform.Find("HUDRoot") ?? hudCanvas.transform;
var bannerGo = GetOrCreateChild(hudRoot, "RegionNameBanner").gameObject;
var br = bannerGo.GetComponent() ?? bannerGo.AddComponent();
br.anchorMin = br.anchorMax = new Vector2(0.5f, 0.72f);
br.pivot = new Vector2(0.5f, 0.5f);
br.sizeDelta = new Vector2(640f, 90f);
br.anchoredPosition = Vector2.zero;
if (bannerGo.GetComponent() == null) bannerGo.AddComponent();
var txt = MakeText(bannerGo.transform, "RegionText", "区域名");
txt.fontSize = 48;
StretchFill((RectTransform)txt.transform, 0f);
var rnd = GetOrAddComponent(bannerGo);
AssignRef(rnd, "_regionText", txt);
AssignAsset(rnd, "_onRegionChanged", report, false, "EVT_RegionChanged");
report.Add("RegionNameDisplay(进入区域时渐显区域名横幅)已搭建于 HUDRoot。");
}
// ─────────────────────────────────────────────────────────────────────
// 全屏地图(独立 Canvas)
// ─────────────────────────────────────────────────────────────────────
private static GameObject BuildFullMap(GameObject cellPrefab, GameObject exitPrefab,
GameObject pinPrefab, MapPinConfigSO pinConfig, List report)
{
GameObject canvasGo = GetOrCreateMapCanvas("Map Canvas", 25);
var panelGo = GetOrCreateChild(canvasGo.transform, "MapPanel").gameObject;
StretchFill(panelGo.GetComponent() ?? panelGo.AddComponent(), 0f);
// 半透明全屏底
var panelBg = panelGo.GetComponent() ?? panelGo.AddComponent();
panelBg.color = new Color(0.05f, 0.05f, 0.07f, 0.92f);
// ScrollView → Viewport(RectMask2D) → RoomContainer(content)
var scrollGo = GetOrCreateChild(panelGo.transform, "ScrollView").gameObject;
StretchFill(scrollGo.GetComponent() ?? scrollGo.AddComponent(), 40f);
var scrollRect = GetOrAddComponent(scrollGo);
scrollRect.horizontal = true; scrollRect.vertical = true;
scrollRect.movementType = ScrollRect.MovementType.Clamped;
scrollRect.scrollSensitivity = 0f; // 缩放/平移由 MapInputHandler 处理
var viewportGo = GetOrCreateChild(scrollGo.transform, "Viewport").gameObject;
var vpRect = viewportGo.GetComponent() ?? viewportGo.AddComponent();
StretchFill(vpRect, 0f);
if (viewportGo.GetComponent() == null) viewportGo.AddComponent();
var vpImg = viewportGo.GetComponent() ?? viewportGo.AddComponent();
vpImg.color = new Color(0, 0, 0, 0.01f); // 近透明,作为 ScrollRect viewport 的图形
var contentGo = GetOrCreateChild(viewportGo.transform, "RoomContainer").gameObject;
var contentRect = contentGo.GetComponent() ?? contentGo.AddComponent();
contentRect.anchorMin = contentRect.anchorMax = new Vector2(0.5f, 0.5f);
contentRect.pivot = new Vector2(0.5f, 0.5f);
contentRect.sizeDelta = new Vector2(4000, 4000);
scrollRect.content = contentRect;
scrollRect.viewport = vpRect;
// 玩家图标(content 内)
var playerIcon = MakeImage(contentGo.transform, "PlayerIcon", new Color(1f, 0.3f, 0.3f, 1f), new Vector2(16, 16), raycast: false);
// Tooltip(默认隐藏)
var tooltipGo = GetOrCreateChild(panelGo.transform, "Tooltip").gameObject;
var ttRect = tooltipGo.GetComponent() ?? tooltipGo.AddComponent();
AnchorTopRight(ttRect, new Vector2(240, 60), new Vector2(-20, -20));
var ttImg = tooltipGo.GetComponent() ?? tooltipGo.AddComponent();
ttImg.color = new Color(0, 0, 0, 0.8f); ttImg.raycastTarget = false;
var ttText = MakeText(tooltipGo.transform, "Text", "房间");
tooltipGo.SetActive(false);
// 组件
var mapPanel = GetOrAddComponent(panelGo);
var mapInput = GetOrAddComponent(panelGo);
AssignRef(mapPanel, "_roomContainer", contentRect);
AssignRef(mapPanel, "_cellPrefab", cellPrefab.GetComponent());
AssignRef(mapPanel, "_exitConnectorPrefab", exitPrefab.GetComponent());
AssignRef(mapPanel, "_scrollRect", scrollRect);
AssignRef(mapPanel, "_playerIconImg", playerIcon);
AssignRef(mapPanel, "_pinPrefab", pinPrefab.GetComponent());
AssignRef(mapPanel, "_pinConfig", pinConfig);
AssignRef(mapPanel, "_tooltipPanel", tooltipGo);
AssignRef(mapPanel, "_tooltipText", ttText);
AssignMapIcons(mapPanel, report);
AssignRefObj(mapPanel, "_iconPlayerPos", null); // 占位玩家图标用纯色,留空
AssignAsset(mapInput, "_inputReader", report, true, "InputReader");
AssignRef(mapInput, "_scrollRect", scrollRect);
AssignRef(mapInput, "_zoomTarget", contentRect);
// ── 探索进度(左上角,全局% + 当前区域%;格式串走本地化 Key)──────
var progGo = GetOrCreateChild(panelGo.transform, "ProgressDisplay").gameObject;
var progRect = progGo.GetComponent() ?? progGo.AddComponent();
progRect.anchorMin = progRect.anchorMax = new Vector2(0f, 1f);
progRect.pivot = new Vector2(0f, 1f);
progRect.anchoredPosition = new Vector2(28f, -24f);
progRect.sizeDelta = new Vector2(380f, 96f);
var globalTxt = MakeText(progGo.transform, "GlobalProgress", "0%");
var gRt = (RectTransform)globalTxt.transform; gRt.anchorMin = gRt.anchorMax = new Vector2(0f, 1f); gRt.pivot = new Vector2(0f, 1f); gRt.anchoredPosition = Vector2.zero;
globalTxt.alignment = TextAlignmentOptions.TopLeft;
var regionTxt = MakeText(progGo.transform, "RegionProgress", "0%");
var rRt = (RectTransform)regionTxt.transform; rRt.anchorMin = rRt.anchorMax = new Vector2(0f, 1f); rRt.pivot = new Vector2(0f, 1f); rRt.anchoredPosition = new Vector2(0f, -44f);
regionTxt.alignment = TextAlignmentOptions.TopLeft; regionTxt.fontSize = 24;
var prog = GetOrAddComponent(progGo);
AssignRef(prog, "_globalProgressText", globalTxt);
AssignRef(prog, "_regionProgressText", regionTxt);
AssignString(prog, "_globalFormat", "MAP_PROGRESS_GLOBAL"); // 本地化 Key,MapProgressDisplay 运行时解析为格式串
AssignString(prog, "_regionFormat", "MAP_PROGRESS_REGION");
AssignAsset(prog, "_onRegionChanged", report, false, "EVT_RegionChanged");
// ── 关闭提示(底部居中):输入图标(随设备自适应) + 本地化标签 ──────
var hintRow = GetOrCreateChild(panelGo.transform, "CloseHint").gameObject;
var hintRowRt = hintRow.GetComponent() ?? hintRow.AddComponent();
hintRowRt.anchorMin = hintRowRt.anchorMax = new Vector2(0.5f, 0f);
hintRowRt.pivot = new Vector2(0.5f, 0f);
hintRowRt.anchoredPosition = new Vector2(0f, 24f);
hintRowRt.sizeDelta = new Vector2(360f, 40f);
var hintLayout = GetOrAddComponent(hintRow);
hintLayout.childAlignment = TextAnchor.MiddleCenter; hintLayout.spacing = 8f;
hintLayout.childForceExpandWidth = false; hintLayout.childForceExpandHeight = false;
MakeInputIcon(hintRow.transform, "CloseIcon", "Cancel"); // 复用 InputIconImage:键鼠/手柄自动显示对应按键图标
var hintLabel = MakeLocalizedText(hintRow.transform, "CloseLabel", "MAP_CLOSE_HINT");
hintLabel.fontSize = 22;
report.Add("关闭提示:InputIconImage(动作 'Cancel') + LocalizedText('MAP_CLOSE_HINT')。" +
"动作名需与 InputActions 一致;按键图标需在 InputDeviceIconSetSO 中配置(用 Input Icon Studio)。");
panelGo.SetActive(false); // 由 PanelStack 控制显隐
report.Add("MapPanel 已搭建(含探索进度 + 输入图标关闭提示;默认隐藏,由 UIManager PanelStack 管理)。");
return panelGo;
}
// ─────────────────────────────────────────────────────────────────────
// 传送确认框 + 控制器
// ─────────────────────────────────────────────────────────────────────
private static void BuildTeleportConfirm(GameObject mapPanelRoot, List report)
{
if (mapPanelRoot == null) { report.Add("MapPanel 缺失,跳过传送确认框搭建。"); return; }
Transform canvas = mapPanelRoot.transform.parent ?? mapPanelRoot.transform;
// 确认框(自包含,SetActive 显隐)
var dialogGo = GetOrCreateChild(canvas, "TeleportConfirmDialog").gameObject;
StretchFill(dialogGo.GetComponent() ?? dialogGo.AddComponent(), 0f);
var dimImg = dialogGo.GetComponent() ?? dialogGo.AddComponent();
dimImg.color = new Color(0, 0, 0, 0.6f);
var boxGo = GetOrCreateChild(dialogGo.transform, "Box").gameObject;
var boxRect = boxGo.GetComponent() ?? boxGo.AddComponent();
boxRect.anchorMin = boxRect.anchorMax = new Vector2(0.5f, 0.5f);
boxRect.pivot = new Vector2(0.5f, 0.5f);
boxRect.sizeDelta = new Vector2(520, 260);
var boxImg = boxGo.GetComponent() ?? boxGo.AddComponent();
boxImg.color = new Color(0.12f, 0.12f, 0.15f, 1f);
var title = MakeText(boxGo.transform, "Title", "快速传送");
((RectTransform)title.transform).anchoredPosition = new Vector2(0, 90);
var body = MakeText(boxGo.transform, "Body", "传送到该地点?");
var confirmBtn = MakeButton(boxGo.transform, "ConfirmButton", "确认", new Vector2(-110, -90), out TMP_Text confirmLabel);
var cancelBtn = MakeButton(boxGo.transform, "CancelButton", "取消", new Vector2( 110, -90), out TMP_Text cancelLabel);
var dialog = GetOrAddComponent(dialogGo);
AssignRef(dialog, "_titleText", title);
AssignRef(dialog, "_bodyText", body);
AssignRef(dialog, "_confirmLabel", confirmLabel);
AssignRef(dialog, "_cancelLabel", cancelLabel);
AssignRef(dialog, "_btnConfirm", confirmBtn);
AssignRef(dialog, "_btnCancel", cancelBtn);
dialogGo.SetActive(false);
// 控制器:接 MapPanel 的 OnTeleportStationSelected
var ctrlGo = GetOrCreateChild(canvas, "MapTeleportConfirmController").gameObject;
var ctrl = GetOrAddComponent(ctrlGo);
AssignRef(ctrl, "_mapPanel", mapPanelRoot.GetComponent());
AssignRef(ctrl, "_confirmDialog", dialog);
report.Add("传送确认框 + MapTeleportConfirmController 已搭建并绑定 MapPanel。");
}
// ─────────────────────────────────────────────────────────────────────
// UIManager 登记
// ─────────────────────────────────────────────────────────────────────
private static void RegisterMapPanelWithUIManager(GameObject mapPanelRoot, List report)
{
if (mapPanelRoot == null) return;
var uiManager = Object.FindFirstObjectByType();
if (uiManager == null)
{
report.Add("场景中无 UIManager,未登记 PanelId.Map;请在 UIManager._panels 手动添加 {Map, MapPanel}。");
return;
}
var so = new SerializedObject(uiManager);
var panels = so.FindProperty("_panels");
if (panels == null || !panels.isArray)
{
report.Add("UIManager._panels 不可写,请手动登记 PanelId.Map。");
}
else
{
// 已登记则跳过
bool exists = false;
for (int i = 0; i < panels.arraySize; i++)
{
var el = panels.GetArrayElementAtIndex(i);
if (el.FindPropertyRelative("id").enumValueIndex == (int)PanelId.Map)
{
el.FindPropertyRelative("root").objectReferenceValue = mapPanelRoot;
exists = true; break;
}
}
if (!exists)
{
int idx = panels.arraySize;
panels.arraySize = idx + 1;
var el = panels.GetArrayElementAtIndex(idx);
el.FindPropertyRelative("id").enumValueIndex = (int)PanelId.Map;
el.FindPropertyRelative("root").objectReferenceValue = mapPanelRoot;
}
so.ApplyModifiedPropertiesWithoutUndo();
report.Add("已登记 UIManager._panels[Map] → MapPanel。");
}
AssignAsset(uiManager, "_onMapOpen", report, false, "EVT_MapOpen", "EVT_OpenMap");
}
// ─────────────────────────────────────────────────────────────────────
// 通用辅助
// ─────────────────────────────────────────────────────────────────────
private static void AssignMapIcons(Object target, List report)
{
AssignAsset(target, "_iconSavePoint", report, false, "ICN_Map_SavePoint", "ICN_SavePoint");
AssignAsset(target, "_iconBossRoom", report, false, "ICN_Map_Boss", "ICN_Boss");
AssignAsset(target, "_iconShop", report, false, "ICN_Map_Shop", "ICN_Shop");
AssignAsset(target, "_iconTeleport", report, false, "ICN_Map_Teleport", "ICN_Teleport");
}
private static GameObject FindHudCanvas()
{
Scene scene = SceneManager.GetActiveScene();
foreach (GameObject root in scene.GetRootGameObjects())
{
if (root.name == "HUD Canvas") return root;
foreach (string path in new[] { "[UI]/UIRoot/HUD Canvas", "UIRoot/HUD Canvas", "HUD Canvas" })
{
var found = root.transform.Find(path);
if (found != null) return found.gameObject;
}
}
return null;
}
private static GameObject GetOrCreateMapCanvas(string name, int sortOrder)
{
Scene scene = SceneManager.GetActiveScene();
Transform uiRoot = null;
foreach (GameObject root in scene.GetRootGameObjects())
{
if (root.name == name) return root;
foreach (string path in new[] { $"[UI]/UIRoot/{name}", $"UIRoot/{name}" })
{
var found = root.transform.Find(path);
if (found != null) return found.gameObject;
}
uiRoot ??= root.transform.Find("[UI]/UIRoot") ?? root.transform.Find("UIRoot");
}
var canvasGo = new GameObject(name);
Undo.RegisterCreatedObjectUndo(canvasGo, $"Create {name}");
if (uiRoot != null) canvasGo.transform.SetParent(uiRoot, false);
var canvas = canvasGo.AddComponent