This commit is contained in:
2026-06-07 11:49:55 +08:00
parent ff0f3bde54
commit 1897658a00
98 changed files with 9903 additions and 13907 deletions

View File

@@ -0,0 +1,265 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using BaseGames.UI.MainMenu;
using BaseGames.UI.Theme;
using BaseGames.Core.Events;
using BaseGames.Localization;
using BaseGames.Editor.Localization;
namespace BaseGames.Editor.UI
{
/// <summary>
/// 新游戏难度选择脚手架:生成 / 更新 <c>UI_NewGameModePanel</c> 预制件 + 默认 <c>UI_NewGameModeConfig</c> 表 + 中英文案。
/// 该面板是 MainMenu Canvas 的子面板(非独立 Canvas由 SaveSlotController 经 ShowAsync 模态弹出。
///
/// 美术 → 改 UI_NewGameModePanel 预制件(暗化/对话框/标题/说明/返回样式,难度按钮样式见 UI_MainMenu_Button
/// 策划 → 改 UI_NewGameModeConfig增删/重排难度、改标签/说明)。
///
/// 菜单BaseGames/UI/控件库/生成新游戏难度选择(预制件 + 默认配置)
/// </summary>
public static class UINewGameModeScaffold
{
private const string PrefabDir = "Assets/_Game/Prefabs/UI";
private const string ConfigDir = "Assets/_Game/Data/UI";
private const string ThemePath = "Assets/_Game/Data/UI/Themes/UI_Theme_Default.asset";
private const string PrefabName = "UI_NewGameModePanel";
private const string ConfigName = "UI_NewGameModeConfig";
private const string BtnPrefabPath = "Assets/_Game/Prefabs/UI/Controls/UI_MainMenu_Button.prefab";
[MenuItem("BaseGames/UI/控件库/生成新游戏难度选择(预制件 + 默认配置)")]
public static void GeneratePrefab()
{
EnsureFolder(PrefabDir);
EnsureFolder(ConfigDir);
var report = new List<string>();
var theme = AssetDatabase.LoadAssetAtPath<UIThemeSO>(ThemePath);
var config = EnsureDefaultConfig(report);
SeedLocalization(report);
BuildPrefab(theme, config, report);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
var sb = new System.Text.StringBuilder("[UINewGameMode] 难度选择已生成/更新:\n");
foreach (var r in report) sb.AppendLine(" • " + r);
sb.AppendLine("用法重跑「BaseGames/Scene/Setup/Scaffold MainMenu Scene」实例化进 MainMenu接 SaveSlotController._modeSelect。");
Debug.Log(sb.ToString());
}
private static void BuildPrefab(UIThemeSO theme, NewGameModeConfigSO config, List<string> report)
{
// 根:铺满全屏的面板(非 Canvas作为 MainMenu Canvas 子节点)。初始隐藏,由导航器激活。
var root = new GameObject(PrefabName, typeof(RectTransform), typeof(CanvasGroup));
var rootRt = (RectTransform)root.transform;
Stretch(rootRt);
var ctrl = root.AddComponent<NewGameModeController>();
var applier = root.AddComponent<UIThemeApplier>();
if (theme != null) AssignRef(applier, "_theme", theme);
// 暗化遮罩(铺满,挡点击)
var overlayGo = NewUIChild(root.transform, "Overlay", out var overlayRt);
Stretch(overlayRt);
var overlay = overlayGo.AddComponent<Image>();
overlay.color = new Color(0f, 0f, 0f, 0.6f); overlay.raycastTarget = true;
// 对话框
var boxGo = NewUIChild(root.transform, "DialogBox", out var boxRt);
SetRect(boxRt, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f),
Vector2.zero, new Vector2(760f, 520f));
var box = boxGo.AddComponent<Image>();
box.sprite = Standard(); box.type = Image.Type.Sliced;
box.color = new Color(0.10f, 0.11f, 0.15f, 0.98f);
SetEnum(boxGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Graphic_Background);
// 标题
var titleGo = NewUIChild(boxGo.transform, "Title", out var titleRt);
SetRect(titleRt, new Vector2(0f, 1f), new Vector2(1f, 1f), new Vector2(0.5f, 1f),
new Vector2(0f, -50f), new Vector2(-60f, 60f));
var title = titleGo.AddComponent<TextMeshProUGUI>();
title.text = "选择难度"; title.alignment = TextAlignmentOptions.Center; title.fontSize = 40f;
title.raycastTarget = false;
var titleLoc = titleGo.AddComponent<LocalizedText>();
SetString(titleLoc, "_key", "MODE_SELECT_TITLE");
SetEnum(titleGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Header);
// 难度按钮容器(竖排)
var optionGo = NewUIChild(boxGo.transform, "OptionContainer", out var optionRt);
SetRect(optionRt, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f),
new Vector2(0f, -130f), new Vector2(460f, 200f));
var vlg = optionGo.AddComponent<VerticalLayoutGroup>();
vlg.spacing = 12f; vlg.childAlignment = TextAnchor.UpperCenter;
vlg.childControlWidth = true; vlg.childControlHeight = true;
vlg.childForceExpandWidth = true; vlg.childForceExpandHeight = false;
// 说明文本(选中项时显示)
var descGo = NewUIChild(boxGo.transform, "DescText", out var descRt);
SetRect(descRt, new Vector2(0f, 0f), new Vector2(1f, 0f), new Vector2(0.5f, 0f),
new Vector2(0f, 130f), new Vector2(-80f, 80f));
var desc = descGo.AddComponent<TextMeshProUGUI>();
desc.text = ""; desc.alignment = TextAlignmentOptions.Center; desc.fontSize = 22f;
desc.color = new Color(0.82f, 0.6f, 0.6f, 1f); desc.raycastTarget = false; desc.enableWordWrapping = true;
SetEnum(descGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Secondary);
// 返回按钮
var backGo = NewUIChild(boxGo.transform, "Btn_Back", out var backRt);
SetRect(backRt, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0.5f, 0f),
new Vector2(0f, 46f), new Vector2(260f, 60f));
var backImg = backGo.AddComponent<Image>();
backImg.sprite = Standard(); backImg.type = Image.Type.Sliced; backImg.color = new Color(1f, 1f, 1f, 0.06f);
var backBtn = backGo.AddComponent<Button>(); backBtn.targetGraphic = backImg;
var backLblGo = NewUIChild(backGo.transform, "Label", out var backLblRt);
Stretch(backLblRt);
var backLbl = backLblGo.AddComponent<TextMeshProUGUI>();
backLbl.text = "返回"; backLbl.alignment = TextAlignmentOptions.Center; backLbl.fontSize = 28f;
backLblGo.AddComponent<LocalizedText>();
SetString(backLblGo.GetComponent<LocalizedText>(), "_key", "BTN_BACK");
SetEnum(backLblGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Primary);
// 绑定控制器
AssignRef(ctrl, "_config", config);
AssignRef(ctrl, "_container", optionRt);
AssignRef(ctrl, "_canvasGroup", root.GetComponent<CanvasGroup>());
AssignRef(ctrl, "_titleText", titleLoc);
AssignRef(ctrl, "_descText", desc);
AssignRef(ctrl, "_btnBack", backBtn);
var btnPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(BtnPrefabPath);
var btnView = btnPrefab != null ? btnPrefab.GetComponent<MainMenuButtonView>() : null;
if (btnView == null) report.Add($"未找到按钮预制件 {BtnPrefabPath}(请先运行「生成主菜单」)。");
AssignRef(ctrl, "_buttonPrefab", btnView);
root.SetActive(false); // 结果面板由导航器激活
string path = $"{PrefabDir}/{PrefabName}.prefab";
PrefabUtility.SaveAsPrefabAsset(root, path);
Object.DestroyImmediate(root);
report.Add(path);
}
private static NewGameModeConfigSO EnsureDefaultConfig(List<string> report)
{
string path = $"{ConfigDir}/{ConfigName}.asset";
var cfg = AssetDatabase.LoadAssetAtPath<NewGameModeConfigSO>(path);
bool created = cfg == null;
if (created) { cfg = ScriptableObject.CreateInstance<NewGameModeConfigSO>(); AssetDatabase.CreateAsset(cfg, path); }
if (created)
{
var items = new (DifficultyLevel lvl, string label, string desc)[]
{
(DifficultyLevel.Normal, "MODE_NORMAL", ""),
(DifficultyLevel.SteelSoul, "MODE_STEELSOUL", "MODE_STEELSOUL_DESC"),
};
var so = new SerializedObject(cfg);
var prop = so.FindProperty("_items");
prop.arraySize = items.Length;
for (int i = 0; i < items.Length; i++)
{
var el = prop.GetArrayElementAtIndex(i);
el.FindPropertyRelative("level").enumValueIndex = (int)items[i].lvl;
el.FindPropertyRelative("labelKey").stringValue = items[i].label;
el.FindPropertyRelative("descKey").stringValue = items[i].desc;
}
so.ApplyModifiedPropertiesWithoutUndo();
report.Add($"{path}(默认 {items.Length} 项:普通 / 钢铁之魂)");
}
return cfg;
}
private static void SeedLocalization(List<string> report)
{
var zh = new Dictionary<string, string>
{
{ "MODE_SELECT_TITLE", "选择难度" },
{ "MODE_NORMAL", "普通" },
{ "MODE_STEELSOUL", "钢铁之魂" },
{ "MODE_STEELSOUL_DESC", "一命模式:死亡即清空存档,请谨慎选择。" },
{ "BTN_BACK", "返回" },
};
var en = new Dictionary<string, string>
{
{ "MODE_SELECT_TITLE", "Select Mode" },
{ "MODE_NORMAL", "Normal" },
{ "MODE_STEELSOUL", "Steel Soul" },
{ "MODE_STEELSOUL_DESC", "One life. Death wipes the save — choose with care." },
{ "BTN_BACK", "Back" },
};
int added = MergeWriteUI(Language.ChineseSimplified, zh) + MergeWriteUI(Language.English, en);
report.Add($"本地化新增 {added} 条(已存在不覆盖)。");
}
private static int MergeWriteUI(Language lang, Dictionary<string, string> kv)
{
var dict = LocalizationFileIO.Read(lang, LocalizationTable.UI);
int added = 0;
foreach (var p in kv)
if (!dict.ContainsKey(p.Key)) { dict[p.Key] = p.Value; added++; }
if (added > 0) LocalizationFileIO.Write(lang, LocalizationTable.UI, dict);
return added;
}
// ── 助手 ─────────────────────────────────────────────────────────────
private static GameObject NewUIChild(Transform parent, string name, out RectTransform rt)
{
var go = new GameObject(name, typeof(RectTransform));
rt = (RectTransform)go.transform;
rt.SetParent(parent, false);
return go;
}
private static void Stretch(RectTransform rt)
{
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
}
private static void SetRect(RectTransform rt, Vector2 aMin, Vector2 aMax, Vector2 pivot,
Vector2 anchoredPos, Vector2 size)
{
rt.anchorMin = aMin; rt.anchorMax = aMax; rt.pivot = pivot;
rt.anchoredPosition = anchoredPos; rt.sizeDelta = size;
}
private static void SetEnum(Component c, string prop, int value)
{
var so = new SerializedObject(c);
var p = so.FindProperty(prop);
if (p != null) { p.enumValueIndex = value; so.ApplyModifiedPropertiesWithoutUndo(); }
}
private static void SetString(Component c, string prop, string value)
{
if (c == null) return;
var so = new SerializedObject(c);
var p = so.FindProperty(prop);
if (p != null) { p.stringValue = value; so.ApplyModifiedPropertiesWithoutUndo(); }
}
private static void AssignRef(Object target, string prop, Object value)
{
var so = new SerializedObject(target);
var p = so.FindProperty(prop);
if (p == null) { Debug.LogWarning($"[UINewGameMode] 未找到属性 {target.GetType().Name}.{prop}"); return; }
p.objectReferenceValue = value;
so.ApplyModifiedPropertiesWithoutUndo();
}
private static Sprite Standard() => AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/UISprite.psd");
private static void EnsureFolder(string dir)
{
string[] parts = dir.Split('/');
string cur = parts[0];
for (int i = 1; i < parts.Length; i++)
{
string next = $"{cur}/{parts[i]}";
if (!AssetDatabase.IsValidFolder(next)) AssetDatabase.CreateFolder(cur, parts[i]);
cur = next;
}
}
}
}