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

217 lines
11 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 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;
}
}
}