refactor(editor): reorganize Editor directory and unify menu hierarchy

File directory changes (mirror Scripts/ module structure):
- AbilityTypeDrawer.cs         → Equipment/
- CharacterWizardWindow.cs     → Character/
- FormEditorWindow.cs          → Player/
- GMToolWindow.cs              → Tools/
- SOManagerWindow.cs           → Tools/
- Map/MapRoomDataEditor.cs     → World/Map/
- Navigation/ (root)           → Enemies/Navigation/
- Achievements/                → Progression/

Menu hierarchy changes (BaseGames/ top-level):
- Data/: +Character Wizard (from Tools/), +Boss Skill Sequence (from Tools/)
- Addressables/: +Addressable Batch Tool, +Asset Reference Graph, +Validate Address Keys (from Tools/Verification/)
- Scene/Setup/: +Boot Flow Wizard, +Scaffold *, +Auto-Open Persistent (from Tools/)
- Scene/: +Camera Area Setup (from Camera/), +Bake All NavSurfaces (from Tools/)
- Events/: +Event Bus Monitor, +Event Chain Viewer, +Create/Reimport Event Channels (from Tools/)
- Tools/Validation/: +Validate All SOs, +Apply/Validate Script Order (from Tools/ flat)
- Tools/Maintenance/: +Missing Scripts/*, +Physics2D Layer Matrix/* (from Tools/ flat)

Result: BaseGames/Tools/ reduced from 16 flat items to 4 items + 2 submenus

Docs: update AssetFolderSpec §12 editor tool table with new menu paths

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-05-20 11:52:17 +08:00
parent 442d4c9cfc
commit 82ce9ff09a
38 changed files with 115 additions and 61 deletions

View File

@@ -0,0 +1,243 @@
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using BaseGames.Player;
namespace BaseGames.Editor
{
/// <summary>
/// AbilityType [Flags] uint 的 PropertyDrawer。
/// 将枚举按能力类别分组,以可读的复选框网格呈现,替代默认的 MaskField。
///
/// 分组:
/// 移动能力 — WallCling / WallJump / Dash / AirDash / DoubleJump / SuperJump / Swim / Dive
/// 法术能力 — Spell1 / Spell2 / Spell3
/// 形态能力 — SpiritForm / SpiritDash
/// 战斗能力 — Parry / ChargeAttack / DownSlash
/// 互动能力 — Interact / FastTravel
/// 能力强化 — InvincibleDash
/// </summary>
[CustomPropertyDrawer(typeof(AbilityType))]
public sealed class AbilityTypeDrawer : PropertyDrawer
{
// ── 分组定义 ──────────────────────────────────────────────────────────
private static readonly (string groupLabel, (AbilityType flag, string label)[] members)[] Groups =
{
("移动能力", new[]
{
(AbilityType.WallCling, "贴墙悬挂"),
(AbilityType.WallJump, "墙跳"),
(AbilityType.Dash, "冲刺"),
(AbilityType.DoubleJump, "二段跳"),
(AbilityType.SuperJump, "超级跳"),
(AbilityType.Swim, "游泳"),
(AbilityType.Dive, "下劈"),
}),
("法术能力", new[]
{
(AbilityType.Spell1, "法术槽 1"),
(AbilityType.Spell2, "法术槽 2"),
(AbilityType.Spell3, "法术槽 3"),
}),
("灵魄形态", new[]
{
(AbilityType.SpiritForm, "灵魄形态"),
(AbilityType.SpiritDash, "灵魄冲刺"),
}),
("战斗能力", new[]
{
(AbilityType.Parry, "弹反"),
(AbilityType.ChargeAttack, "蓄力攻击"),
(AbilityType.DownSlash, "下斩"),
}),
("互动能力", new[]
{
(AbilityType.Interact, "互动"),
(AbilityType.FastTravel, "快速旅行"),
}),
("能力强化", new[]
{
(AbilityType.InvincibleDash, "无敌冲刺"),
}),
};
// ── 布局常量 ──────────────────────────────────────────────────────────
private static readonly float RowH = EditorGUIUtility.singleLineHeight;
private static readonly float GroupHeaderH = EditorGUIUtility.singleLineHeight + 4f;
private static readonly float BtnRowH = EditorGUIUtility.singleLineHeight + 4f;
private const float Spacing = 2f;
private const float MinToggleW = 100f; // 每列最小宽度,用于动态计算列数
private const int MaxColCount = 3; // 列数上限
// 缓存每个属性路径上次渲染时的列数,供 GetPropertyHeight 使用
private static readonly Dictionary<string, int> _colsCache = new();
private static int ComputeCols(float availableWidth)
=> Mathf.Clamp(Mathf.FloorToInt(availableWidth / MinToggleW), 1, MaxColCount);
// ── 高度计算 ──────────────────────────────────────────────────────────
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
float h = RowH + Spacing; // 属性标签行(含 None / All 按钮)
// 使用上次 OnGUI 缓存的列数;首次绘制前按视图宽度估算
float viewW = EditorGUIUtility.currentViewWidth - EditorGUI.indentLevel * 15f - 18f;
int cols = _colsCache.TryGetValue(property.propertyPath, out var cached)
? cached
: ComputeCols(viewW);
foreach (var (_, members) in Groups)
{
h += GroupHeaderH + Spacing;
int rows = Mathf.CeilToInt((float)members.Length / cols);
h += rows * (RowH + Spacing);
}
return h + 4f;
}
// ── 绘制 ──────────────────────────────────────────────────────────────
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
float y = position.y;
float x = position.x;
float w = position.width;
int cols = ComputeCols(w);
_colsCache[property.propertyPath] = cols;
uint current = (uint)property.longValue;
bool changed = false;
// ── 统计已启用数量 ────────────────────────────────────────────────
int enabledCount = 0, totalCount = 0;
foreach (var (_, mems) in Groups)
foreach (var (flag, _) in mems)
{
totalCount++;
if ((current & (uint)flag) != 0) enabledCount++;
}
// ── 标签行 + None / All 快捷按钮 + 数量提示 ──────────────────────
Rect labelRect = new Rect(x, y, EditorGUIUtility.labelWidth, RowH);
Rect btnNoneRect = new Rect(x + EditorGUIUtility.labelWidth, y, 50f, RowH);
Rect btnAllRect = new Rect(btnNoneRect.xMax + 4f, y, 50f, RowH);
Rect countRect = new Rect(btnAllRect.xMax + 8f, y, w - (btnAllRect.xMax + 8f - x), RowH);
EditorGUI.LabelField(labelRect, label);
if (GUI.Button(btnNoneRect, "None"))
{
current = 0;
changed = true;
}
if (GUI.Button(btnAllRect, "All"))
{
uint all = 0;
foreach (var (_, mems) in Groups)
foreach (var (flag, _) in mems)
all |= (uint)flag;
current = all;
changed = true;
}
var countStyle = new GUIStyle(EditorStyles.miniLabel)
{
normal = { textColor = enabledCount > 0
? new Color(0.55f, 0.85f, 0.55f)
: new Color(0.6f, 0.6f, 0.6f) }
};
EditorGUI.LabelField(countRect, $"({enabledCount} / {totalCount} 项已解锁)", countStyle);
y += RowH + Spacing;
// ── 各分组 ────────────────────────────────────────────────────────
foreach (var (groupLabel, members) in Groups)
{
// 分组标题(含组级全选/清空按钮)
Rect groupRect = new Rect(x, y, w, GroupHeaderH);
uint newCurrent = DrawGroupHeader(groupRect, groupLabel, members, current);
if (newCurrent != current) { current = newCurrent; changed = true; }
y += GroupHeaderH + Spacing;
// 复选框网格(列宽均分可用宽度,列数随窗口大小自动调整)
float toggleW = w / cols;
for (int i = 0; i < members.Length; i++)
{
int col = i % cols;
if (col == 0 && i > 0)
y += RowH + Spacing; // 换行
Rect togRect = new Rect(x + col * toggleW, y, toggleW, RowH);
var (flag, toggleLabel) = members[i];
bool isOn = (current & (uint)flag) != 0;
bool newOn = GUI.Toggle(togRect, isOn, toggleLabel, EditorStyles.toggle);
if (newOn != isOn)
{
if (newOn) current |= (uint)flag;
else current &= ~(uint)flag;
changed = true;
}
// 若是最后一个且在本行
if (i == members.Length - 1)
y += RowH + Spacing;
}
}
if (changed)
{
property.longValue = (long)(uint)current;
property.serializedObject.ApplyModifiedProperties();
}
EditorGUI.EndProperty();
}
// ── 辅助:分组标题绘制(含组级全选/清空按钮与已选数量)────────────────
private static uint DrawGroupHeader(Rect rect, string text,
(AbilityType flag, string label)[] members, uint current)
{
// 计算本组已选数量
int groupEnabled = 0;
foreach (var (flag, _) in members)
if ((current & (uint)flag) != 0) groupEnabled++;
Color old = GUI.backgroundColor;
GUI.backgroundColor = new Color(0.25f, 0.25f, 0.28f, 1f);
GUI.Box(new Rect(rect.x, rect.y, rect.width, rect.height - 2f), GUIContent.none, EditorStyles.helpBox);
GUI.backgroundColor = old;
// 分组标签(含已选/总数)
Rect labelRect = new Rect(rect.x + 4f, rect.y + 1f, rect.width - 86f, RowH);
EditorGUI.LabelField(labelRect,
$"{text} {groupEnabled}/{members.Length}",
EditorStyles.boldLabel);
// 组级按钮:全选 / 清空
const float BtnW = 36f;
Rect btnAll = new Rect(rect.xMax - BtnW * 2 - 6f, rect.y + 1f, BtnW, RowH);
Rect btnNone = new Rect(rect.xMax - BtnW - 4f, rect.y + 1f, BtnW, RowH);
if (GUI.Button(btnAll, "全选", EditorStyles.miniButton))
foreach (var (flag, _) in members) current |= (uint)flag;
if (GUI.Button(btnNone, "清空", EditorStyles.miniButton))
foreach (var (flag, _) in members) current &= ~(uint)flag;
// 底部分割线
EditorGUI.DrawRect(new Rect(rect.x, rect.yMax - 2f, rect.width, 1f),
new Color(0.45f, 0.45f, 0.50f, 0.8f));
return current;
}
}
}

View File

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