diff --git a/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs b/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs index b7ebec27..a2a77b66 100644 --- a/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs +++ b/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs @@ -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(go); EnemyMovement movement = GetOrAddComponent(go); GetOrAddComponent(go); // 地面导航(直接移动,无寻路图) - BossSkillExecutor skillExec = GetOrAddComponent(go); + EnemyAiBrain brain = GetOrAddComponent(go); + BossPhaseAbilityGate phaseGate = GetOrAddComponent(go); ChaoFengFloatController floatCtrl = GetOrAddComponent(go); ChaoFengKnockdownCounter knockdown = GetOrAddComponent(go); PhysicsPerceptionSystem sensorHub = GetOrAddComponent(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( + GetOrCreateChild(abilitiesT, "PlayClipAbility_Intro").gameObject); + var abFan = GetOrAddComponent( + GetOrCreateChild(abilitiesT, "MeleeAttackAbility_FanCombo").gameObject); + var abBoomer = GetOrAddComponent( + GetOrCreateChild(abilitiesT, "PlayClipAbility_Boomerang").gameObject); + var abTornadoS = GetOrAddComponent( + GetOrCreateChild(abilitiesT, "PlayClipAbility_TornadoSmall").gameObject); + var abTornadoL = GetOrAddComponent( + GetOrCreateChild(abilitiesT, "PlayClipAbility_TornadoLarge").gameObject); + var abWind = GetOrAddComponent( + 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(); - 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("★ 回旋扇收招/阶段过渡/击败演出等动画 Clip 待美术接入后在 ChaoFengBoss Inspector 指定。"); - report.Add("★ AI(BrainGraph):用菜单 BaseGames/AI/Enemy AI Recipe Wizard 建一个配方资产," + - "在 Inspector 选两层模块(未发现/交战),再挂 EnemyAiBrain 组件并把 _recipe 指向该资产" + - "(技能执行仍由 BossSkillExecutor 负责)。有独门机制的敌人才写 [AiDefinition] 的 AiScript 子类并改用 _definitionId。"); + 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:Boss 走定制路径,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} 个槽位。"); } + /// + /// 为 BossPhaseAbilityGate._entries 赋值(struct 数组 {ability, phases})。 + /// phases 为空数组 = 该能力全阶段可用。 + /// + private static void AssignPhaseGate(BossPhaseAbilityGate gate, + (EnemyAbilityBase ability, int[] phases)[] entries, + List 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(); + 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)