315 lines
10 KiB
C#
315 lines
10 KiB
C#
|
|
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
|
||
|
|
}
|
||
|
|
}
|