删除 BossSkillModule / BossSkillSequenceWindow 后能力资产没有任何编辑器总览
(spec §6.4 记为待补)。本次补上,但不照搬「列表 + 详情」的标准形态:
能力资产按敌人分散在 Data/Enemies/{敌人}/Abilities/ 下,逐个点开看不出问题;
真正需要的是横向对比——同一敌人的招之间射程/权重/冷却是否成比例、有没有
Attack 类却射程为 0 的死招。所以做成一张跨敌人的对比表,表格本身就是导航:
点行即选中资产,Inspector 出现在表格下方。列均可点击排序。
两点刻意的设计:
- 问题列直接调资产自己的 Validate(),不在表格里另写一份判定规则。否则 SO 侧
改了规则,表格会继续按旧规则报。
- 不提供新建。能力有类型化子类且必须落到对应敌人目录,这两件事由角色向导保证
(CLAUDE.md 第 2 条不裸建);模块只给一个跳转向导的按钮。
取数与排序抽成纯函数(EnemyAbilityOverview)与 UI 分离:UIElements 构建不可单测,
而表格真正会出错的是「归属怎么从路径推导」「问题从哪来」「排序对不对」,
这三处都是纯逻辑,抽出来即可覆盖。
spec 里还提到「阶段一览」,本次不做且不打算做:阶段现在归 BossPhaseAbilityGate
(预制体上的组件),不在 SO 上,配方/资产层看不到它——与 ApproachAttackEngagement
无法在配方层校验选招器是同一类边界。
验证:编译 0 错;EditMode 279/279(273 + 新增 6)。
UI 路径另做程序化冒烟(单测覆盖不到):模块被 DataHubWindow 自动发现并排在
「敌人」之后,两个面板构建无异常,表格 18 行 = 项目 18 个能力资产;
过滤实测 18 / 仅 Attack 5 / 仅有问题 0(与 SO 校验"全部合法"一致),
归属降序首行为 E006 符合序数排序。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
147 lines
7.1 KiB
C#
147 lines
7.1 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using NUnit.Framework;
|
|
using UnityEngine;
|
|
using BaseGames.Editor.Modules;
|
|
using BaseGames.Enemies.Abilities;
|
|
|
|
namespace BaseGames.Tests.EditMode.EditorTools
|
|
{
|
|
/// <summary>
|
|
/// 能力总览表格的取数与排序。UI 构建不可测,所以把「一行长什么样」「怎么排序」
|
|
/// 「问题从哪来」抽成纯函数放这里——表格真正会出错的也正是这三处。
|
|
/// </summary>
|
|
public class EnemyAbilityOverviewTests
|
|
{
|
|
private readonly List<EnemyAbilitySO> _created = new List<EnemyAbilitySO>();
|
|
|
|
[TearDown]
|
|
public void TearDown()
|
|
{
|
|
// 项目关闭了 Domain/Scene Reload:断言失败会跳过用例后续语句,
|
|
// 清理必须放在 TearDown,否则失败一次就往内存里漏一个 SO。
|
|
foreach (var so in _created) if (so != null) Object.DestroyImmediate(so);
|
|
_created.Clear();
|
|
}
|
|
|
|
private EnemyAbilitySO MakeAbility(
|
|
string id, AbilityCategory category = AbilityCategory.None,
|
|
float range = 1f, float weight = 1f, float cooldown = 1f,
|
|
int priority = 0, string exclusion = "")
|
|
{
|
|
var so = ScriptableObject.CreateInstance<EnemyAbilitySO>();
|
|
so.abilityId = id;
|
|
so.category = category;
|
|
so.rangeRadius = range;
|
|
so.weight = weight;
|
|
so.cooldown = cooldown;
|
|
so.priority = priority;
|
|
so.exclusionGroup = exclusion;
|
|
_created.Add(so);
|
|
return so;
|
|
}
|
|
|
|
// ── 归属提取 ─────────────────────────────────────────────────────
|
|
|
|
[Test]
|
|
public void OwnerFromPath_TakesEnemyFolderName()
|
|
{
|
|
// 能力资产按 AssetFolderSpec 落在 Data/Enemies/{敌人}/Abilities/ 下,
|
|
// 归属列靠路径推导——表格要横向对比,必须能一眼看出这招是谁的。
|
|
Assert.AreEqual("ChaoFeng", EnemyAbilityOverview.OwnerFromPath(
|
|
"Assets/_Game/Data/Enemies/ChaoFeng/Abilities/ABL_ChaoFeng_Intro.asset"));
|
|
Assert.AreEqual("E004", EnemyAbilityOverview.OwnerFromPath(
|
|
"Assets/_Game/Data/Enemies/E004/Abilities/ABL_E004_Bite.asset"));
|
|
}
|
|
|
|
[Test]
|
|
public void OwnerFromPath_OffSpecLayout_ReturnsPlaceholder()
|
|
{
|
|
// 放错目录的资产不该让整张表崩掉,但也不能假装它有归属——显示占位符,
|
|
// 让"这个资产没按规范放"本身在表里可见。
|
|
Assert.AreEqual(EnemyAbilityOverview.UnknownOwner,
|
|
EnemyAbilityOverview.OwnerFromPath("Assets/Random/ABL_Stray.asset"));
|
|
Assert.AreEqual(EnemyAbilityOverview.UnknownOwner,
|
|
EnemyAbilityOverview.OwnerFromPath(""));
|
|
}
|
|
|
|
// ── 行取数 ───────────────────────────────────────────────────────
|
|
|
|
[Test]
|
|
public void BuildRow_CarriesTunableFields()
|
|
{
|
|
var so = MakeAbility("blink_strike", AbilityCategory.Attack,
|
|
range: 3.5f, weight: 2f, cooldown: 4.25f,
|
|
priority: 7, exclusion: "melee");
|
|
|
|
var row = EnemyAbilityOverview.BuildRow(
|
|
so, "Assets/_Game/Data/Enemies/E004/Abilities/ABL_E004_Blink.asset");
|
|
|
|
Assert.AreEqual("E004", row.Owner);
|
|
Assert.AreEqual("blink_strike", row.Id);
|
|
Assert.AreEqual(AbilityCategory.Attack, row.Category);
|
|
Assert.AreEqual(3.5f, row.Range);
|
|
Assert.AreEqual(2f, row.Weight);
|
|
Assert.AreEqual(4.25f, row.Cooldown);
|
|
Assert.AreEqual(7, row.Priority);
|
|
Assert.AreEqual("melee", row.Exclusion);
|
|
Assert.AreSame(so, row.Asset, "行必须持有资产引用,点选才能跳到它");
|
|
}
|
|
|
|
[Test]
|
|
public void BuildRow_SurfacesIssue_FromAssetsOwnValidate()
|
|
{
|
|
// 问题判定不在表格里另写一份规则,而是调资产自己的 Validate()。
|
|
// 这条断言守住"规则只有一处"——否则 SO 侧改了规则,表格会继续按旧规则报。
|
|
var broken = MakeAbility("bad_attack", AbilityCategory.Attack, range: 0f);
|
|
var ok = MakeAbility("good_attack", AbilityCategory.Attack, range: 2f);
|
|
|
|
var brokenRow = EnemyAbilityOverview.BuildRow(broken, "Assets/x/ABL_A.asset");
|
|
var okRow = EnemyAbilityOverview.BuildRow(ok, "Assets/x/ABL_B.asset");
|
|
|
|
Assert.IsTrue(brokenRow.HasIssue,
|
|
"Attack 类但射程为 0 的招永远选不中,表格必须标出来");
|
|
Assert.IsFalse(string.IsNullOrEmpty(brokenRow.Issue), "要带上原因文本,不能只给个红点");
|
|
Assert.IsFalse(okRow.HasIssue);
|
|
}
|
|
|
|
// ── 排序 ─────────────────────────────────────────────────────────
|
|
|
|
[Test]
|
|
public void Sort_ByCooldown_BothDirections()
|
|
{
|
|
var rows = new List<AbilityOverviewRow>
|
|
{
|
|
EnemyAbilityOverview.BuildRow(MakeAbility("b", cooldown: 5f), "Assets/x/B.asset"),
|
|
EnemyAbilityOverview.BuildRow(MakeAbility("a", cooldown: 1f), "Assets/x/A.asset"),
|
|
EnemyAbilityOverview.BuildRow(MakeAbility("c", cooldown: 3f), "Assets/x/C.asset"),
|
|
};
|
|
|
|
var asc = EnemyAbilityOverview.Sort(rows, AbilityOverviewColumn.Cooldown, ascending: true);
|
|
CollectionAssert.AreEqual(new[] { "a", "c", "b" }, asc.Select(r => r.Id).ToArray());
|
|
|
|
var desc = EnemyAbilityOverview.Sort(rows, AbilityOverviewColumn.Cooldown, ascending: false);
|
|
CollectionAssert.AreEqual(new[] { "b", "c", "a" }, desc.Select(r => r.Id).ToArray());
|
|
}
|
|
|
|
[Test]
|
|
public void Sort_ByOwnerThenId_IsStableAcrossEnemies()
|
|
{
|
|
// 默认视图按归属分组才有对比意义:同一敌人的招要挨在一起,组内按 id 稳定排列。
|
|
var rows = new List<AbilityOverviewRow>
|
|
{
|
|
EnemyAbilityOverview.BuildRow(MakeAbility("bite"), "Assets/_Game/Data/Enemies/E005/Abilities/ABL_1.asset"),
|
|
EnemyAbilityOverview.BuildRow(MakeAbility("acid"), "Assets/_Game/Data/Enemies/E004/Abilities/ABL_2.asset"),
|
|
EnemyAbilityOverview.BuildRow(MakeAbility("appear"),"Assets/_Game/Data/Enemies/E004/Abilities/ABL_3.asset"),
|
|
};
|
|
|
|
var sorted = EnemyAbilityOverview.Sort(rows, AbilityOverviewColumn.Owner, ascending: true);
|
|
|
|
CollectionAssert.AreEqual(
|
|
new[] { "acid", "appear", "bite" },
|
|
sorted.Select(r => r.Id).ToArray(),
|
|
"同归属内按 id 排,跨归属按归属名排");
|
|
}
|
|
}
|
|
}
|