【关卡】 关卡怪物导出工具

main
zhangaotian 2026-02-02 14:41:00 +08:00
parent 1276dec6db
commit 01b9b43978
2 changed files with 325 additions and 0 deletions

View File

@ -0,0 +1,314 @@
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<RowKey, JobCount>();
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<LevelData>(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<RowKey, JobCount> 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<int> GetGroupIds(List<int> 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<RowKey>
{
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
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a8dac2b2263604bf19db0c9ffc6c22d1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: