refactor(editor): PlaceChaoFeng 改挂新轨——决策层 + 阶段门 + 能力组件

BossSkillExecutor 换为 EnemyAiBrain(_definitionId=ChaoFeng) +
BossPhaseAbilityGate + 6 个能力组件(一招一子物体,绕开
DisallowMultipleComponent)。HitBox 改绑命名槽位,阶段招池经反射写入。
顺带删掉 Place Boss Enemy 报告里的 BossSkillExecutor 旧轨提示,
以及已无引用的 using BaseGames.Boss。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-30 15:17:52 +08:00
co-authored by Claude Opus 5
parent a64449d213
commit ba2e22cd58
@@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.Reflection;
using Animancer;
using BaseGames.Boss;
using BaseGames.Camera;
using BaseGames.Combat;
using BaseGames.Combat.StatusEffects;
@@ -402,7 +401,6 @@ namespace BaseGames.Editor
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "los" }, report);
report.Add("填写 _bossId。");
report.Add("★ 挂载 BossSkillExecutor 并指定 BossSkillSO 列表(技能执行层)。");
report.Add("★ AI(BrainGraph):用菜单 BaseGames/AI/Enemy AI Recipe Wizard 建一个配方资产," +
"在 Inspector 选两层模块(未发现/交战),再挂 EnemyAiBrain 组件并把 _recipe 指向该资产。" +
"有独门机制的敌人才写 [AiDefinition] 的 AiScript 子类并改用 _definitionId。");
@@ -1086,7 +1084,8 @@ namespace BaseGames.Editor
EnemyFeedback feedback = GetOrAddComponent<EnemyFeedback>(go);
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
GetOrAddComponent<BaseGames.Enemies.Navigation.GroundNavigator>(go); // 地面导航(直接移动,无寻路图)
BossSkillExecutor skillExec = GetOrAddComponent<BossSkillExecutor>(go);
EnemyAiBrain brain = GetOrAddComponent<EnemyAiBrain>(go);
BossPhaseAbilityGate phaseGate = GetOrAddComponent<BossPhaseAbilityGate>(go);
ChaoFengFloatController floatCtrl = GetOrAddComponent<ChaoFengFloatController>(go);
ChaoFengKnockdownCounter knockdown = GetOrAddComponent<ChaoFengKnockdownCounter>(go);
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
@@ -1095,7 +1094,7 @@ namespace BaseGames.Editor
var (hurtBox, bodyContact, contactHitBox) =
SetupHurtAndContactBoxes(go, size, report);
// Phase1 攻击 HitBox(默认禁用;技能执行时由 BossSkillExecutor 开关)。
// Phase1 攻击 HitBox(默认禁用;由能力按 EnemyAttackSO.hitBoxSlot 归一化时机开关)。
// 计划:挥扇三连 FanCombo ×3 + 龙卷接触 Tornado。
HitBox fan1 = CreateDisabledHitBox(go.transform, "Phase1_FanCombo_HitBox_1", "EnemyHitBox",
true, report, size: new Vector2(1.0f, 0.5f));
@@ -1111,6 +1110,23 @@ namespace BaseGames.Editor
Transform tornadoMuzzleT = GetOrCreateChild(go.transform, "TornadoMuzzle");
Transform windStoneMuzzleT = GetOrCreateChild(go.transform, "WindStoneMuzzle");
// 能力集:入场演出 + 五个战斗招,一招一个子物体。
// EnemyAbilityBase 带 [DisallowMultipleComponent],同一物体放不下四个 PlayClipAbility
// EnemyAbilityRegistry.CollectFrom 用 GetComponentsInChildren 递归收集,子物体照常注册。
Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities");
var abIntro = GetOrAddComponent<PlayClipAbility>(
GetOrCreateChild(abilitiesT, "PlayClipAbility_Intro").gameObject);
var abFan = GetOrAddComponent<MeleeAttackAbility>(
GetOrCreateChild(abilitiesT, "MeleeAttackAbility_FanCombo").gameObject);
var abBoomer = GetOrAddComponent<PlayClipAbility>(
GetOrCreateChild(abilitiesT, "PlayClipAbility_Boomerang").gameObject);
var abTornadoS = GetOrAddComponent<PlayClipAbility>(
GetOrCreateChild(abilitiesT, "PlayClipAbility_TornadoSmall").gameObject);
var abTornadoL = GetOrAddComponent<PlayClipAbility>(
GetOrCreateChild(abilitiesT, "PlayClipAbility_TornadoLarge").gameObject);
var abWind = GetOrAddComponent<PlayClipAbility>(
GetOrCreateChild(abilitiesT, "PlayClipAbility_WindStone").gameObject);
// SOs — assign first so OnValidate doesn't warn during wiring
AssignAsset(bossBase, "_statsSO", report, false, "ENM_ChaoFeng_Stats");
AssignAsset(bossBase, "_animConfig", report, false, "ENM_ChaoFeng_AnimConfig");
@@ -1122,7 +1138,6 @@ namespace BaseGames.Editor
AssignReference(bossBase, "_feedback", feedback, report);
AssignReference(bossBase, "_hurtBox", hurtBox, report);
AssignReference(bossBase, "_bodyCollider", body, report); // 身体几何唯一权威(EnemyBase.Body
AssignReference(skillExec, "_animancer", animancer, report);
// 浮空 / 击落 / 弹体发射点接线(计划)
AssignReference(bossBase, "_floatController", floatCtrl, report);
@@ -1130,6 +1145,7 @@ namespace BaseGames.Editor
AssignReference(bossBase, "_boomerangMuzzle", boomerangMuzzleT, report);
AssignReference(bossBase, "_tornadoMuzzle", tornadoMuzzleT, report);
AssignReference(bossBase, "_windStoneMuzzle", windStoneMuzzleT, report);
AssignReference(bossBase, "_phaseGate", phaseGate, report);
AssignReference(floatCtrl, "_rb", rb, report);
AssignReference(knockdown, "_boss", bossBase, report);
AssignReference(knockdown, "_floatCtrl", floatCtrl, report);
@@ -1158,21 +1174,40 @@ namespace BaseGames.Editor
report);
AssignLayerMask(movement, "_wallMask", new[] { "Platform", "Wall" }, report); // 撞墙判定层(策划可调)
// 收集 BossSkillSO 并赋给执行器(计划技能集)
var skillAssets = new System.Collections.Generic.List<Object>();
foreach (var n in new[] { "ABL_ChaoFeng_Boomerang", "ABL_ChaoFeng_FanCombo",
"ABL_ChaoFeng_TornadoSmall", "ABL_ChaoFeng_TornadoLarge",
"ABL_ChaoFeng_WindStone" })
{
Object sk = FindFirstAsset(n);
if (sk != null) skillAssets.Add(sk);
else report.Add($"未找到 BossSkillSO{n},请先一键创建 ChaoFeng SO 后再重新运行此放置操作。");
}
if (skillAssets.Count > 0)
AssignObjectArray(skillExec, "_skills", skillAssets.ToArray(), report);
// 决策层:Boss 走定制路径(AiScript),不用配方
AssignString(brain, "_definitionId", "ChaoFeng", report);
AssignString(skillExec, "_bossId", "ChaoFeng", report);
AssignObjectArray(skillExec, "_hitBoxes", new Object[] { fan1, fan2, fan3, tornadoHB }, report);
// 能力 SO 绑定(一招一组件)
var abilityWiring = new (Object comp, string assetName)[]
{
(abIntro, "ABL_ChaoFeng_Intro"),
(abFan, "ABL_ChaoFeng_FanCombo"),
(abBoomer, "ABL_ChaoFeng_Boomerang"),
(abTornadoS, "ABL_ChaoFeng_TornadoSmall"),
(abTornadoL, "ABL_ChaoFeng_TornadoLarge"),
(abWind, "ABL_ChaoFeng_WindStone"),
};
foreach (var (comp, assetName) in abilityWiring)
{
Object so = FindFirstAsset(assetName);
if (so != null) AssignReference(comp, "_config", so, report);
else report.Add($"未找到 EnemyAbilitySO{assetName}" +
"请先在角色向导 Boss 页「一键创建全部 ChaoFeng SO」后重新运行本放置操作。");
}
// 近战三段 HitBox 走命名槽位(EnemyAttackSO.hitBoxSlot 按名索引)
AssignMeleeHitBoxSlots(abFan, new[] { ("fan_1", fan1), ("fan_2", fan2), ("fan_3", fan3) }, report);
// 阶段招池:地面四招 = 阶段 0,风石 = 阶段 1;
// 入场演出不登记(category=None,本就不进选招池,且需全阶段可用)
AssignPhaseGate(phaseGate, new (EnemyAbilityBase ability, int[] phases)[]
{
(abFan, new[] { 0 }),
(abBoomer, new[] { 0 }),
(abTornadoS, new[] { 0 }),
(abTornadoL, new[] { 0 }),
(abWind, new[] { 1 }),
}, report);
Object dmgSrc = FindFirstAsset("CMB_DS_BossBody", "CMB_DS_EnemyBody");
if (dmgSrc != null)
@@ -1183,12 +1218,16 @@ namespace BaseGames.Editor
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "sight" }, report);
report.Add("★ FanCombo 三段 HitBox 与 Tornado HitBox 已挂入 BossSkillExecutor._hitBoxes。");
report.Add("★ 将 BoomerangMuzzle / TornadoMuzzle / WindStoneMuzzle 拖入对应 BossSkillSO 的发射点字段(如有)。");
report.Add("★ FanCombo 三段 HitBox 已绑为命名槽位 fan_1 / fan_2 / fan_3" +
"在各 EATK_ 资产的 hitBoxSlot 里填对应名字。");
report.Add("★ 回旋扇 / 龙卷 / 风石的生成由技能 clip 上的 SpawnProjectile 动画事件驱动," +
"payload 填 boomerang / tornado_small / tornado_large / wind_stone" +
"炮口引用留在 ChaoFengBoss 上,无需再拖到 SO。");
report.Add("★ 回旋扇收招 / 阶段过渡 / 击败演出等动画 Clip 待美术接入后在 ChaoFengBoss Inspector 指定。");
report.Add("★ AI(BrainGraph):用菜单 BaseGames/AI/Enemy AI Recipe Wizard 建一个配方资产," +
"在 Inspector 选两层模块(未发现/交战),再挂 EnemyAiBrain 组件并把 _recipe 指向该资产" +
"(技能执行仍由 BossSkillExecutor 负责)。有独门机制的敌人才写 [AiDefinition] 的 AiScript 子类并改用 _definitionId。");
report.Add("★ AIBoss 走定制路径,EnemyAiBrain._definitionId 已设为 ChaoFeng(对应 ChaoFengAi)。" +
"小怪才用配方(_recipe)。");
report.Add("★ 开战触发:Boss 房间入口需另放一个带 BossFightTrigger 的触发区," +
"指向本 Boss 的 EnemyAiBrain 并填 bossId=ChaoFeng,否则 Boss 不会开战。");
Undo.CollapseUndoOperations(undoGroup);
Selection.activeGameObject = go;
@@ -2448,6 +2487,37 @@ namespace BaseGames.Editor
report?.Add($"[OK] MeleeAttackAbility._hitBoxSlots 已配置 {slots.Length} 个槽位。");
}
/// <summary>
/// 为 BossPhaseAbilityGate._entries 赋值(struct 数组 {ability, phases})。
/// phases 为空数组 = 该能力全阶段可用。
/// </summary>
private static void AssignPhaseGate(BossPhaseAbilityGate gate,
(EnemyAbilityBase ability, int[] phases)[] entries,
List<string> report)
{
var so = new SerializedObject(gate);
var prop = so.FindProperty("_entries");
if (prop == null || !prop.isArray)
{
report?.Add("[WARN] BossPhaseAbilityGate._entries 属性未找到,阶段招池未写入,请检查字段名。");
return;
}
prop.arraySize = entries.Length;
for (int i = 0; i < entries.Length; i++)
{
var elem = prop.GetArrayElementAtIndex(i);
elem.FindPropertyRelative("ability").objectReferenceValue = entries[i].ability;
var phasesProp = elem.FindPropertyRelative("phases");
int[] phases = entries[i].phases ?? System.Array.Empty<int>();
phasesProp.arraySize = phases.Length;
for (int p = 0; p < phases.Length; p++)
phasesProp.GetArrayElementAtIndex(p).intValue = phases[p];
}
so.ApplyModifiedPropertiesWithoutUndo();
report?.Add($"[OK] BossPhaseAbilityGate._entries 已写入 {entries.Length} 条阶段招池。");
}
private static Object FindFirstAsset(params string[] candidates)
{
foreach (string candidate in candidates)