Files
zeling_v2/Assets/_Game/Scripts/Editor/UI/UINewGameModeScaffold.cs
2026-06-07 11:49:55 +08:00

266 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}
}
}