using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; using BaseGames.Enemies.Abilities; namespace BaseGames.Editor.Modules { /// /// DataHub 敌人能力模块 —— 全部 的对比表格。 /// /// 本模块刻意不做成「列表 + 详情」的标准形态:能力资产按敌人分散在 /// Data/Enemies/{敌人}/Abilities/ 下,逐个点开看不出问题;真正需要的是横向对比 /// —— 同一敌人的招之间射程/权重/冷却是否成比例、有没有 Attack 类却射程为 0 的死招。 /// 所以表格本身就是导航:点行即选中资产,Inspector 出现在表格下方。 /// /// 新建不在此模块内做:能力有类型化子类(EnemyAbilitySO 的各子类),且必须落到 /// 对应敌人的目录,这两件事由角色向导保证(CLAUDE.md 第 2 条:不裸建)。 /// public class EnemyAbilityModule : IDataModule, IDataModuleOrdered { public string ModuleId => "enemyability"; public string DisplayName => "敌人能力"; public string IconName => null; public int DisplayOrder => 35; // 紧随「敌人」(30) private readonly List _rows = new List(); private AbilityOverviewColumn _sortColumn = AbilityOverviewColumn.Owner; private bool _sortAscending = true; private bool _onlyIssues; private bool _onlyAttack; private EnemyAbilitySO _selected; private Label _summaryLabel; private VisualElement _tableHost; // 表格容器:过滤/排序变化时只重建它 private Action _onSelected; // ── IDataModule ────────────────────────────────────────────────── public void Initialize() => Reload(); public void OnActivated() { Reload(); RebuildTable(); UpdateSummary(); } public void BuildListPane(VisualElement container, Action onSelected) { _onSelected = onSelected; _summaryLabel = new Label(); _summaryLabel.style.fontSize = 10; _summaryLabel.style.opacity = 0.7f; _summaryLabel.style.paddingLeft = 10; _summaryLabel.style.paddingTop = 8; _summaryLabel.style.whiteSpace = WhiteSpace.Normal; container.Add(_summaryLabel); UpdateSummary(); var hint = new Label("能力资产按敌人分目录存放,本页把它们并到一张表里横向对比。\n点表格中的行即可选中并编辑该资产。"); hint.style.fontSize = 10; hint.style.opacity = 0.45f; hint.style.whiteSpace = WhiteSpace.Normal; hint.style.paddingLeft = 10; hint.style.paddingRight = 8; hint.style.paddingTop = 6; container.Add(hint); var refreshBtn = new Button(() => { Reload(); RebuildTable(); UpdateSummary(); }) { text = "刷新" }; refreshBtn.style.marginTop = 10; refreshBtn.style.marginLeft = 8; refreshBtn.style.marginRight = 8; container.Add(refreshBtn); // 新建走向导:能力有类型化子类且必须落到对应敌人目录,裸建两者都保证不了 var wizardBtn = new Button(OpenCharacterWizard) { text = "用角色向导新建…" }; wizardBtn.style.marginTop = 4; wizardBtn.style.marginLeft = 8; wizardBtn.style.marginRight = 8; container.Add(wizardBtn); } public void BuildDetailPane(VisualElement container, UnityEngine.Object selected) { _selected = selected as EnemyAbilitySO; // 过滤 var filterRow = new VisualElement(); filterRow.style.flexDirection = FlexDirection.Row; filterRow.style.flexWrap = Wrap.Wrap; filterRow.style.paddingLeft = 6; filterRow.style.paddingTop = 6; filterRow.style.paddingBottom = 4; filterRow.Add(DataHubEditorKit.MakeFilterChip("仅有问题", v => { _onlyIssues = v; RebuildTable(); UpdateSummary(); })); filterRow.Add(DataHubEditorKit.MakeFilterChip("仅 Attack 类", v => { _onlyAttack = v; RebuildTable(); UpdateSummary(); })); container.Add(filterRow); _tableHost = new VisualElement(); container.Add(_tableHost); RebuildTable(); if (_selected == null) return; container.Add(SkillModule.MakeDivider()); container.Add(new InspectorElement(_selected)); } // ── 表格 ───────────────────────────────────────────────────────── private void Reload() { _rows.Clear(); foreach (var so in AssetOperations.FindAll()) _rows.Add(EnemyAbilityOverview.BuildRow(so, AssetDatabase.GetAssetPath(so))); } private IEnumerable VisibleRows() { var rows = _rows.AsEnumerable(); if (_onlyIssues) rows = rows.Where(r => r.HasIssue); if (_onlyAttack) rows = rows.Where(r => r.Category == AbilityCategory.Attack); return EnemyAbilityOverview.Sort(rows.ToList(), _sortColumn, _sortAscending); } private void RebuildTable() { if (_tableHost == null) return; _tableHost.Clear(); _tableHost.Add(BuildHeaderRow()); foreach (var row in VisibleRows()) _tableHost.Add(BuildDataRow(row)); } // 列宽(px);归属与 id 用弹性宽度,数值列固定以便纵向对齐比较 private const int WIssue = 18, WOwner = 90, WId = 150, WCat = 70, WNum = 58, WExcl = 90; private VisualElement BuildHeaderRow() { var header = new VisualElement(); header.style.flexDirection = FlexDirection.Row; header.style.alignItems = Align.Center; header.style.paddingLeft = 8; header.style.paddingRight = 8; header.style.paddingBottom = 3; header.style.borderBottomWidth = 1; header.style.borderBottomColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.35f)); header.Add(FixedLabel("", WIssue, bold: true)); header.Add(SortButton("归属", AbilityOverviewColumn.Owner, WOwner)); header.Add(SortButton("能力 Id", AbilityOverviewColumn.Id, WId)); header.Add(SortButton("类别", AbilityOverviewColumn.Category, WCat)); header.Add(SortButton("射程", AbilityOverviewColumn.Range, WNum)); header.Add(SortButton("权重", AbilityOverviewColumn.Weight, WNum)); header.Add(SortButton("冷却", AbilityOverviewColumn.Cooldown, WNum)); header.Add(SortButton("优先级", AbilityOverviewColumn.Priority, WNum)); header.Add(SortButton("互斥组", AbilityOverviewColumn.Exclusion, WExcl)); return header; } private Button SortButton(string text, AbilityOverviewColumn column, int width) { string arrow = _sortColumn == column ? (_sortAscending ? " ▲" : " ▼") : ""; var btn = new Button(() => { if (_sortColumn == column) _sortAscending = !_sortAscending; else { _sortColumn = column; _sortAscending = true; } RebuildTable(); }) { text = text + arrow }; btn.style.width = width; btn.style.flexShrink = 0; btn.style.fontSize = 10; btn.style.marginLeft = 0; btn.style.marginRight = 0; btn.style.paddingLeft = 2; btn.style.paddingRight = 2; btn.style.backgroundColor = StyleKeyword.None; btn.style.borderTopWidth = btn.style.borderBottomWidth = 0; btn.style.borderLeftWidth = btn.style.borderRightWidth = 0; btn.style.unityTextAlign = TextAnchor.MiddleLeft; return btn; } private VisualElement BuildDataRow(AbilityOverviewRow r) { bool isSelected = _selected != null && r.Asset == _selected; var row = new VisualElement(); row.style.flexDirection = FlexDirection.Row; row.style.alignItems = Align.Center; row.style.paddingTop = 2; row.style.paddingBottom = 2; row.style.paddingLeft = 8; row.style.paddingRight = 8; row.style.backgroundColor = isSelected ? new StyleColor(new Color(0.25f, 0.5f, 1f, 0.2f)) : StyleKeyword.None; var issue = FixedLabel(r.HasIssue ? "⚠" : "", WIssue); if (r.HasIssue) { issue.style.color = new StyleColor(new Color(1f, 0.45f, 0.2f)); issue.tooltip = r.Issue; // 悬停看原因,不占列宽 } row.Add(issue); row.Add(FixedLabel(r.Owner, WOwner)); row.Add(FixedLabel(string.IsNullOrEmpty(r.Id) ? "(未命名)" : r.Id, WId)); row.Add(FixedLabel(r.Category.ToString(), WCat)); row.Add(FixedLabel(Num(r.Range), WNum)); row.Add(FixedLabel(Num(r.Weight), WNum)); row.Add(FixedLabel(Num(r.Cooldown), WNum)); row.Add(FixedLabel(r.Priority.ToString(), WNum)); row.Add(FixedLabel(string.IsNullOrEmpty(r.Exclusion) ? "-" : r.Exclusion, WExcl)); row.RegisterCallback(_ => { _selected = r.Asset; _onSelected?.Invoke(r.Asset); }); return row; } private static string Num(float v) => v.ToString("0.##"); private static Label FixedLabel(string text, int width, bool bold = false) { var lbl = new Label(text); lbl.style.width = width; lbl.style.flexShrink = 0; lbl.style.fontSize = 11; if (bold) lbl.style.unityFontStyleAndWeight = FontStyle.Bold; return lbl; } private void UpdateSummary() { if (_summaryLabel == null) return; int total = _rows.Count; int issues = _rows.Count(r => r.HasIssue); int shown = VisibleRows().Count(); _summaryLabel.text = issues > 0 ? $"共 {total} 个能力 · 有问题 {issues} 个 · 当前显示 {shown}" : $"共 {total} 个能力 · 无问题 · 当前显示 {shown}"; } private static void OpenCharacterWizard() { // 与脚手架保持单一入口:能力的类型化子类与目标目录都由向导保证 var type = typeof(EnemyAbilityModule).Assembly.GetType("BaseGames.Editor.CharacterWizardWindow"); if (type != null) { EditorWindow.GetWindow(type).Show(); return; } EditorUtility.DisplayDialog("未找到角色向导", "没有找到 CharacterWizardWindow。请从菜单打开角色向导创建能力资产。", "确定"); } } }