refactor(editor): 角色向导 Boss 页改产出 EnemyAbilitySO

该向导是 5 个空 BossSkillSO 资产的创建链路——不改则会持续产出旧轨资产。
招式表扩为带 soType / rangeRadius / cooldown / weight 的形式(与
GetEnemyAbilityDefs 对齐),阶段可用性移交 BossPhaseAbilityGate。
产出时强制 rangeRadius > 0,不把责任推给下游作者。
状态面板改查 EnemyAbilitySO,并新增 AI 图注册状态一行。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-30 15:08:10 +08:00
co-authored by Claude Opus 5
parent e488255315
commit a64449d213
@@ -5,7 +5,6 @@ using UnityEditor;
using UnityEngine; using UnityEngine;
using UnityEngine.UIElements; using UnityEngine.UIElements;
using BaseGames.Boss;
using BaseGames.Combat; using BaseGames.Combat;
using BaseGames.Enemies; using BaseGames.Enemies;
using BaseGames.Enemies.Abilities; using BaseGames.Enemies.Abilities;
@@ -76,8 +75,7 @@ namespace BaseGames.Editor
// 动态内容区(类型切换时重建) // 动态内容区(类型切换时重建)
private VisualElement _enemyContentArea; private VisualElement _enemyContentArea;
// Boss 命名字段 // 命名字段
private string _bossId = "NewBoss"; // kept for legacy SkillSequenceSO queries if any
private string _enemyId = "E001"; // kept for legacy status calls if any private string _enemyId = "E001"; // kept for legacy status calls if any
private string _playerId = "Player"; private string _playerId = "Player";
@@ -330,11 +328,12 @@ namespace BaseGames.Editor
var factory = MakeActionGroup(); var factory = MakeActionGroup();
factory.Add(MakeFactoryButton("ENM_ChaoFeng_Stats.asset", () => { CreateChaoFengStatsSO(); RefreshSOStatus(); })); factory.Add(MakeFactoryButton("ENM_ChaoFeng_Stats.asset", () => { CreateChaoFengStatsSO(); RefreshSOStatus(); }));
factory.Add(MakeFactoryButton("ENM_ChaoFeng_AnimConfig.asset",() => { CreateChaoFengAnimConfigSO(); RefreshSOStatus(); })); factory.Add(MakeFactoryButton("ENM_ChaoFeng_AnimConfig.asset",() => { CreateChaoFengAnimConfigSO(); RefreshSOStatus(); }));
foreach (var (skName, skId, skPhases, skWeight) in ChaoFengSkillDefs) foreach (var (abName, abId, soType, r, cd, w, ph) in ChaoFengAbilityDefs)
{ {
string cName = skName; string cId = skId; int[] cPhases = skPhases; float cWeight = skWeight; string cName = abName; string cId = abId; System.Type cType = soType;
float cR = r, cCd = cd, cW = w; int cPh = ph;
factory.Add(MakeFactoryButton($"ABL_ChaoFeng_{cName}.asset", factory.Add(MakeFactoryButton($"ABL_ChaoFeng_{cName}.asset",
() => { CreateChaoFengSkillSO(cName, cId, cPhases, cWeight); RefreshSOStatus(); })); () => { CreateChaoFengAbilitySO(cName, cId, cType, cR, cCd, cW, cPh); RefreshSOStatus(); }));
} }
root.Add(factory); root.Add(factory);
@@ -345,7 +344,9 @@ namespace BaseGames.Editor
root.Add(MakeSeparator()); root.Add(MakeSeparator());
root.Add(MakeSectionHeader("▶ 场景搭建")); root.Add(MakeSectionHeader("▶ 场景搭建"));
root.Add(MakeHelpBox("放置嘲风完整组件树(ChaoFengBoss + 浮空控制器 + 击倒计数 + Phase1 HitBox × 4 + 炮口 × 3)。")); root.Add(MakeHelpBox("放置嘲风完整组件树(ChaoFengBoss + 浮空控制器 + 击倒计数 + " +
"EnemyAiBrain + BossPhaseAbilityGate + 6 个能力组件 + " +
"Phase1 HitBox × 4 + 炮口 × 3)。"));
var bossSpriteField = new UnityEditor.UIElements.ObjectField("默认外观 Sprite(碰撞体尺寸依据,可留空)") var bossSpriteField = new UnityEditor.UIElements.ObjectField("默认外观 Sprite(碰撞体尺寸依据,可留空)")
{ objectType = typeof(Sprite), allowSceneObjects = false, value = _defaultSprite }; { objectType = typeof(Sprite), allowSceneObjects = false, value = _defaultSprite };
@@ -362,9 +363,8 @@ namespace BaseGames.Editor
root.Add(MakeSectionHeader("▶ 专项编辑器")); root.Add(MakeSectionHeader("▶ 专项编辑器"));
var jumpGroup = MakeActionGroup(); var jumpGroup = MakeActionGroup();
jumpGroup.Add(MakeJumpButton("Boss 技能序列查看器", BossSkillSequenceWindow.OpenWindow));
jumpGroup.Add(MakeJumpButton("武器 HitBox 向导", WeaponHitBoxWizard.Open)); jumpGroup.Add(MakeJumpButton("武器 HitBox 向导", WeaponHitBoxWizard.Open));
jumpGroup.Add(MakeJumpButton("Data HubBoss技能", DataHubWindow.Open)); jumpGroup.Add(MakeJumpButton("Data Hub敌人 / 能力", DataHubWindow.Open));
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu)); jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
root.Add(jumpGroup); root.Add(jumpGroup);
@@ -689,41 +689,72 @@ namespace BaseGames.Editor
EditorScaffoldUtils.CreateSOAsset<EnemyAnimationConfigSO>(dir, "ENM_ChaoFeng_AnimConfig"); EditorScaffoldUtils.CreateSOAsset<EnemyAnimationConfigSO>(dir, "ENM_ChaoFeng_AnimConfig");
} }
private static void CreateChaoFengSkillSO(string skillName, string skillId, int[] phaseIndices, float weight) private static void CreateChaoFengAbilitySO(
string abilityName, string abilityId, System.Type soType,
float rangeRadius, float cooldown, float weight, int phase)
{ {
string dir = "Assets/_Game/Data/Enemies/ChaoFeng/Abilities"; const string dir = "Assets/_Game/Data/Enemies/ChaoFeng/Abilities";
string name = $"ABL_ChaoFeng_{skillName}"; string name = $"ABL_ChaoFeng_{abilityName}";
var so = EditorScaffoldUtils.CreateSOAsset<BossSkillSO>(dir, name);
if (so != null) var so = EditorScaffoldUtils.CreateSOAsset(soType, dir, name) as EnemyAbilitySO;
if (so == null) return; // 已存在则不覆盖
so.abilityId = abilityId;
so.designNote = phase < 0
? "入场演出,不进选招池"
: $"阶段 {phase} 招式;阶段可用性由 BossPhaseAbilityGate 配置";
so.cooldown = cooldown;
if (phase < 0)
{ {
so.skillId = skillId; so.category = AbilityCategory.None;
so.displayName = skillName; so.rangeRadius = 0f;
so.availablePhaseIndices = phaseIndices; so.weight = 0f;
so.weight = weight;
EditorUtility.SetDirty(so);
AssetDatabase.SaveAssets();
} }
else
{
so.category = AbilityCategory.Attack;
so.rangeRadius = rangeRadius; // 必须 > 0,否则选招器永远选不中
so.weight = weight;
}
EditorUtility.SetDirty(so);
AssetDatabase.SaveAssets();
} }
/// <summary>嘲风技能集(计划):Phase0 四技能加权随机 + Phase1 风石。</summary> /// <summary>
private static readonly (string name, string id, int[] phases, float weight)[] ChaoFengSkillDefs = /// 嘲风能力集。阶段可用性不在 SO 上——由 BossPhaseAbilityGate 承载
/// (见放置工具 PlaceChaoFeng),这里只记录供说明文本使用。
/// rangeRadius 必须 &gt; 0category==Attack 且射程为 0 的招永远够不着玩家,
/// EnemyAbilitySO.Validate 与 EnemyBase.BuildAttackSelector 都会显式报错。
/// </summary>
private static readonly (string name, string id, System.Type soType,
float rangeRadius, float cooldown, float weight, int phase)[]
ChaoFengAbilityDefs =
{ {
("Boomerang", "boomerang", new[] { 0 }, 1.0f), ("Intro", "chaofeng_intro", typeof(PlayClipAbilitySO), 0f, 0f, 0f, -1),
("FanCombo", "fan_combo", new[] { 0 }, 1.5f), ("Boomerang", "boomerang", typeof(PlayClipAbilitySO), 9f, 4.0f, 1.0f, 0),
("TornadoSmall", "tornado_small", new[] { 0 }, 1.2f), ("FanCombo", "fan_combo", typeof(EnemyAbilitySO), 2.5f, 2.5f, 1.5f, 0),
("TornadoLarge", "tornado_large", new[] { 0 }, 0.8f), ("TornadoSmall", "tornado_small", typeof(PlayClipAbilitySO), 8f, 3.5f, 1.2f, 0),
("WindStone", "wind_stone", new[] { 1 }, 1.0f), ("TornadoLarge", "tornado_large", typeof(PlayClipAbilitySO), 12f, 6.0f, 0.8f, 0),
("WindStone", "wind_stone", typeof(PlayClipAbilitySO), 12f, 3.0f, 1.0f, 1),
}; };
private static void CreateAllChaoFengSOs() private static void CreateAllChaoFengSOs()
{ {
CreateChaoFengStatsSO(); CreateChaoFengStatsSO();
CreateChaoFengAnimConfigSO(); CreateChaoFengAnimConfigSO();
foreach (var (n, id, phases, weight) in ChaoFengSkillDefs) foreach (var (n, id, t, r, cd, w, ph) in ChaoFengAbilityDefs)
CreateChaoFengSkillSO(n, id, phases, weight); CreateChaoFengAbilitySO(n, id, t, r, cd, w, ph);
AssetDatabase.SaveAssets(); AssetDatabase.SaveAssets();
EditorUtility.DisplayDialog("创建完成", EditorUtility.DisplayDialog("创建完成",
"全部嘲风 SO 已创建(已存在的跳过)。\n放置到场景后检查 BossSkillExecutor._skills 绑定。", "确定"); "全部嘲风 SO 已创建(已存在的跳过)。\n" +
"接下来:\n" +
"1. 用「放置嘲风到场景并绑定 SO」生成组件树;\n" +
"2. 检查 EnemyAiBrain._definitionId = ChaoFeng\n" +
"3. 检查各能力组件的 _config 已绑定对应 ABL_ 资产;\n" +
"4. 在 BossPhaseAbilityGate 上确认阶段表(地面四招=阶段0,风石=阶段1)。",
"确定");
} }
// ── 场景搭建(已移至 RefreshEnemyTabContent 内的内联按钮) ───────────── // ── 场景搭建(已移至 RefreshEnemyTabContent 内的内联按钮) ─────────────
@@ -794,10 +825,16 @@ namespace BaseGames.Editor
("ENM_ChaoFeng_Stats", FindAtPath<EnemyStatsSO>($"{dir}/ENM_ChaoFeng_Stats.asset")), ("ENM_ChaoFeng_Stats", FindAtPath<EnemyStatsSO>($"{dir}/ENM_ChaoFeng_Stats.asset")),
("ENM_ChaoFeng_AnimConfig",FindAtPath<EnemyAnimationConfigSO>($"{dir}/ENM_ChaoFeng_AnimConfig.asset")), ("ENM_ChaoFeng_AnimConfig",FindAtPath<EnemyAnimationConfigSO>($"{dir}/ENM_ChaoFeng_AnimConfig.asset")),
}; };
foreach (var (skName, _, _, _) in ChaoFengSkillDefs) foreach (var (abName, _, _, _, _, _, _) in ChaoFengAbilityDefs)
checks.Add(($"ABL_ChaoFeng_{skName}", FindAtPath<BossSkillSO>($"{ablDir}/ABL_ChaoFeng_{skName}.asset"))); checks.Add(($"ABL_ChaoFeng_{abName}",
FindAtPath<EnemyAbilitySO>($"{ablDir}/ABL_ChaoFeng_{abName}.asset")));
// AI 图是代码定义的(定制路径),没有资产可查——用注册表确认它被反射收集到了
bool hasAiGraph = BaseGames.AI.AiDefinitionRegistry.Has("ChaoFeng");
_bossStatusPanel.Add(MakeStatusGrid(checks.ToArray())); _bossStatusPanel.Add(MakeStatusGrid(checks.ToArray()));
_bossStatusPanel.Add(new Label(
hasAiGraph ? "✔ AI 图 ChaoFengAi 已注册(定制路径 _definitionId = ChaoFeng"
: "✘ AI 图 ChaoFengAi 未注册——检查 [AiDefinition(\"ChaoFeng\")] 特性"));
} }
// ── 辅助:状态格 ───────────────────────────────────────────────────── // ── 辅助:状态格 ─────────────────────────────────────────────────────