267 lines
17 KiB
C#
267 lines
17 KiB
C#
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; } }
|
||
}
|
||
}
|