217 lines
11 KiB
C#
217 lines
11 KiB
C#
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>来源 A:Assets/_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>();
|
||
|
||
// 来源 A:Data/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;
|
||
}
|
||
}
|
||
}
|