Files
zeling_v2/Assets/_Game/Scripts/Editor/UI/UIBestiaryScaffold.cs
2026-06-08 11:26:17 +08:00

267 lines
17 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;
using BaseGames.UI.Theme;
using BaseGames.Core.Events;
using BaseGames.Localization;
using BaseGames.Editor.Localization;
namespace BaseGames.Editor.UI
{
/// <summary>
/// 图鉴脚手架:生成 <c>UI_BestiaryCell</c> + <c>UI_BestiaryPanel</c> 预制件 + 空 <c>UI_BestiaryDatabase</c> 表 + BESTIARY_* 中英文案。
/// 面板据 BestiaryDatabaseSO 列出全部敌人,经 IBestiaryService 读已发现态(已发现亮/未发现 ??? + 剪影 + 锁),
/// 订阅 EVT_BestiaryUpdated 实时刷新;点击格子在右侧详情区展示。
///
/// 数据:策划在 UI_BestiaryDatabase 增删条目enemyId 须与 EnemyBase._enemyId 一致),或用「同步图鉴条目」工具自动补。
/// 菜单BaseGames/UI/控件库/生成图鉴(预制件 + 空表)
/// </summary>
public static class UIBestiaryScaffold
{
private const string PrefabDir = "Assets/_Game/Prefabs/UI";
private const string ControlsDir = "Assets/_Game/Prefabs/UI/Controls";
private const string ConfigDir = "Assets/_Game/Data/UI";
private const string ThemePath = "Assets/_Game/Data/UI/Themes/UI_Theme_Default.asset";
private const string PanelName = "UI_BestiaryPanel";
private const string CellName = "UI_BestiaryCell";
private const string DbName = "UI_BestiaryDatabase";
[MenuItem("BaseGames/UI/控件库/生成图鉴(预制件 + 空表)")]
public static void GeneratePrefab()
{
EnsureFolder(PrefabDir); EnsureFolder(ControlsDir); EnsureFolder(ConfigDir);
var report = new List<string>();
var theme = AssetDatabase.LoadAssetAtPath<UIThemeSO>(ThemePath);
var db = EnsureDatabase(report);
SeedLocalization(report);
var cell = BuildCellPrefab(theme, report);
BuildPanelPrefab(theme, db, cell, report);
AssetDatabase.SaveAssets(); AssetDatabase.Refresh();
var sb = new System.Text.StringBuilder("[UIBestiary] 图鉴已生成:\n");
foreach (var r in report) sb.AppendLine(" • " + r);
sb.AppendLine("策划在 UI_BestiaryDatabase 填敌人条目enemyId 同 EnemyBase._enemyId重跑「Scaffold Inventory Hub」收编为图鉴 Tab。");
Debug.Log(sb.ToString());
}
private static BestiaryDatabaseSO EnsureDatabase(List<string> report)
{
string path = $"{ConfigDir}/{DbName}.asset";
var db = AssetDatabase.LoadAssetAtPath<BestiaryDatabaseSO>(path);
if (db == null)
{
db = ScriptableObject.CreateInstance<BestiaryDatabaseSO>();
AssetDatabase.CreateAsset(db, path);
report.Add($"{path}(空表,待策划/同步工具填充)");
}
return db;
}
private static void SeedLocalization(List<string> report)
{
var zh = new Dictionary<string, string>
{
{ "MENU_BESTIARY", "图鉴" }, { "BESTIARY_TITLE", "图鉴" },
{ "BESTIARY_LOCKED", "???" }, { "BESTIARY_LOCKED_DESC", "尚未发现该敌人。" },
{ "BESTIARY_HP", "生命" }, { "BESTIARY_KILLS", "击杀" },
};
var en = new Dictionary<string, string>
{
{ "MENU_BESTIARY", "Bestiary" }, { "BESTIARY_TITLE", "Bestiary" },
{ "BESTIARY_LOCKED", "???" }, { "BESTIARY_LOCKED_DESC", "Not yet discovered." },
{ "BESTIARY_HP", "HP" }, { "BESTIARY_KILLS", "Kills" },
};
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 BestiaryCellView BuildCellPrefab(UIThemeSO theme, List<string> report)
{
var root = new GameObject(CellName, typeof(RectTransform), typeof(CanvasGroup));
((RectTransform)root.transform).sizeDelta = new Vector2(170f, 130f);
var view = root.AddComponent<BestiaryCellView>();
var bg = root.AddComponent<Image>();
bg.sprite = Std(); bg.type = Image.Type.Sliced; bg.color = new Color(1f, 1f, 1f, 0.05f);
var btn = root.AddComponent<Button>(); btn.targetGraphic = bg;
var iconGo = NewUIChild(root.transform, "Icon", out var iconRt);
SetRect(iconRt, C(0.5f, 1f), C(0.5f, 1f), C(0.5f, 1f), new Vector2(0f, -44f), new Vector2(72f, 72f));
var icon = iconGo.AddComponent<Image>(); icon.preserveAspect = true; icon.raycastTarget = false;
var nameGo = NewUIChild(root.transform, "Name", out var nameRt);
SetRect(nameRt, C(0.5f, 0f), C(0.5f, 0f), C(0.5f, 0f), new Vector2(0f, 14f), new Vector2(154f, 40f));
var nameTmp = nameGo.AddComponent<TextMeshProUGUI>();
nameTmp.alignment = TextAlignmentOptions.Center; nameTmp.fontSize = 18f; nameTmp.enableWordWrapping = true;
nameTmp.raycastTarget = false;
var loc = nameGo.AddComponent<LocalizedText>();
SetEnum(nameGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Primary);
var lockGo = NewUIChild(root.transform, "LockedOverlay", out var lockRt);
Stretch(lockRt);
var lockImg = lockGo.AddComponent<Image>(); lockImg.color = new Color(0f, 0f, 0f, 0.5f); lockImg.raycastTarget = false;
lockGo.SetActive(false);
var so = new SerializedObject(view);
so.FindProperty("_icon").objectReferenceValue = icon;
so.FindProperty("_label").objectReferenceValue = loc;
so.FindProperty("_lockedOverlay").objectReferenceValue = lockGo;
so.FindProperty("_group").objectReferenceValue = root.GetComponent<CanvasGroup>();
so.FindProperty("_selectButton").objectReferenceValue = btn;
so.ApplyModifiedPropertiesWithoutUndo();
string path = $"{ControlsDir}/{CellName}.prefab";
var asset = PrefabUtility.SaveAsPrefabAsset(root, path);
Object.DestroyImmediate(root);
report.Add(path);
return asset.GetComponent<BestiaryCellView>();
}
// ── 面板预制件 ───────────────────────────────────────────────────────
private static void BuildPanelPrefab(UIThemeSO theme, BestiaryDatabaseSO db, BestiaryCellView cell, List<string> report)
{
var root = new GameObject(PanelName,
typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster), typeof(CanvasGroup));
Stretch((RectTransform)root.transform);
SetupCanvas(root, 62);
var panel = root.AddComponent<BestiaryPanel>();
var applier = root.AddComponent<UIThemeApplier>();
if (theme != null) AssignRef(applier, "_theme", theme);
var overlayGo = NewUIChild(root.transform, "Overlay", out var ovRt);
Stretch(ovRt);
overlayGo.AddComponent<Image>().color = new Color(0f, 0f, 0f, 0.6f);
var boxGo = NewUIChild(root.transform, "DialogBox", out var boxRt);
SetRect(boxRt, C(0.5f), C(0.5f), C(0.5f), Vector2.zero, new Vector2(1280f, 760f));
var box = boxGo.AddComponent<Image>(); box.sprite = Std(); 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, C(0.5f, 1f), C(0.5f, 1f), C(0.5f, 1f), new Vector2(0f, -46f), new Vector2(600f, 54f));
var title = titleGo.AddComponent<TextMeshProUGUI>();
title.alignment = TextAlignmentOptions.Center; title.fontSize = 40f; title.raycastTarget = false;
titleGo.AddComponent<LocalizedText>();
SetString(titleGo.GetComponent<LocalizedText>(), "_key", "BESTIARY_TITLE");
SetEnum(titleGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Header);
// 完成度(右上)
var compGo = NewUIChild(boxGo.transform, "Completion", out var compRt);
SetRect(compRt, C(1f, 1f), C(1f, 1f), C(1f, 1f), new Vector2(-40f, -52f), new Vector2(300f, 40f));
var comp = compGo.AddComponent<TextMeshProUGUI>();
comp.alignment = TextAlignmentOptions.Right; comp.fontSize = 26f; comp.raycastTarget = false;
SetEnum(compGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Secondary);
// 敌人格网格(左)
var gridGo = NewUIChild(boxGo.transform, "GridContainer", out var gridRt);
SetRect(gridRt, C(0f, 1f), C(0f, 1f), C(0f, 1f), new Vector2(40f, -120f), new Vector2(760f, 600f));
var grid = gridGo.AddComponent<GridLayoutGroup>();
grid.cellSize = new Vector2(170f, 130f); grid.spacing = new Vector2(14f, 14f);
grid.childAlignment = TextAnchor.UpperLeft;
grid.constraint = GridLayoutGroup.Constraint.FixedColumnCount; grid.constraintCount = 4;
// 详情区(右)
var detailGo = NewUIChild(boxGo.transform, "DetailPane", out var detailRt);
SetRect(detailRt, C(1f, 1f), C(1f, 1f), C(1f, 1f), new Vector2(-40f, -120f), new Vector2(420f, 600f));
var detailBg = detailGo.AddComponent<Image>(); detailBg.color = new Color(1f, 1f, 1f, 0.03f);
detailBg.sprite = Std(); detailBg.type = Image.Type.Sliced;
var dIconGo = NewUIChild(detailGo.transform, "DetailIcon", out var dIconRt);
SetRect(dIconRt, C(0.5f, 1f), C(0.5f, 1f), C(0.5f, 1f), new Vector2(0f, -30f), new Vector2(160f, 160f));
var dIcon = dIconGo.AddComponent<Image>(); dIcon.preserveAspect = true; dIcon.raycastTarget = false;
var dNameGo = NewUIChild(detailGo.transform, "DetailName", out var dNameRt);
SetRect(dNameRt, C(0.5f, 1f), C(0.5f, 1f), C(0.5f, 1f), new Vector2(0f, -210f), new Vector2(384f, 44f));
var dName = dNameGo.AddComponent<TextMeshProUGUI>();
dName.alignment = TextAlignmentOptions.Center; dName.fontSize = 28f; dName.raycastTarget = false;
dNameGo.AddComponent<LocalizedText>();
SetEnum(dNameGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Header);
var dStatsGo = NewUIChild(detailGo.transform, "DetailStats", out var dStatsRt);
SetRect(dStatsRt, C(0.5f, 1f), C(0.5f, 1f), C(0.5f, 1f), new Vector2(0f, -258f), new Vector2(384f, 34f));
var dStats = dStatsGo.AddComponent<TextMeshProUGUI>();
dStats.alignment = TextAlignmentOptions.Center; dStats.fontSize = 20f; dStats.raycastTarget = false;
SetEnum(dStatsGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Secondary);
var dDescGo = NewUIChild(detailGo.transform, "DetailDesc", out var dDescRt);
SetRect(dDescRt, C(0.5f, 1f), C(0.5f, 1f), C(0.5f, 1f), new Vector2(0f, -310f), new Vector2(372f, 260f));
var dDesc = dDescGo.AddComponent<TextMeshProUGUI>();
dDesc.alignment = TextAlignmentOptions.TopLeft; dDesc.fontSize = 18f; dDesc.enableWordWrapping = true; dDesc.raycastTarget = false;
SetEnum(dDescGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Secondary);
// 关闭
var closeGo = NewUIChild(boxGo.transform, "Btn_Close", out var closeRt);
SetRect(closeRt, C(0.5f, 0f), C(0.5f, 0f), C(0.5f, 0f), new Vector2(0f, 32f), new Vector2(240f, 56f));
var closeImg = closeGo.AddComponent<Image>(); closeImg.sprite = Std(); closeImg.type = Image.Type.Sliced;
closeImg.color = new Color(1f, 1f, 1f, 0.06f);
var closeBtn = closeGo.AddComponent<Button>(); closeBtn.targetGraphic = closeImg;
var clblGo = NewUIChild(closeGo.transform, "Label", out var clblRt);
Stretch(clblRt);
var clbl = clblGo.AddComponent<TextMeshProUGUI>();
clbl.alignment = TextAlignmentOptions.Center; clbl.fontSize = 26f;
clblGo.AddComponent<LocalizedText>();
SetString(clblGo.GetComponent<LocalizedText>(), "_key", "BTN_BACK");
SetEnum(clblGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Primary);
// 绑定 BestiaryPanel
var so = new SerializedObject(panel);
so.FindProperty("_database").objectReferenceValue = db;
so.FindProperty("_container").objectReferenceValue = gridRt;
so.FindProperty("_cellPrefab").objectReferenceValue = cell;
so.FindProperty("_completionText").objectReferenceValue = comp;
so.FindProperty("_btnClose").objectReferenceValue = closeBtn;
so.FindProperty("_detailIcon").objectReferenceValue = dIcon;
so.FindProperty("_detailName").objectReferenceValue = dNameGo.GetComponent<LocalizedText>();
so.FindProperty("_detailDesc").objectReferenceValue = dDesc;
so.FindProperty("_detailStats").objectReferenceValue = dStats;
so.FindProperty("_onBestiaryUpdated").objectReferenceValue = FindAsset<StringEventChannelSO>("EVT_BestiaryUpdated");
so.ApplyModifiedPropertiesWithoutUndo();
root.SetActive(false);
PrefabUtility.SaveAsPrefabAsset(root, $"{PrefabDir}/{PanelName}.prefab");
Object.DestroyImmediate(root);
report.Add($"{PrefabDir}/{PanelName}.prefab");
}
// ── 助手(对照 UIAbilityOverviewScaffold──────────────────────────────
private static Vector2 C(float x = 0.5f, float y = 0.5f) => new Vector2(x, y);
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 pos, Vector2 size)
{ rt.anchorMin = aMin; rt.anchorMax = aMax; rt.pivot = pivot; rt.anchoredPosition = pos; 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) { p.objectReferenceValue = value; so.ApplyModifiedPropertiesWithoutUndo(); } }
private static T FindAsset<T>(string name) where T : Object
{ foreach (var g in AssetDatabase.FindAssets($"{name} t:{typeof(T).Name}")) { var pa = AssetDatabase.GUIDToAssetPath(g); var a = AssetDatabase.LoadAssetAtPath<T>(pa); if (a != null && a.name == name) return a; } return null; }
private static void SetupCanvas(GameObject go, int sortOrder)
{
var canvas = go.GetComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.sortingOrder = sortOrder;
var scaler = go.GetComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920f, 1080f);
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
scaler.matchWidthOrHeight = 0.5f;
}
private static Sprite Std() => AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/UISprite.psd");
private static void EnsureFolder(string dir) { var parts = dir.Split('/'); string cur = parts[0]; for (int i = 1; i < parts.Length; i++) { string n = $"{cur}/{parts[i]}"; if (!AssetDatabase.IsValidFolder(n)) AssetDatabase.CreateFolder(cur, parts[i]); cur = n; } }
}
}