UI 系统

This commit is contained in:
2026-06-08 11:26:17 +08:00
parent 1897658a00
commit b582317692
94 changed files with 33540 additions and 3726 deletions

View File

@@ -0,0 +1,216 @@
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using BaseGames.UI;
using BaseGames.Enemies;
using BaseGames.Localization;
using BaseGames.Editor.Localization;
namespace BaseGames.Editor.UI
{
/// <summary>
/// 图鉴条目同步 / 校验工具(策划友好)。从现有敌人数据自动补全 <see cref="BestiaryDatabaseSO"/>
/// <list type="bullet">
/// <item>来源 AAssets/_Game/Data/Enemies/{id}/ 下的 EnemyStatsSO文件夹名即 enemyId读 MaxHP</item>
/// <item>来源 B含 EnemyBase 的预制件(权威 _enemyId + StatsSO.MaxHP + Boss 判定),优先级高于 A</item>
/// </list>
/// 仅"补全缺失":已存在条目的策划字段(名/图标/描述/分级)一律保留,只在 displayMaxHP 为 0 时回填。
/// 新增条目自动生成 ENM_{id}_NAME / ENM_{id}_DESC 占位本地化(已存在不覆盖)。
/// 孤儿条目enemyId 无对应敌人数据,常为手敲错字)仅警告,不删除。
///
/// 菜单BaseGames/UI/控件库/同步图鉴条目(从敌人数据) · 校验图鉴条目
/// </summary>
public static class BestiarySyncTool
{
private const string DbPath = "Assets/_Game/Data/UI/UI_BestiaryDatabase.asset";
private const string EnemiesDir = "Assets/_Game/Data/Enemies";
private struct Src { public int hp; public bool boss; public string from; }
[MenuItem("BaseGames/UI/控件库/同步图鉴条目(从敌人数据)", priority = 300)]
public static void Sync()
{
var db = LoadDb();
if (db == null) return;
var discovered = Discover(out var srcLog);
var so = new SerializedObject(db);
var arr = so.FindProperty("_entries");
// 现有条目 id → 索引
var existing = new Dictionary<string, int>();
for (int i = 0; i < arr.arraySize; i++)
{
string id = arr.GetArrayElementAtIndex(i).FindPropertyRelative("enemyId").stringValue;
if (!string.IsNullOrEmpty(id) && !existing.ContainsKey(id)) existing[id] = i;
}
int added = 0, hpFilled = 0;
var newLocKeys = new List<string>();
foreach (var kv in discovered.OrderBy(k => k.Key))
{
string id = kv.Key;
if (existing.TryGetValue(id, out int idx))
{
// 已存在:仅在 HP 为 0 时回填,其余策划字段保留
var el = arr.GetArrayElementAtIndex(idx);
var hpProp = el.FindPropertyRelative("displayMaxHP");
if (hpProp.intValue == 0 && kv.Value.hp > 0) { hpProp.intValue = kv.Value.hp; hpFilled++; }
continue;
}
// 新增
int n = arr.arraySize;
arr.arraySize = n + 1;
var nel = arr.GetArrayElementAtIndex(n);
nel.FindPropertyRelative("enemyId").stringValue = id;
nel.FindPropertyRelative("displayNameKey").stringValue = $"ENM_{id}_NAME";
nel.FindPropertyRelative("descKey").stringValue = $"ENM_{id}_DESC";
nel.FindPropertyRelative("icon").objectReferenceValue = null;
nel.FindPropertyRelative("tier").enumValueIndex = kv.Value.boss ? 2 : 0; // Boss : Normal
nel.FindPropertyRelative("region").stringValue = "";
nel.FindPropertyRelative("fullLoreKillCount").intValue = 1;
nel.FindPropertyRelative("displayMaxHP").intValue = kv.Value.hp;
newLocKeys.Add(id);
added++;
}
so.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(db);
int locAdded = SeedLocalization(newLocKeys);
// 孤儿DB 有、敌人数据无)
var orphans = existing.Keys.Where(id => !discovered.ContainsKey(id)).OrderBy(s => s).ToList();
AssetDatabase.SaveAssets();
Selection.activeObject = db;
EditorGUIUtility.PingObject(db);
var sb = new System.Text.StringBuilder();
sb.AppendLine($"[图鉴同步] 完成:新增 {added} 条 · 回填 HP {hpFilled} 条 · 本地化新增 {locAdded} 条 · 现有总计 {arr.arraySize} 条。");
sb.AppendLine($"发现敌人来源 {discovered.Count} 个:");
foreach (var l in srcLog) sb.AppendLine(" • " + l);
if (orphans.Count > 0)
sb.AppendLine($"⚠ 孤儿条目 {orphans.Count} 个enemyId 无对应敌人数据,请核对拼写,未自动删除):{string.Join(", ", orphans)}");
Debug.Log(sb.ToString(), db);
EditorUtility.DisplayDialog("图鉴同步",
$"新增 {added} 条,回填 HP {hpFilled} 条。\n现有总计 {arr.arraySize} 条。\n" +
(orphans.Count > 0 ? $"⚠ {orphans.Count} 个孤儿条目(见 Console。" : "无孤儿条目。") +
"\n\n详情见 Console已选中 UI_BestiaryDatabase。", "好");
}
[MenuItem("BaseGames/UI/控件库/校验图鉴条目", priority = 301)]
public static void Validate()
{
var db = LoadDb();
if (db == null) return;
var discovered = Discover(out _);
var so = new SerializedObject(db);
var arr = so.FindProperty("_entries");
var dbIds = new HashSet<string>();
var dupes = new List<string>();
var noKey = new List<string>();
for (int i = 0; i < arr.arraySize; i++)
{
var el = arr.GetArrayElementAtIndex(i);
string id = el.FindPropertyRelative("enemyId").stringValue;
if (string.IsNullOrEmpty(id)) { noKey.Add($"#{i}"); continue; }
if (!dbIds.Add(id)) dupes.Add(id);
if (string.IsNullOrEmpty(el.FindPropertyRelative("displayNameKey").stringValue))
noKey.Add(id);
}
var orphans = dbIds.Where(id => !discovered.ContainsKey(id)).OrderBy(s => s).ToList();
var missing = discovered.Keys.Where(id => !dbIds.Contains(id)).OrderBy(s => s).ToList();
var sb = new System.Text.StringBuilder("[图鉴校验]\n");
sb.AppendLine($"DB 条目 {arr.arraySize} · 敌人数据来源 {discovered.Count} · 已覆盖 {dbIds.Count - orphans.Count}/{discovered.Count}");
sb.AppendLine(missing.Count > 0 ? $"✗ 缺失(有敌人数据无条目){missing.Count}{string.Join(", ", missing)}" : "✓ 无缺失");
sb.AppendLine(orphans.Count > 0 ? $"⚠ 孤儿(条目无敌人数据){orphans.Count}{string.Join(", ", orphans)}" : "✓ 无孤儿");
sb.AppendLine(dupes.Count > 0 ? $"✗ 重复 enemyId {dupes.Count}{string.Join(", ", dupes)}" : "✓ 无重复");
sb.AppendLine(noKey.Count > 0 ? $"⚠ 缺 enemyId/名称 Key{string.Join(", ", noKey)}" : "✓ id/名称 Key 齐全");
Debug.Log(sb.ToString(), db);
EditorUtility.DisplayDialog("图鉴校验",
$"DB {arr.arraySize} 条 / 敌人数据 {discovered.Count} 个\n" +
$"缺失 {missing.Count} · 孤儿 {orphans.Count} · 重复 {dupes.Count}\n\n详情见 Console。",
missing.Count == 0 && orphans.Count == 0 && dupes.Count == 0 ? "全部通过" : "好");
}
// ── 发现敌人来源 ─────────────────────────────────────────────────────
private static Dictionary<string, Src> Discover(out List<string> log)
{
var map = new Dictionary<string, Src>();
log = new List<string>();
// 来源 AData/Enemies/{id}/ 子文件夹 + EnemyStatsSO
if (AssetDatabase.IsValidFolder(EnemiesDir))
{
foreach (var folder in AssetDatabase.GetSubFolders(EnemiesDir))
{
string id = folder.Substring(folder.LastIndexOf('/') + 1);
EnemyStatsSO stats = null;
foreach (var g in AssetDatabase.FindAssets("t:EnemyStatsSO", new[] { folder }))
{
stats = AssetDatabase.LoadAssetAtPath<EnemyStatsSO>(AssetDatabase.GUIDToAssetPath(g));
if (stats != null) break;
}
if (stats == null) continue; // 无 EnemyStatsSO 的文件夹不算敌人(跳过 Stats 等杂项目录)
map[id] = new Src { hp = stats.MaxHP, boss = false, from = "data" };
log.Add($"{id}data, HP={stats.MaxHP}");
}
}
// 来源 B含 EnemyBase 的预制件(权威 id优先覆盖
foreach (var g in AssetDatabase.FindAssets("t:Prefab"))
{
var go = AssetDatabase.LoadAssetAtPath<GameObject>(AssetDatabase.GUIDToAssetPath(g));
if (go == null) continue;
var eb = go.GetComponentInChildren<EnemyBase>(true);
if (eb == null || string.IsNullOrEmpty(eb.EnemyId)) continue;
int hp = eb.StatsSO != null ? eb.StatsSO.MaxHP : 0;
bool boss = eb.GetType().Name.IndexOf("Boss", System.StringComparison.OrdinalIgnoreCase) >= 0;
map[eb.EnemyId] = new Src { hp = hp, boss = boss, from = "prefab" };
log.Add($"{eb.EnemyId}prefab{(boss ? ",BOSS" : "")}, HP={hp}");
}
return map;
}
private static int SeedLocalization(List<string> ids)
{
if (ids == null || ids.Count == 0) return 0;
var zh = LocalizationFileIO.Read(Language.ChineseSimplified, LocalizationTable.UI);
var en = LocalizationFileIO.Read(Language.English, LocalizationTable.UI);
int added = 0;
foreach (var id in ids)
{
string nk = $"ENM_{id}_NAME", dk = $"ENM_{id}_DESC";
if (!zh.ContainsKey(nk)) { zh[nk] = $"敌人 {id}"; added++; }
if (!zh.ContainsKey(dk)) { zh[dk] = "(占位)该敌人的背景与弱点描述,由策划填写。"; added++; }
if (!en.ContainsKey(nk)) { en[nk] = $"Enemy {id}"; added++; }
if (!en.ContainsKey(dk)) { en[dk] = "(Placeholder) Lore and weakness, to be filled by design."; added++; }
}
if (added > 0)
{
LocalizationFileIO.Write(Language.ChineseSimplified, LocalizationTable.UI, zh);
LocalizationFileIO.Write(Language.English, LocalizationTable.UI, en);
}
return added;
}
private static BestiaryDatabaseSO LoadDb()
{
var db = AssetDatabase.LoadAssetAtPath<BestiaryDatabaseSO>(DbPath);
if (db == null)
EditorUtility.DisplayDialog("图鉴同步",
$"未找到 {DbPath}。\n请先运行「BaseGames/UI/控件库/生成图鉴(预制件 + 空表)」。", "好");
return db;
}
}
}