From f2b7e41b558d3d1a68465ea64b548f4cb9268bb8 Mon Sep 17 00:00:00 2001 From: liutao <821580467@qq.com> Date: Thu, 25 Jun 2026 19:15:26 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90PVP=E8=B0=83=E8=AF=95=E5=99=A8?= =?UTF-8?q?=E3=80=91=E5=A2=9E=E5=8A=A0PVP=E8=B0=83=E8=AF=95=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Config/Data/actorcfg_actorlevel.json | 116 +- .../Data/actorcfg_energypointpurchase.json | 8 +- .../Data/modifycfg_propmodifyvalueconfig.json | 6 +- ProjectNLD/Assets/Config/PVPDebugBattle.meta | 8 + .../PVPDebugBattlePreset 1.asset | 17 + .../PVPDebugBattlePreset 1.asset.meta | 8 + .../PVPDebugBattle/PVPDebugBattlePreset.asset | 29 + .../PVPDebugBattlePreset.asset.meta | 8 + ProjectNLD/Assets/Editor/PVPDebugBattle.meta | 8 + .../PVPDebugBattle/PVPDebugBattleWindow.cs | 1248 +++++++++++++++++ .../PVPDebugBattleWindow.cs.meta | 11 + .../_Test/Editor/PVPDebugBattlePresetTests.cs | 139 ++ .../Editor/PVPDebugBattlePresetTests.cs.meta | 11 + 13 files changed, 1552 insertions(+), 65 deletions(-) create mode 100644 ProjectNLD/Assets/Config/PVPDebugBattle.meta create mode 100644 ProjectNLD/Assets/Config/PVPDebugBattle/PVPDebugBattlePreset 1.asset create mode 100644 ProjectNLD/Assets/Config/PVPDebugBattle/PVPDebugBattlePreset 1.asset.meta create mode 100644 ProjectNLD/Assets/Config/PVPDebugBattle/PVPDebugBattlePreset.asset create mode 100644 ProjectNLD/Assets/Config/PVPDebugBattle/PVPDebugBattlePreset.asset.meta create mode 100644 ProjectNLD/Assets/Editor/PVPDebugBattle.meta create mode 100644 ProjectNLD/Assets/Editor/PVPDebugBattle/PVPDebugBattleWindow.cs create mode 100644 ProjectNLD/Assets/Editor/PVPDebugBattle/PVPDebugBattleWindow.cs.meta create mode 100644 ProjectNLD/Assets/_Test/Editor/PVPDebugBattlePresetTests.cs create mode 100644 ProjectNLD/Assets/_Test/Editor/PVPDebugBattlePresetTests.cs.meta diff --git a/ProjectNLD/Assets/Config/Data/actorcfg_actorlevel.json b/ProjectNLD/Assets/Config/Data/actorcfg_actorlevel.json index 15a1bc9dd0b..73f61f4f876 100644 --- a/ProjectNLD/Assets/Config/Data/actorcfg_actorlevel.json +++ b/ProjectNLD/Assets/Config/Data/actorcfg_actorlevel.json @@ -13,349 +13,349 @@ }, { "Level": 3, - "Exp": 190, + "Exp": 200, "MaxEnergy": 210, "RewardEnergy": 60 }, { "Level": 4, - "Exp": 280, + "Exp": 290, "MaxEnergy": 215, "RewardEnergy": 60 }, { "Level": 5, - "Exp": 370, + "Exp": 390, "MaxEnergy": 220, "RewardEnergy": 60 }, { "Level": 6, - "Exp": 460, + "Exp": 480, "MaxEnergy": 225, "RewardEnergy": 60 }, { "Level": 7, - "Exp": 550, + "Exp": 580, "MaxEnergy": 230, "RewardEnergy": 60 }, { "Level": 8, - "Exp": 640, + "Exp": 670, "MaxEnergy": 235, "RewardEnergy": 60 }, { "Level": 9, - "Exp": 730, + "Exp": 770, "MaxEnergy": 240, "RewardEnergy": 60 }, { "Level": 10, - "Exp": 820, + "Exp": 860, "MaxEnergy": 245, "RewardEnergy": 60 }, { "Level": 11, - "Exp": 920, + "Exp": 960, "MaxEnergy": 250, "RewardEnergy": 60 }, { "Level": 12, - "Exp": 1010, + "Exp": 1050, "MaxEnergy": 255, "RewardEnergy": 60 }, { "Level": 13, - "Exp": 1160, + "Exp": 1210, "MaxEnergy": 260, "RewardEnergy": 60 }, { "Level": 14, - "Exp": 1290, + "Exp": 1340, "MaxEnergy": 265, "RewardEnergy": 60 }, { "Level": 15, - "Exp": 1410, + "Exp": 1480, "MaxEnergy": 270, "RewardEnergy": 60 }, { "Level": 16, - "Exp": 1540, + "Exp": 1610, "MaxEnergy": 275, "RewardEnergy": 60 }, { "Level": 17, - "Exp": 1670, + "Exp": 1740, "MaxEnergy": 280, "RewardEnergy": 60 }, { "Level": 18, - "Exp": 1800, + "Exp": 1880, "MaxEnergy": 285, "RewardEnergy": 60 }, { "Level": 19, - "Exp": 1920, + "Exp": 2010, "MaxEnergy": 290, "RewardEnergy": 60 }, { "Level": 20, - "Exp": 2050, + "Exp": 2140, "MaxEnergy": 295, "RewardEnergy": 60 }, { "Level": 21, - "Exp": 2180, + "Exp": 2280, "MaxEnergy": 300, "RewardEnergy": 60 }, { "Level": 22, - "Exp": 2310, + "Exp": 2410, "MaxEnergy": 305, "RewardEnergy": 60 }, { "Level": 23, - "Exp": 2460, + "Exp": 2570, "MaxEnergy": 310, "RewardEnergy": 60 }, { "Level": 24, - "Exp": 2610, + "Exp": 2730, "MaxEnergy": 315, "RewardEnergy": 60 }, { "Level": 25, - "Exp": 2760, + "Exp": 2890, "MaxEnergy": 320, "RewardEnergy": 60 }, { "Level": 26, - "Exp": 2920, + "Exp": 3050, "MaxEnergy": 325, "RewardEnergy": 60 }, { "Level": 27, - "Exp": 3070, + "Exp": 3210, "MaxEnergy": 330, "RewardEnergy": 60 }, { "Level": 28, - "Exp": 3220, + "Exp": 3370, "MaxEnergy": 335, "RewardEnergy": 60 }, { "Level": 29, - "Exp": 3370, + "Exp": 3530, "MaxEnergy": 340, "RewardEnergy": 60 }, { "Level": 30, - "Exp": 3530, + "Exp": 3690, "MaxEnergy": 345, "RewardEnergy": 60 }, { "Level": 31, - "Exp": 3680, + "Exp": 3850, "MaxEnergy": 350, "RewardEnergy": 60 }, { "Level": 32, - "Exp": 3830, + "Exp": 4010, "MaxEnergy": 355, "RewardEnergy": 60 }, { "Level": 33, - "Exp": 5050, + "Exp": 5280, "MaxEnergy": 360, "RewardEnergy": 60 }, { "Level": 34, - "Exp": 6260, + "Exp": 6550, "MaxEnergy": 365, "RewardEnergy": 60 }, { "Level": 35, - "Exp": 7480, + "Exp": 7820, "MaxEnergy": 370, "RewardEnergy": 60 }, { "Level": 36, - "Exp": 8690, + "Exp": 9090, "MaxEnergy": 375, "RewardEnergy": 60 }, { "Level": 37, - "Exp": 9910, + "Exp": 10360, "MaxEnergy": 380, "RewardEnergy": 60 }, { "Level": 38, - "Exp": 11120, + "Exp": 11630, "MaxEnergy": 385, "RewardEnergy": 60 }, { "Level": 39, - "Exp": 12340, + "Exp": 12900, "MaxEnergy": 390, "RewardEnergy": 60 }, { "Level": 40, - "Exp": 13550, + "Exp": 14170, "MaxEnergy": 395, "RewardEnergy": 60 }, { "Level": 41, - "Exp": 14770, + "Exp": 15440, "MaxEnergy": 400, "RewardEnergy": 60 }, { "Level": 42, - "Exp": 15980, + "Exp": 16710, "MaxEnergy": 405, "RewardEnergy": 60 }, { "Level": 43, - "Exp": 17850, + "Exp": 18670, "MaxEnergy": 410, "RewardEnergy": 60 }, { "Level": 44, - "Exp": 19720, + "Exp": 20620, "MaxEnergy": 415, "RewardEnergy": 60 }, { "Level": 45, - "Exp": 21600, + "Exp": 22580, "MaxEnergy": 420, "RewardEnergy": 60 }, { "Level": 46, - "Exp": 23470, + "Exp": 24540, "MaxEnergy": 425, "RewardEnergy": 60 }, { "Level": 47, - "Exp": 25340, + "Exp": 26490, "MaxEnergy": 430, "RewardEnergy": 60 }, { "Level": 48, - "Exp": 27210, + "Exp": 28450, "MaxEnergy": 435, "RewardEnergy": 60 }, { "Level": 49, - "Exp": 29090, + "Exp": 30410, "MaxEnergy": 440, "RewardEnergy": 60 }, { "Level": 50, - "Exp": 30960, + "Exp": 32370, "MaxEnergy": 445, "RewardEnergy": 60 }, { "Level": 51, - "Exp": 32830, + "Exp": 34320, "MaxEnergy": 450, "RewardEnergy": 60 }, { "Level": 52, - "Exp": 34700, + "Exp": 36280, "MaxEnergy": 455, "RewardEnergy": 60 }, { "Level": 53, - "Exp": 37310, + "Exp": 39010, "MaxEnergy": 460, "RewardEnergy": 60 }, { "Level": 54, - "Exp": 39910, + "Exp": 41730, "MaxEnergy": 465, "RewardEnergy": 60 }, { "Level": 55, - "Exp": 42520, + "Exp": 44450, "MaxEnergy": 470, "RewardEnergy": 60 }, { "Level": 56, - "Exp": 45120, + "Exp": 47180, "MaxEnergy": 475, "RewardEnergy": 60 }, { "Level": 57, - "Exp": 47730, + "Exp": 49900, "MaxEnergy": 480, "RewardEnergy": 60 }, { "Level": 58, - "Exp": 50330, + "Exp": 52630, "MaxEnergy": 485, "RewardEnergy": 60 }, { "Level": 59, - "Exp": 52940, + "Exp": 55350, "MaxEnergy": 490, "RewardEnergy": 60 }, { "Level": 60, - "Exp": 55550, + "Exp": 58070, "MaxEnergy": 500, "RewardEnergy": 60 } diff --git a/ProjectNLD/Assets/Config/Data/actorcfg_energypointpurchase.json b/ProjectNLD/Assets/Config/Data/actorcfg_energypointpurchase.json index 1133529d528..7781f0be403 100644 --- a/ProjectNLD/Assets/Config/Data/actorcfg_energypointpurchase.json +++ b/ProjectNLD/Assets/Config/Data/actorcfg_energypointpurchase.json @@ -2,19 +2,19 @@ { "Count": 1, "PurchaseItem": 2, - "Price": 25, + "Price": 20, "EnergyPointGet": 50 }, { "Count": 2, "PurchaseItem": 2, - "Price": 50, + "Price": 40, "EnergyPointGet": 50 }, { "Count": 3, "PurchaseItem": 2, - "Price": 75, + "Price": 60, "EnergyPointGet": 50 }, { @@ -26,7 +26,7 @@ { "Count": 5, "PurchaseItem": 2, - "Price": 200, + "Price": 150, "EnergyPointGet": 50 } ] \ No newline at end of file diff --git a/ProjectNLD/Assets/Config/Data/modifycfg_propmodifyvalueconfig.json b/ProjectNLD/Assets/Config/Data/modifycfg_propmodifyvalueconfig.json index a2908c1faee..3042bc29f84 100644 --- a/ProjectNLD/Assets/Config/Data/modifycfg_propmodifyvalueconfig.json +++ b/ProjectNLD/Assets/Config/Data/modifycfg_propmodifyvalueconfig.json @@ -374,7 +374,7 @@ 31004210 ], "ModifyValues": [ - 0.36 + 0.17 ] }, { @@ -383,7 +383,7 @@ 31004810 ], "ModifyValues": [ - 0.88 + 0.4 ] }, { @@ -392,7 +392,7 @@ 31005010 ], "ModifyValues": [ - 4.26 + 1.94 ] }, { diff --git a/ProjectNLD/Assets/Config/PVPDebugBattle.meta b/ProjectNLD/Assets/Config/PVPDebugBattle.meta new file mode 100644 index 00000000000..bf2cbbfa402 --- /dev/null +++ b/ProjectNLD/Assets/Config/PVPDebugBattle.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2983065e15c107d4f9b135ebd17d3e64 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ProjectNLD/Assets/Config/PVPDebugBattle/PVPDebugBattlePreset 1.asset b/ProjectNLD/Assets/Config/PVPDebugBattle/PVPDebugBattlePreset 1.asset new file mode 100644 index 00000000000..c3cd8ede6f3 --- /dev/null +++ b/ProjectNLD/Assets/Config/PVPDebugBattle/PVPDebugBattlePreset 1.asset @@ -0,0 +1,17 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fa6956f019c2a454bb9328b313dae5c2, type: 3} + m_Name: PVPDebugBattlePreset 1 + m_EditorClassIdentifier: + levelIdOverride: 0 + selfUnits: [] + enemyUnits: [] diff --git a/ProjectNLD/Assets/Config/PVPDebugBattle/PVPDebugBattlePreset 1.asset.meta b/ProjectNLD/Assets/Config/PVPDebugBattle/PVPDebugBattlePreset 1.asset.meta new file mode 100644 index 00000000000..2fd14f77a23 --- /dev/null +++ b/ProjectNLD/Assets/Config/PVPDebugBattle/PVPDebugBattlePreset 1.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fa39f03b82cd3104ebe1433877d004b5 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/ProjectNLD/Assets/Config/PVPDebugBattle/PVPDebugBattlePreset.asset b/ProjectNLD/Assets/Config/PVPDebugBattle/PVPDebugBattlePreset.asset new file mode 100644 index 00000000000..9a6ad34fd49 --- /dev/null +++ b/ProjectNLD/Assets/Config/PVPDebugBattle/PVPDebugBattlePreset.asset @@ -0,0 +1,29 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fa6956f019c2a454bb9328b313dae5c2, type: 3} + m_Name: PVPDebugBattlePreset + m_EditorClassIdentifier: + levelIdOverride: 0 + selfUnits: + - unitType: 0 + unitID: 0 + cellIndex: 0 + level: 1 + breakLevel: 0 + awakeLevel: 0 + skillLevel0: 1 + skillLevel1: 1 + skillLevel2: 1 + skinId: 1 + vehicleComponents: + paintSet: + enemyUnits: [] diff --git a/ProjectNLD/Assets/Config/PVPDebugBattle/PVPDebugBattlePreset.asset.meta b/ProjectNLD/Assets/Config/PVPDebugBattle/PVPDebugBattlePreset.asset.meta new file mode 100644 index 00000000000..783a049ab68 --- /dev/null +++ b/ProjectNLD/Assets/Config/PVPDebugBattle/PVPDebugBattlePreset.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4a584d2b14d3d134da3763e50f7f976a +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/ProjectNLD/Assets/Editor/PVPDebugBattle.meta b/ProjectNLD/Assets/Editor/PVPDebugBattle.meta new file mode 100644 index 00000000000..8b4d051922f --- /dev/null +++ b/ProjectNLD/Assets/Editor/PVPDebugBattle.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ced9cd9c8bdc2f54e93ea219ffd2589e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ProjectNLD/Assets/Editor/PVPDebugBattle/PVPDebugBattleWindow.cs b/ProjectNLD/Assets/Editor/PVPDebugBattle/PVPDebugBattleWindow.cs new file mode 100644 index 00000000000..5f64e48d357 --- /dev/null +++ b/ProjectNLD/Assets/Editor/PVPDebugBattle/PVPDebugBattleWindow.cs @@ -0,0 +1,1248 @@ +using System; +using System.Collections.Generic; +using Framework; +using Framework.Wrapper; +using Gameplay; +using Sirenix.OdinInspector.Editor; +using UnityEditor; +using UnityEngine; + +namespace NLD.Editor.PVPDebugBattle +{ + public static class PVPDebugBattleCellPickerUtility + { + public static HashSet GetUsedCells(List units, int currentIndex) + { + var usedCells = new HashSet(); + if (units == null) + return usedCells; + + for (int i = 0; i < units.Count; i++) + { + if (i == currentIndex) + continue; + + var unit = units[i]; + if (unit == null || unit.cellIndex < 0) + continue; + + usedCells.Add(unit.cellIndex); + } + + return usedCells; + } + + public static Vector2 GetFlatTopHexCenter(int row, int column, float hexWidth, float hexHeight) + { + var x = hexWidth * 0.5f + column * hexWidth * 0.75f; + var y = hexHeight * 0.5f + row * hexHeight; + if ((column & 1) != 0) + y += hexHeight * 0.5f; + + return new Vector2(x, y); + } + } + + public class PVPDebugBattleWindow : OdinEditorWindow + { + private const string DefaultPresetFolder = "Assets/Config/PVPDebugBattle"; + private const string LevelConfigFormatPath = "Assets/Config/Levels/{0}.json"; + private const string MapConfigFormatPath = "Assets/Config/Maps/{0}.json"; + private const float UnitIconSize = 36f; + + [SerializeField] + private PVPDebugBattlePreset preset; + + private Vector2 _scrollPosition; + private string _status = "请选择或创建 PVP 调试预设"; + + [MenuItem("Tools/*战斗工具/PVP调试战斗")] + public static void ShowWindow() + { + var window = GetWindow("PVP调试战斗"); + window.minSize = new Vector2(760, 640); + window.Show(); + } + + protected override void OnGUI() + { + EditorGUILayout.Space(6f); + DrawToolbar(); + EditorGUILayout.Space(4f); + EditorGUILayout.HelpBox(_status, MessageType.Info); + + if (preset == null) + { + EditorGUILayout.HelpBox("请先选择或创建一个 PVPDebugBattlePreset。", MessageType.Warning); + return; + } + + _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition); + DrawPresetFields(); + EditorGUILayout.EndScrollView(); + } + + private void DrawToolbar() + { + EditorGUILayout.BeginHorizontal(); + preset = (PVPDebugBattlePreset)EditorGUILayout.ObjectField("调试预设", preset, + typeof(PVPDebugBattlePreset), false); + + if (GUILayout.Button("创建新预设", GUILayout.Width(100f))) + CreatePreset(); + + if (GUILayout.Button("校验", GUILayout.Width(64f))) + ValidatePreset(); + + GUI.enabled = EditorApplication.isPlaying; + if (GUILayout.Button("进入PVP调试", GUILayout.Width(116f))) + EnterPVPDebugBattle(); + GUI.enabled = true; + EditorGUILayout.EndHorizontal(); + + if (!EditorApplication.isPlaying) + EditorGUILayout.HelpBox("进入战斗按钮仅在 Play Mode 可用;预设编辑可以在非运行时完成。", MessageType.None); + } + + private void DrawPresetFields() + { + EditorGUI.BeginChangeCheck(); + var levelId = EditorGUILayout.IntField("关卡ID覆盖", preset.levelIdOverride); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(preset, "Edit PVP Debug Level"); + preset.levelIdOverride = Math.Max(0, levelId); + EditorUtility.SetDirty(preset); + } + + EditorGUILayout.Space(8f); + DrawUnitList("己方阵容", preset.selfUnits); + EditorGUILayout.Space(8f); + DrawUnitList("敌方阵容", preset.enemyUnits); + } + + private void DrawUnitList(string title, List units) + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(title, EditorStyles.boldLabel); + + if (GUILayout.Button("添加角色", GUILayout.Width(86f))) + OpenPicker(Unit.UnitType.Charactor, unitId => AddUnit(units, PVPDebugBattleUnit.CreateCharacter(unitId))); + + if (GUILayout.Button("添加载具", GUILayout.Width(86f))) + OpenPicker(Unit.UnitType.Vehicle, unitId => AddUnit(units, PVPDebugBattleUnit.CreateVehicle(unitId))); + + EditorGUILayout.EndHorizontal(); + + if (units == null || units.Count == 0) + { + EditorGUILayout.HelpBox("当前阵容为空。", MessageType.None); + EditorGUILayout.EndVertical(); + return; + } + + for (int i = 0; i < units.Count; i++) + { + DrawUnitItem(units, i); + if (i < units.Count - 1) + EditorGUILayout.Space(4f); + } + + EditorGUILayout.EndVertical(); + } + + private void DrawUnitItem(List units, int index) + { + var unit = units[index]; + if (unit == null) + { + EditorGUILayout.HelpBox("单位数据为空。", MessageType.Error); + if (GUILayout.Button("移除空单位")) + RemoveUnit(units, index); + return; + } + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField($"{index + 1}. {GetUnitTypeName(unit.unitType)} {unit.unitID} {GetUnitName(unit)}", + EditorStyles.boldLabel); + + if (GUILayout.Button("选择", GUILayout.Width(58f))) + OpenPicker(unit.unitType, unitId => SetUnitId(unit, unitId)); + + if (GUILayout.Button("删除", GUILayout.Width(58f))) + { + RemoveUnit(units, index); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + return; + } + + EditorGUILayout.EndHorizontal(); + + if (!IsSupportedUnitType(unit.unitType)) + { + EditorGUILayout.HelpBox("仅支持角色和载具,请删除后重新添加。", MessageType.Error); + EditorGUILayout.EndVertical(); + return; + } + + EditorGUILayout.BeginHorizontal(); + EditorGUI.BeginChangeCheck(); + var nextCellIndex = EditorGUILayout.IntField("Cell Index", unit.cellIndex); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(preset, "Edit PVP Debug Cell"); + unit.cellIndex = nextCellIndex; + EditorUtility.SetDirty(preset); + } + + if (GUILayout.Button("地图选择", GUILayout.Width(78f))) + OpenCellPicker(units, index, unit); + EditorGUILayout.EndHorizontal(); + + if (unit.unitType == Unit.UnitType.Charactor) + DrawCharacterCultivate(unit); + else + DrawVehicleCultivate(unit); + + EditorGUILayout.EndVertical(); + } + + private void DrawCharacterCultivate(PVPDebugBattleUnit unit) + { + EditorGUILayout.LabelField("角色养成", EditorStyles.boldLabel); + + EditorGUI.BeginChangeCheck(); + var level = UIntField("Level", unit.level); + var breakLevel = UIntField("Break Level", unit.breakLevel); + var awakeLevel = UIntField("Awake Level", unit.awakeLevel); + var skillLevel0 = UIntField("Skill Level 0", unit.skillLevel0); + var skillLevel1 = UIntField("Skill Level 1", unit.skillLevel1); + var skillLevel2 = UIntField("Skill Level 2", unit.skillLevel2); + EditorGUILayout.BeginHorizontal(); + var skinId = UIntField("Skin Id", unit.skinId); + if (GUILayout.Button("选择", GUILayout.Width(58f))) + OpenSkinPicker(unit); + EditorGUILayout.EndHorizontal(); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(preset, "Edit PVP Debug Character"); + unit.level = level; + unit.breakLevel = breakLevel; + unit.awakeLevel = awakeLevel; + unit.skillLevel0 = skillLevel0; + unit.skillLevel1 = skillLevel1; + unit.skillLevel2 = skillLevel2; + unit.skinId = skinId; + EditorUtility.SetDirty(preset); + } + } + + private void OpenSkinPicker(PVPDebugBattleUnit unit) + { + if (unit == null || unit.unitType != Unit.UnitType.Charactor) + return; + + if (unit.unitID == Team.NoneID) + { + EditorUtility.DisplayDialog("PVP调试战斗", "请先选择角色,再选择皮肤。", "确定"); + return; + } + + PVPDebugBattleSkinPickerWindow.Open(unit.unitID, skinId => SetCharacterSkin(unit, skinId)); + } + + private void DrawVehicleCultivate(PVPDebugBattleUnit unit) + { + EditorGUILayout.LabelField("载具养成", EditorStyles.boldLabel); + DrawLongList("Vehicle Components", unit.vehicleComponents); + DrawUIntList("Paint Set", unit.paintSet); + } + + private void DrawLongList(string label, List values) + { + if (values == null) + return; + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(label, EditorStyles.boldLabel); + if (GUILayout.Button("+", GUILayout.Width(24f))) + { + Undo.RecordObject(preset, "Add PVP Debug Vehicle Value"); + values.Add(0); + EditorUtility.SetDirty(preset); + } + EditorGUILayout.EndHorizontal(); + + for (int i = 0; i < values.Count; i++) + { + EditorGUILayout.BeginHorizontal(); + EditorGUI.BeginChangeCheck(); + var next = EditorGUILayout.LongField(i.ToString(), values[i]); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(preset, "Edit PVP Debug Vehicle Value"); + values[i] = next; + EditorUtility.SetDirty(preset); + } + + if (GUILayout.Button("-", GUILayout.Width(24f))) + { + Undo.RecordObject(preset, "Remove PVP Debug Vehicle Value"); + values.RemoveAt(i); + EditorUtility.SetDirty(preset); + EditorGUILayout.EndHorizontal(); + break; + } + + EditorGUILayout.EndHorizontal(); + } + + EditorGUILayout.EndVertical(); + } + + private void DrawUIntList(string label, List values) + { + if (values == null) + return; + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(label, EditorStyles.boldLabel); + if (GUILayout.Button("+", GUILayout.Width(24f))) + { + Undo.RecordObject(preset, "Add PVP Debug Paint"); + values.Add(0); + EditorUtility.SetDirty(preset); + } + EditorGUILayout.EndHorizontal(); + + for (int i = 0; i < values.Count; i++) + { + EditorGUILayout.BeginHorizontal(); + EditorGUI.BeginChangeCheck(); + var next = UIntField(i.ToString(), values[i]); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(preset, "Edit PVP Debug Paint"); + values[i] = next; + EditorUtility.SetDirty(preset); + } + + if (GUILayout.Button("-", GUILayout.Width(24f))) + { + Undo.RecordObject(preset, "Remove PVP Debug Paint"); + values.RemoveAt(i); + EditorUtility.SetDirty(preset); + EditorGUILayout.EndHorizontal(); + break; + } + + EditorGUILayout.EndHorizontal(); + } + + EditorGUILayout.EndVertical(); + } + + private void OpenPicker(Unit.UnitType unitType, Action onSelected) + { + if (!IsSupportedUnitType(unitType)) + { + EditorUtility.DisplayDialog("PVP调试战斗", "仅支持角色和载具。", "确定"); + return; + } + + PVPDebugBattleUnitPickerWindow.Open(unitType, onSelected); + } + + private void AddUnit(List units, PVPDebugBattleUnit unit) + { + if (units == null || unit == null) + return; + + Undo.RecordObject(preset, "Add PVP Debug Unit"); + units.Add(unit); + EditorUtility.SetDirty(preset); + Repaint(); + } + + private void RemoveUnit(List units, int index) + { + if (units == null || index < 0 || index >= units.Count) + return; + + Undo.RecordObject(preset, "Remove PVP Debug Unit"); + units.RemoveAt(index); + EditorUtility.SetDirty(preset); + Repaint(); + } + + private void SetUnitId(PVPDebugBattleUnit unit, uint unitId) + { + if (unit == null) + return; + + Undo.RecordObject(preset, "Select PVP Debug Unit"); + unit.unitID = unitId; + if (unit.unitType == Unit.UnitType.Charactor) + unit.skinId = 1; + EditorUtility.SetDirty(preset); + Repaint(); + } + + private void SetCharacterSkin(PVPDebugBattleUnit unit, uint skinId) + { + if (unit == null) + return; + + Undo.RecordObject(preset, "Select PVP Debug Skin"); + unit.skinId = skinId; + EditorUtility.SetDirty(preset); + Repaint(); + } + + private void OpenCellPicker(List units, int index, PVPDebugBattleUnit unit) + { + if (preset == null || unit == null) + return; + + var levelId = ResolvePresetLevelId(); + var usedCells = PVPDebugBattleCellPickerUtility.GetUsedCells(units, index); + PVPDebugBattleCellPickerWindow.Open(levelId, unit.cellIndex, usedCells, cellIndex => SetCellIndex(unit, cellIndex)); + } + + private int ResolvePresetLevelId() + { + if (preset != null && preset.levelIdOverride > 0) + return preset.levelIdOverride; + + try + { + return EditorTableManager.instance.tables.GlobalConfig.PVPLevelID; + } + catch (Exception) + { + return 0; + } + } + + private void SetCellIndex(PVPDebugBattleUnit unit, int cellIndex) + { + if (unit == null) + return; + + Undo.RecordObject(preset, "Select PVP Debug Cell"); + unit.cellIndex = cellIndex; + EditorUtility.SetDirty(preset); + Repaint(); + } + + private void CreatePreset() + { + EnsureFolder(DefaultPresetFolder); + + var asset = ScriptableObject.CreateInstance(); + asset.name = "PVPDebugBattlePreset"; + var path = AssetDatabase.GenerateUniqueAssetPath(DefaultPresetFolder + "/PVPDebugBattlePreset.asset"); + AssetDatabase.CreateAsset(asset, path); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + + preset = asset; + Selection.activeObject = asset; + _status = "已创建预设: " + path; + } + + private void ValidatePreset() + { + if (preset == null) + { + _status = "未选择预设"; + EditorUtility.DisplayDialog("PVP调试战斗", _status, "确定"); + return; + } + + if (preset.Validate(out var error)) + { + _status = "预设校验通过"; + EditorUtility.DisplayDialog("PVP调试战斗", _status, "确定"); + } + else + { + _status = error; + EditorUtility.DisplayDialog("PVP调试战斗", "预设校验失败:\n" + error, "确定"); + } + } + + private void EnterPVPDebugBattle() + { + if (!EditorApplication.isPlaying) + { + _status = "请先运行游戏,再进入 PVP 调试战斗"; + EditorUtility.DisplayDialog("PVP调试战斗", _status, "确定"); + return; + } + + if (preset == null) + { + _status = "未选择预设"; + EditorUtility.DisplayDialog("PVP调试战斗", _status, "确定"); + return; + } + + if (PVPDebugBattleLauncher.TryEnter(preset, out var error)) + { + _status = "已进入 PVP 调试战斗"; + return; + } + + _status = error; + EditorUtility.DisplayDialog("PVP调试战斗", "进入失败:\n" + error, "确定"); + } + + private static bool IsSupportedUnitType(Unit.UnitType unitType) + { + return unitType == Unit.UnitType.Charactor || unitType == Unit.UnitType.Vehicle; + } + + private static uint UIntField(string label, uint value) + { + var next = EditorGUILayout.LongField(label, value); + if (next < 0) + return 0; + + if (next > uint.MaxValue) + return uint.MaxValue; + + return (uint)next; + } + + private static string GetUnitTypeName(Unit.UnitType unitType) + { + switch (unitType) + { + case Unit.UnitType.Charactor: + return "角色"; + case Unit.UnitType.Vehicle: + return "载具"; + default: + return "不支持"; + } + } + + private static string GetUnitName(PVPDebugBattleUnit unit) + { + if (unit == null || TableManager.Instance?.Tables == null) + return string.Empty; + + try + { + if (unit.unitType == Unit.UnitType.Charactor) + { + var cfg = TableManager.Instance.Tables.CharacterAttri.GetOrDefault((int)unit.unitID); + return cfg == null ? string.Empty : GetCharacterDisplayName(cfg); + } + + if (unit.unitType == Unit.UnitType.Vehicle) + { + var cfg = TableManager.Instance.Tables.VehicleConfig.GetOrDefault((int)unit.unitID); + return cfg == null ? string.Empty : cfg.Name; + } + } + catch (Exception) + { + return string.Empty; + } + + return string.Empty; + } + + private static string GetCharacterDisplayName(cfg.CharacterCfg.DataCharacterAttri cfg) + { + if (cfg == null) + return string.Empty; + + var name = SafeGetCharacterName(cfg.RoleID); + if (!string.IsNullOrEmpty(name)) + return name; + + if (!string.IsNullOrEmpty(cfg.NameRead)) + return cfg.NameRead; + + return cfg.NameID; + } + + private static string SafeGetCharacterName(int roleId) + { + try + { + return CommonUtils.GetCharacterName(roleId); + } + catch (Exception) + { + return string.Empty; + } + } + + private static void EnsureFolder(string folder) + { + if (AssetDatabase.IsValidFolder(folder)) + return; + + var parts = folder.Split('/'); + var current = parts[0]; + for (int i = 1; i < parts.Length; i++) + { + var next = current + "/" + parts[i]; + if (!AssetDatabase.IsValidFolder(next)) + AssetDatabase.CreateFolder(current, parts[i]); + + current = next; + } + } + + private sealed class PVPDebugBattleCellPickerWindow : EditorWindow + { + private const float MinHexWidth = 28f; + private const float MaxHexWidth = 48f; + private const float GridPadding = 12f; + private const float HexHeightScale = 0.8660254f; + + private readonly Color _normalColor = new Color(0.24f, 0.24f, 0.24f, 1f); + private readonly Color _prepareColor = new Color(0.22f, 0.46f, 0.24f, 1f); + private readonly Color _defendColor = new Color(0.22f, 0.42f, 0.58f, 1f); + private readonly Color _sharedRegionColor = new Color(0.24f, 0.46f, 0.48f, 1f); + private readonly Color _usedColor = new Color(0.72f, 0.43f, 0.16f, 1f); + private readonly Color _selectedColor = new Color(0.96f, 0.78f, 0.22f, 1f); + private readonly Color _borderColor = new Color(0.08f, 0.08f, 0.08f, 1f); + + private int _levelId; + private int _currentCellIndex; + private HashSet _usedCells = new HashSet(); + private Action _onSelected; + private Vector2 _scrollPosition; + private string _error = string.Empty; + private MapData _mapData; + private Gameplay.Level.LevelData _levelData; + private HashSet _prepareCells = new HashSet(); + private HashSet _defendCells = new HashSet(); + private GUIStyle _cellLabelStyle; + private GUIStyle _legendLabelStyle; + + public static void Open(int levelId, int currentCellIndex, HashSet usedCells, Action onSelected) + { + var window = CreateInstance(); + window._levelId = levelId; + window._currentCellIndex = currentCellIndex; + window._usedCells = usedCells ?? new HashSet(); + window._onSelected = onSelected; + window.titleContent = new GUIContent("选择 CellIndex"); + window.minSize = new Vector2(560f, 560f); + window.LoadData(); + window.ShowUtility(); + } + + private void OnGUI() + { + EnsureStyles(); + EditorGUILayout.Space(6f); + DrawHeader(); + + if (!string.IsNullOrEmpty(_error)) + { + EditorGUILayout.HelpBox(_error, MessageType.Error); + if (GUILayout.Button("刷新")) + LoadData(); + return; + } + + DrawLegend(); + DrawGrid(); + } + + private void DrawHeader() + { + EditorGUILayout.LabelField("关卡ID", _levelId.ToString()); + if (_mapData != null) + { + EditorGUILayout.LabelField("地图尺寸", _mapData.Width + " x " + _mapData.Height); + EditorGUILayout.LabelField("当前 Cell", _currentCellIndex.ToString()); + } + } + + private void DrawLegend() + { + EditorGUILayout.Space(4f); + EditorGUILayout.BeginHorizontal(); + DrawLegendItem(_selectedColor, "当前"); + DrawLegendItem(_usedColor, "同阵营已占用"); + DrawLegendItem(_prepareColor, "准备区"); + DrawLegendItem(_defendColor, "PVP防守区"); + DrawLegendItem(_normalColor, "普通"); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.Space(4f); + } + + private void DrawLegendItem(Color color, string label) + { + var rect = GUILayoutUtility.GetRect(14f, 14f, GUILayout.Width(14f), GUILayout.Height(14f)); + EditorGUI.DrawRect(rect, color); + GUILayout.Label(label, _legendLabelStyle, GUILayout.Width(92f)); + } + + private void DrawGrid() + { + if (_mapData == null) + return; + + var width = _mapData.Width; + var height = _mapData.Height; + if (width <= 0 || height <= 0) + return; + + _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition); + var availableWidth = Mathf.Max(position.width - GridPadding * 2f, width * MinHexWidth); + var hexWidth = Mathf.Clamp(availableWidth / (1f + (width - 1) * 0.75f), MinHexWidth, MaxHexWidth); + var hexHeight = hexWidth * HexHeightScale; + var gridWidth = hexWidth * (1f + (width - 1) * 0.75f); + var gridHeight = hexHeight * (height + 0.5f); + var gridRect = GUILayoutUtility.GetRect(gridWidth, gridHeight, GUILayout.ExpandWidth(false)); + + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + var cellIndex = row * width + column; + var center = PVPDebugBattleCellPickerUtility.GetFlatTopHexCenter(row, column, hexWidth, hexHeight); + center += gridRect.position; + DrawCell(center, cellIndex, hexWidth, hexHeight); + } + } + + EditorGUILayout.EndScrollView(); + } + + private void DrawCell(Vector2 center, int cellIndex, float hexWidth, float hexHeight) + { + var points = GetFlatTopHexPoints(center, hexWidth, hexHeight); + var previousColor = Handles.color; + Handles.color = GetCellColor(cellIndex); + Handles.DrawAAConvexPolygon(points); + Handles.color = _borderColor; + Handles.DrawAAPolyLine(1.5f, points[0], points[1], points[2], points[3], points[4], points[5], points[0]); + Handles.color = previousColor; + + if (hexWidth >= 34f) + { + var labelRect = new Rect(center.x - hexWidth * 0.3f, center.y - 8f, hexWidth * 0.6f, 16f); + GUI.Label(labelRect, cellIndex.ToString(), _cellLabelStyle); + } + + var evt = Event.current; + if (evt.type == EventType.MouseDown && evt.button == 0 && ContainsPoint(points, evt.mousePosition)) + { + _onSelected?.Invoke(cellIndex); + evt.Use(); + Close(); + } + } + + private static Vector3[] GetFlatTopHexPoints(Vector2 center, float hexWidth, float hexHeight) + { + var halfWidth = hexWidth * 0.5f; + var quarterWidth = hexWidth * 0.25f; + var halfHeight = hexHeight * 0.5f; + return new[] + { + new Vector3(center.x - halfWidth, center.y, 0f), + new Vector3(center.x - quarterWidth, center.y - halfHeight, 0f), + new Vector3(center.x + quarterWidth, center.y - halfHeight, 0f), + new Vector3(center.x + halfWidth, center.y, 0f), + new Vector3(center.x + quarterWidth, center.y + halfHeight, 0f), + new Vector3(center.x - quarterWidth, center.y + halfHeight, 0f) + }; + } + + private static bool ContainsPoint(Vector3[] polygon, Vector2 point) + { + var inside = false; + for (int i = 0, j = polygon.Length - 1; i < polygon.Length; j = i++) + { + var pi = polygon[i]; + var pj = polygon[j]; + if (((pi.y > point.y) != (pj.y > point.y)) && + point.x < (pj.x - pi.x) * (point.y - pi.y) / (pj.y - pi.y) + pi.x) + { + inside = !inside; + } + } + + return inside; + } + + private Color GetCellColor(int cellIndex) + { + if (cellIndex == _currentCellIndex) + return _selectedColor; + + if (_usedCells.Contains(cellIndex)) + return _usedColor; + + var inPrepare = _prepareCells.Contains(cellIndex); + var inDefend = _defendCells.Contains(cellIndex); + if (inPrepare && inDefend) + return _sharedRegionColor; + + if (inPrepare) + return _prepareColor; + + if (inDefend) + return _defendColor; + + return _normalColor; + } + + private void LoadData() + { + _error = string.Empty; + _mapData = null; + _levelData = null; + _prepareCells.Clear(); + _defendCells.Clear(); + + if (_levelId <= 0) + { + _error = "PVP关卡ID无效,无法加载地图。"; + return; + } + + try + { + var levelCfg = EditorTableManager.instance.tables.Level.GetOrDefault(_levelId); + if (levelCfg == null) + { + _error = "找不到关卡配置,Level ID: " + _levelId; + return; + } + + if (string.IsNullOrEmpty(levelCfg.LevelData) || string.IsNullOrEmpty(levelCfg.MapData)) + { + _error = "关卡配置缺少 LevelData 或 MapData 文件名,Level ID: " + _levelId; + return; + } + + var levelPath = string.Format(LevelConfigFormatPath, levelCfg.LevelData); + var mapPath = string.Format(MapConfigFormatPath, levelCfg.MapData); + var levelAsset = AssetDatabase.LoadAssetAtPath(levelPath); + var mapAsset = AssetDatabase.LoadAssetAtPath(mapPath); + if (levelAsset == null) + { + _error = "加载 LevelData 失败: " + levelPath; + return; + } + + if (mapAsset == null) + { + _error = "加载 MapData 失败: " + mapPath; + return; + } + + _levelData = JsonUtility.FromJson(levelAsset.text); + _mapData = JsonUtility.FromJson(mapAsset.text); + if (_levelData == null) + { + _error = "解析 LevelData 失败: " + levelPath; + return; + } + + if (_mapData == null || _mapData.Width <= 0 || _mapData.Height <= 0) + { + _error = "解析 MapData 失败或地图尺寸无效: " + mapPath; + _mapData = null; + return; + } + + _prepareCells = BuildRegionSet(_mapData, _levelData.preparingRegionId); + _defendCells = BuildRegionSet(_mapData, _levelData.pvpDefendRegionId); + } + catch (Exception e) + { + _error = "加载地图数据异常: " + e.Message; + _mapData = null; + _levelData = null; + } + } + + private static HashSet BuildRegionSet(MapData mapData, int regionId) + { + var result = new HashSet(); + if (mapData == null || mapData.regions == null || regionId < 0 || regionId >= mapData.regions.Count) + return result; + + var region = mapData.regions[regionId]; + if (region == null || region.positions == null) + return result; + + for (int i = 0; i < region.positions.Count; i++) + { + var cellIndex = region.positions[i]; + if (cellIndex >= 0) + result.Add(cellIndex); + } + + return result; + } + + private void EnsureStyles() + { + if (_cellLabelStyle == null) + { + _cellLabelStyle = new GUIStyle(EditorStyles.miniLabel) + { + alignment = TextAnchor.MiddleCenter, + normal = { textColor = Color.white } + }; + } + + if (_legendLabelStyle == null) + { + _legendLabelStyle = new GUIStyle(EditorStyles.miniLabel) + { + alignment = TextAnchor.MiddleLeft + }; + } + } + } + + private sealed class PVPDebugBattleSkinPickerWindow : EditorWindow + { + private readonly List _choices = new List(16); + private uint _roleId; + private Action _onSelected; + private Vector2 _scrollPosition; + private string _searchText = string.Empty; + private string _error = string.Empty; + + public static void Open(uint roleId, Action onSelected) + { + var window = CreateInstance(); + window._roleId = roleId; + window._onSelected = onSelected; + window.titleContent = new GUIContent("选择皮肤"); + window.minSize = new Vector2(520f, 520f); + window.BuildChoices(); + window.ShowUtility(); + } + + private void OnGUI() + { + EditorGUILayout.Space(6f); + EditorGUILayout.LabelField("角色ID", _roleId.ToString()); + _searchText = EditorGUILayout.TextField("搜索", _searchText); + + if (!string.IsNullOrEmpty(_error)) + { + EditorGUILayout.HelpBox(_error, MessageType.Warning); + if (GUILayout.Button("刷新")) + BuildChoices(); + + return; + } + + _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition); + for (int i = 0; i < _choices.Count; i++) + { + var choice = _choices[i]; + if (!IsMatch(choice)) + continue; + + DrawChoice(choice); + } + EditorGUILayout.EndScrollView(); + } + + private void BuildChoices() + { + _choices.Clear(); + _error = string.Empty; + + if (TableManager.Instance?.Tables == null) + { + _error = "TableManager 尚未加载配置表。请在 Play Mode 且主流程初始化完成后再打开选择窗口。"; + return; + } + + var list = CommonUtils.GetCharacterSkinCfgList((int)_roleId); + for (int i = 0; i < list.Count; i++) + { + var cfg = list[i]; + _choices.Add(new SkinChoice((uint)cfg.SkinID, GetSkinDisplayName(cfg), + SafeGetSkinIconPath(cfg.RoleID, cfg.SkinID))); + } + + if (_choices.Count == 0) + _error = "找不到该角色的皮肤配置,角色ID: " + _roleId; + } + + private void DrawChoice(SkinChoice choice) + { + var rect = GUILayoutUtility.GetRect(0f, 48f, GUILayout.ExpandWidth(true)); + if (Event.current.type == EventType.Repaint) + EditorStyles.helpBox.Draw(rect, GUIContent.none, false, false, false, false); + + var iconRect = new Rect(rect.x + 6f, rect.y + 6f, UnitIconSize, UnitIconSize); + var icon = choice.GetIcon(); + if (icon != null) + GUI.DrawTexture(iconRect, icon, ScaleMode.ScaleToFit); + else + GUI.Box(iconRect, GUIContent.none); + + var textRect = new Rect(iconRect.xMax + 8f, rect.y + 5f, rect.width - iconRect.width - 22f, 18f); + EditorGUI.LabelField(textRect, "Skin " + choice.SkinId + " " + choice.Name, EditorStyles.boldLabel); + + var pathRect = new Rect(textRect.x, textRect.yMax + 2f, textRect.width, 18f); + EditorGUI.LabelField(pathRect, choice.IconPath, EditorStyles.miniLabel); + + if (GUI.Button(rect, GUIContent.none, GUIStyle.none)) + { + _onSelected?.Invoke(choice.SkinId); + Close(); + } + } + + private bool IsMatch(SkinChoice choice) + { + if (string.IsNullOrEmpty(_searchText)) + return true; + + return choice.SkinId.ToString().Contains(_searchText) || + (!string.IsNullOrEmpty(choice.Name) && + choice.Name.IndexOf(_searchText, StringComparison.OrdinalIgnoreCase) >= 0); + } + + private static string GetSkinDisplayName(cfg.CharacterCfg.DataCharacterSkin cfg) + { + if (cfg == null) + return string.Empty; + + if (!string.IsNullOrEmpty(cfg.NickName)) + return cfg.NickName; + + if (!string.IsNullOrEmpty(cfg.Name)) + return cfg.Name; + + return cfg.NameID; + } + + private static string SafeGetSkinIconPath(int roleId, int skinId) + { + try + { + return CommonUtils.GetCharacterPicPath(roleId, CommonUtils.ECharacterPicPathType.HeadPic, skinId); + } + catch (Exception) + { + return string.Empty; + } + } + + private sealed class SkinChoice + { + public readonly uint SkinId; + public readonly string Name; + public readonly string IconPath; + private Texture2D _icon; + private bool _iconLoaded; + + public SkinChoice(uint skinId, string name, string iconPath) + { + SkinId = skinId; + Name = name; + IconPath = iconPath; + } + + public Texture2D GetIcon() + { + if (_iconLoaded) + return _icon; + + _iconLoaded = true; + if (!string.IsNullOrEmpty(IconPath)) + _icon = AssetDatabase.LoadAssetAtPath(IconPath); + + return _icon; + } + } + } + + private sealed class PVPDebugBattleUnitPickerWindow : EditorWindow + { + private readonly List _choices = new List(256); + private Unit.UnitType _unitType; + private Action _onSelected; + private Vector2 _scrollPosition; + private string _searchText = string.Empty; + private string _error = string.Empty; + + public static void Open(Unit.UnitType unitType, Action onSelected) + { + var window = CreateInstance(); + window._unitType = unitType; + window._onSelected = onSelected; + window.titleContent = new GUIContent(unitType == Unit.UnitType.Charactor ? "选择角色" : "选择载具"); + window.minSize = new Vector2(520f, 520f); + window.BuildChoices(); + window.ShowUtility(); + } + + private void OnGUI() + { + EditorGUILayout.Space(6f); + _searchText = EditorGUILayout.TextField("搜索", _searchText); + + if (!string.IsNullOrEmpty(_error)) + { + EditorGUILayout.HelpBox(_error, MessageType.Warning); + if (GUILayout.Button("刷新")) + BuildChoices(); + + return; + } + + _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition); + for (int i = 0; i < _choices.Count; i++) + { + var choice = _choices[i]; + if (!IsMatch(choice)) + continue; + + DrawChoice(choice); + } + EditorGUILayout.EndScrollView(); + } + + private void BuildChoices() + { + _choices.Clear(); + _error = string.Empty; + + if (TableManager.Instance?.Tables == null) + { + _error = "TableManager 尚未加载配置表。请在 Play Mode 且主流程初始化完成后再打开选择窗口。"; + return; + } + + if (_unitType == Unit.UnitType.Charactor) + { + var list = TableManager.Instance.Tables.CharacterAttri.DataList; + for (int i = 0; i < list.Count; i++) + { + var cfg = list[i]; + _choices.Add(new UnitChoice((uint)cfg.RoleID, GetCharacterDisplayName(cfg), + SafeGetCharacterIconPath((uint)cfg.RoleID))); + } + } + else if (_unitType == Unit.UnitType.Vehicle) + { + var list = TableManager.Instance.Tables.VehicleConfig.DataList; + for (int i = 0; i < list.Count; i++) + { + var cfg = list[i]; + _choices.Add(new UnitChoice((uint)cfg.ID, cfg.Name, SafeGetVehicleIconPath((uint)cfg.ID))); + } + } + } + + private void DrawChoice(UnitChoice choice) + { + var rect = GUILayoutUtility.GetRect(0f, 48f, GUILayout.ExpandWidth(true)); + if (Event.current.type == EventType.Repaint) + EditorStyles.helpBox.Draw(rect, GUIContent.none, false, false, false, false); + + var iconRect = new Rect(rect.x + 6f, rect.y + 6f, UnitIconSize, UnitIconSize); + var icon = choice.GetIcon(); + if (icon != null) + GUI.DrawTexture(iconRect, icon, ScaleMode.ScaleToFit); + else + GUI.Box(iconRect, GUIContent.none); + + var textRect = new Rect(iconRect.xMax + 8f, rect.y + 5f, rect.width - iconRect.width - 22f, 18f); + EditorGUI.LabelField(textRect, choice.Id + " " + choice.Name, EditorStyles.boldLabel); + + var pathRect = new Rect(textRect.x, textRect.yMax + 2f, textRect.width, 18f); + EditorGUI.LabelField(pathRect, choice.IconPath, EditorStyles.miniLabel); + + if (GUI.Button(rect, GUIContent.none, GUIStyle.none)) + { + _onSelected?.Invoke(choice.Id); + Close(); + } + } + + private bool IsMatch(UnitChoice choice) + { + if (string.IsNullOrEmpty(_searchText)) + return true; + + return choice.Id.ToString().Contains(_searchText) || + (!string.IsNullOrEmpty(choice.Name) && + choice.Name.IndexOf(_searchText, StringComparison.OrdinalIgnoreCase) >= 0); + } + + private static string SafeGetCharacterIconPath(uint unitId) + { + try + { + return CommonUtils.GetDefaultHeadPathByUid(unitId); + } + catch (Exception) + { + return string.Empty; + } + } + + private static string SafeGetVehicleIconPath(uint unitId) + { + try + { + return CommonUtils.GetDefaultVehicleHeadPath(unitId); + } + catch (Exception) + { + return string.Empty; + } + } + + private sealed class UnitChoice + { + public readonly uint Id; + public readonly string Name; + public readonly string IconPath; + private Texture2D _icon; + private bool _iconLoaded; + + public UnitChoice(uint id, string name, string iconPath) + { + Id = id; + Name = name; + IconPath = iconPath; + } + + public Texture2D GetIcon() + { + if (_iconLoaded) + return _icon; + + _iconLoaded = true; + if (!string.IsNullOrEmpty(IconPath)) + _icon = AssetDatabase.LoadAssetAtPath(IconPath); + + return _icon; + } + } + } + } +} diff --git a/ProjectNLD/Assets/Editor/PVPDebugBattle/PVPDebugBattleWindow.cs.meta b/ProjectNLD/Assets/Editor/PVPDebugBattle/PVPDebugBattleWindow.cs.meta new file mode 100644 index 00000000000..891e9dedbb7 --- /dev/null +++ b/ProjectNLD/Assets/Editor/PVPDebugBattle/PVPDebugBattleWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6677c6aebaf2c8e45bdc3aec90d9e3e2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ProjectNLD/Assets/_Test/Editor/PVPDebugBattlePresetTests.cs b/ProjectNLD/Assets/_Test/Editor/PVPDebugBattlePresetTests.cs new file mode 100644 index 00000000000..37f28349f31 --- /dev/null +++ b/ProjectNLD/Assets/_Test/Editor/PVPDebugBattlePresetTests.cs @@ -0,0 +1,139 @@ +using NUnit.Framework; +using Framework.Wrapper; +using NLD.Editor.PVPDebugBattle; + +public class PVPDebugBattlePresetTests +{ + [Test] + public void Validate_ReturnsError_WhenSelfTeamIsEmpty() + { + var preset = CreatePreset(); + preset.selfUnits.Clear(); + + var valid = PVPDebugBattleValidator.Validate(preset, false, out var error); + + Assert.IsFalse(valid); + StringAssert.Contains("己方阵容为空", error); + } + + [Test] + public void Validate_ReturnsError_WhenCellIndexIsInvalid() + { + var preset = CreatePreset(); + preset.enemyUnits[0].cellIndex = -1; + + var valid = PVPDebugBattleValidator.Validate(preset, false, out var error); + + Assert.IsFalse(valid); + StringAssert.Contains("敌方第1个单位 cellIndex 无效", error); + } + + [Test] + public void Validate_ReturnsError_WhenSameSideCellIndexIsDuplicated() + { + var preset = CreatePreset(); + preset.selfUnits.Add(new PVPDebugBattleUnit + { + unitType = Unit.UnitType.Charactor, + unitID = 1002, + cellIndex = preset.selfUnits[0].cellIndex + }); + + var valid = PVPDebugBattleValidator.Validate(preset, false, out var error); + + Assert.IsFalse(valid); + StringAssert.Contains("己方 cellIndex 重复", error); + } + + [Test] + public void Validate_ReturnsTrue_ForMinimalValidPreset() + { + var preset = CreatePreset(); + + var valid = PVPDebugBattleValidator.Validate(preset, false, out var error); + + Assert.IsTrue(valid); + Assert.IsEmpty(error); + } + + [Test] + public void CreateCharacter_SetsCharacterTypeAndUnitId() + { + var unit = PVPDebugBattleUnit.CreateCharacter(1001); + + Assert.AreEqual(Unit.UnitType.Charactor, unit.unitType); + Assert.AreEqual(1001, unit.unitID); + Assert.AreEqual(1, unit.level); + Assert.AreEqual(1, unit.skillLevel0); + Assert.AreEqual(1, unit.skillLevel1); + Assert.AreEqual(1, unit.skillLevel2); + } + + [Test] + public void CreateVehicle_SetsVehicleTypeAndUnitId() + { + var unit = PVPDebugBattleUnit.CreateVehicle(2001); + + Assert.AreEqual(Unit.UnitType.Vehicle, unit.unitType); + Assert.AreEqual(2001, unit.unitID); + Assert.IsNotNull(unit.vehicleComponents); + Assert.IsNotNull(unit.paintSet); + } + + [Test] + public void GetUsedCells_ExcludesCurrentUnit() + { + var units = new System.Collections.Generic.List + { + new PVPDebugBattleUnit { cellIndex = 5 }, + new PVPDebugBattleUnit { cellIndex = 9 }, + new PVPDebugBattleUnit { cellIndex = 12 } + }; + + var usedCells = PVPDebugBattleCellPickerUtility.GetUsedCells(units, 1); + + CollectionAssert.Contains(usedCells, 5); + CollectionAssert.Contains(usedCells, 12); + CollectionAssert.DoesNotContain(usedCells, 9); + } + + [Test] + public void GetFlatTopHexCenter_OffsetsOddColumns() + { + var first = PVPDebugBattleCellPickerUtility.GetFlatTopHexCenter(0, 0, 40f, 34f); + var second = PVPDebugBattleCellPickerUtility.GetFlatTopHexCenter(0, 1, 40f, 34f); + + Assert.AreEqual(20f, first.x); + Assert.AreEqual(17f, first.y); + Assert.AreEqual(50f, second.x); + Assert.AreEqual(34f, second.y); + } + + [Test] + public void ResolveMoveGuid_DoesNotFallbackToConfigUnitId() + { + const uint configUnitId = 100001; + const uint runtimeGuid = 1234567; + + Assert.AreEqual(runtimeGuid, PVPDebugBattleContext.ResolveMoveGuid(configUnitId, runtimeGuid)); + Assert.AreEqual(0, PVPDebugBattleContext.ResolveMoveGuid(configUnitId, 0)); + } + + private static PVPDebugBattlePreset CreatePreset() + { + var preset = UnityEngine.ScriptableObject.CreateInstance(); + preset.selfUnits.Add(new PVPDebugBattleUnit + { + unitType = Unit.UnitType.Charactor, + unitID = 1001, + cellIndex = 1 + }); + preset.enemyUnits.Add(new PVPDebugBattleUnit + { + unitType = Unit.UnitType.Charactor, + unitID = 2001, + cellIndex = 2 + }); + return preset; + } +} diff --git a/ProjectNLD/Assets/_Test/Editor/PVPDebugBattlePresetTests.cs.meta b/ProjectNLD/Assets/_Test/Editor/PVPDebugBattlePresetTests.cs.meta new file mode 100644 index 00000000000..a090833ab4b --- /dev/null +++ b/ProjectNLD/Assets/_Test/Editor/PVPDebugBattlePresetTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ff534fc7aa071774dab3a66a7c74e9c6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: