using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using Framework; using Gameplay.Level; using PhxhSDK.Phxh; using UnityEditor; using UnityEngine; public static class LevelMonsterCountExporter { private const string ExportFolderName = "LevelMonsterCountExports"; private const string MenuRoot = "Tools/*关卡数据工具/"; private const string FactionName = "敌方"; private const int UngroupedId = -1; private static string ExportFolderPath => Path.GetFullPath(Path.Combine(Application.dataPath, "..", ExportFolderName)); [MenuItem(MenuRoot + "关卡配置导出")] private static void ExportLevelMonsterCounts() { var levelTable = EditorTableManager.instance.tables.Level; var monsterTable = EditorTableManager.instance.tables.Monster; var monsterClassTable = EditorTableManager.instance.tables.MonsterClass; if (levelTable == null || monsterTable == null || monsterClassTable == null) { EditorUtility.DisplayDialog("错误", "无法获取关卡/怪物/怪物职业配置表", "确定"); return; } var levelList = levelTable.DataList; if (levelList == null || levelList.Count == 0) { EditorUtility.DisplayDialog("错误", "关卡配置表为空", "确定"); return; } var results = new Dictionary(); try { for (var index = 0; index < levelList.Count; index++) { var levelCfg = levelList[index]; var progress = (index + 1f) / levelList.Count; EditorUtility.DisplayProgressBar("导出关卡配置", $"处理关卡: {levelCfg.Id}", progress); var levelJsonName = levelCfg.LevelData; if (string.IsNullOrEmpty(levelJsonName)) { Debug.LogWarning($"关卡配置 LevelData 为空: {levelCfg.Id}"); continue; } var levelPath = string.Format(Constants.LEVEL_CONFIG_FORMAT_PATH, levelJsonName); if (!File.Exists(levelPath)) { Debug.LogWarning($"关卡数据不存在: {levelPath}"); continue; } LevelData levelData; try { levelData = JsonHelper.LoadJson(levelPath); } catch (Exception ex) { Debug.LogError($"解析关卡数据失败: {levelPath}\n{ex}"); continue; } if (levelData?.npcInfos == null || levelData.npcInfos.Count == 0) continue; foreach (var npc in levelData.npcInfos) { if (npc == null) continue; var monster = monsterTable.GetOrDefault(npc.id); if (monster == null) { Debug.LogWarning($"怪物表未找到 MonsterID: {npc.id} (关卡 {levelCfg.Id})"); continue; } if (!TryResolveJobCategory(monster, monsterClassTable, out var category)) continue; var groupIds = GetGroupIds(npc.groups); foreach (var groupId in groupIds) { var key = new RowKey(levelCfg.Id, groupId); if (!results.TryGetValue(key, out var count)) { count = new JobCount(); results[key] = count; } count.Add(category); } } } } finally { EditorUtility.ClearProgressBar(); } WriteCsv(results); } [MenuItem(MenuRoot + "打开关卡配置导出目录")] private static void OpenExportFolder() { Directory.CreateDirectory(ExportFolderPath); EditorUtility.RevealInFinder(ExportFolderPath); } private static void WriteCsv(Dictionary results) { Directory.CreateDirectory(ExportFolderPath); var baseFileName = $"LevelMonsterCount_{DateTime.Now:yyyyMMdd}.csv"; var savePath = GetUniqueFilePath(baseFileName); var sb = new StringBuilder(); sb.AppendLine("关卡ID,阵营,怪物组,指挥总数,支援总数,特射总数,侦察总数,压制总数,载具总数,建筑总数"); foreach (var pair in results.OrderBy(r => r.Key.LevelId).ThenBy(r => r.Key.GroupId)) { var key = pair.Key; var count = pair.Value; sb.AppendLine(string.Join(",", Csv(key.LevelId.ToString()), Csv(FactionName), Csv(key.GroupId.ToString()), Csv(count.Commander.ToString()), Csv(count.Support.ToString()), Csv(count.Shoot.ToString()), Csv(count.Scout.ToString()), Csv(count.Suppress.ToString()), Csv(count.Vehicle.ToString()), Csv(count.Building.ToString()))); } File.WriteAllText(savePath, sb.ToString(), Encoding.UTF8); Debug.Log($"关卡怪物职业统计已导出: {savePath}"); EditorUtility.RevealInFinder(savePath); } private static string GetUniqueFilePath(string fileName) { var savePath = Path.Combine(ExportFolderPath, fileName); if (!File.Exists(savePath)) return savePath; var nameWithoutExt = Path.GetFileNameWithoutExtension(fileName); var extension = Path.GetExtension(fileName); var index = 1; do { savePath = Path.Combine(ExportFolderPath, $"{nameWithoutExt}_{index}{extension}"); index++; } while (File.Exists(savePath)); return savePath; } private static IEnumerable GetGroupIds(List groups) { if (groups == null || groups.Count == 0) return new[] { UngroupedId }; return groups.Distinct().ToArray(); } private static bool TryResolveJobCategory(cfg.MonsterCfg.DataMonster monster, cfg.MonsterCfg.MonsterClass monsterClassTable, out JobCategory category) { category = JobCategory.Unknown; switch ((int)monster.NpcType) { case 0: { var classData = monsterClassTable.GetOrDefault(monster.MonsterClassID); if (classData == null) { Debug.LogWarning($"怪物职业表未找到 MonsterClassID: {monster.MonsterClassID} (MonsterID {monster.MonsterID})"); return false; } switch (classData.Job) { case cfg.CharacterCfg.Ejob.Job_Commander: category = JobCategory.Commander; return true; case cfg.CharacterCfg.Ejob.Job_Support: category = JobCategory.Support; return true; case cfg.CharacterCfg.Ejob.Job_Shoot: category = JobCategory.Shoot; return true; case cfg.CharacterCfg.Ejob.Job_Scout: category = JobCategory.Scout; return true; case cfg.CharacterCfg.Ejob.Job_Suppress: category = JobCategory.Suppress; return true; case cfg.CharacterCfg.Ejob.Job_Vehicle: category = JobCategory.Vehicle; return true; default: Debug.LogWarning($"未知职业: {classData.Job} (MonsterID {monster.MonsterID})"); return false; } } case 2: category = JobCategory.Vehicle; return true; case 3: category = JobCategory.Building; return true; default: // Debug.LogWarning($"未知NpcType: {monster.NpcType} (MonsterID {monster.MonsterID})"); return false; } } private static string Csv(string value) { if (string.IsNullOrEmpty(value)) return string.Empty; if (value.Contains(",") || value.Contains("\"") || value.Contains("\n") || value.Contains("\r")) return "\"" + value.Replace("\"", "\"\"") + "\""; return value; } private readonly struct RowKey : IEquatable { public readonly int LevelId; public readonly int GroupId; public RowKey(int levelId, int groupId) { LevelId = levelId; GroupId = groupId; } public bool Equals(RowKey other) => LevelId == other.LevelId && GroupId == other.GroupId; public override bool Equals(object obj) => obj is RowKey other && Equals(other); public override int GetHashCode() => (LevelId * 397) ^ GroupId; } private sealed class JobCount { public int Commander; public int Support; public int Shoot; public int Scout; public int Suppress; public int Vehicle; public int Building; public void Add(JobCategory category) { switch (category) { case JobCategory.Commander: Commander++; break; case JobCategory.Support: Support++; break; case JobCategory.Shoot: Shoot++; break; case JobCategory.Scout: Scout++; break; case JobCategory.Suppress: Suppress++; break; case JobCategory.Vehicle: Vehicle++; break; case JobCategory.Building: Building++; break; } } } private enum JobCategory { Unknown, Commander, Support, Shoot, Scout, Suppress, Vehicle, Building } }