Refactor code structure for improved readability and maintainability

This commit is contained in:
2026-05-26 13:04:38 +08:00
parent f74d7f1877
commit 5a0f1548ea
53 changed files with 4853 additions and 163 deletions

View File

@@ -15,6 +15,7 @@
"BaseGames.Player",
"BaseGames.Player.States",
"BaseGames.Enemies",
"BaseGames.Enemies.Navigation",
"BaseGames.Camera",
"BaseGames.World",
"BaseGames.UI",

View File

@@ -8,6 +8,7 @@ using UnityEngine.UIElements;
using BaseGames.Boss;
using BaseGames.Combat;
using BaseGames.Enemies;
using BaseGames.Enemies.Abilities;
using BaseGames.Equipment;
using BaseGames.Input;
using BaseGames.Parry;
@@ -56,13 +57,24 @@ namespace BaseGames.Editor
private List<(string label, bool exists)> _bossSOStatus = new();
private double _lastRefreshTime;
// 小怪类型选择
private int _enemyTypeIndex = 0;
private static readonly string[] EnemyTypeLabels = { "普通(近战)", "远程", "飞行" };
// 小怪类型选择 — 具体敌人类型
private int _enemyTypeIndex = 0;
private static readonly (string id, string name)[] EnemyTypes =
{
("E001", "草蛭"),
("E002", "簧蛭"),
("E003", "幼蛭"),
("E004", "蛭母"),
("E005", "肥蛭"),
("E006", "讙"),
};
// 动态内容区(类型切换时重建)
private VisualElement _enemyContentArea;
// Boss 命名字段
private string _bossId = "NewBoss";
private string _enemyId = "NewEnemy";
private string _bossId = "NewBoss"; // kept for legacy SkillSequenceSO queries if any
private string _enemyId = "E001"; // kept for legacy status calls if any
private string _playerId = "Player";
// SO 状态面板(按标签页缓存)
@@ -202,59 +214,88 @@ namespace BaseGames.Editor
_enemyStatusPanel = new VisualElement();
root.Add(_enemyStatusPanel);
root.Add(MakeSectionHeader("▶ 敌人类型选择"));
root.Add(MakeSectionHeader("▶ 敌人类型"));
root.Add(MakeHelpBox("选择要创建的具体敌人类型,对应 SO 工厂与场景放置按钮会自动切换。"));
var typeRow = new VisualElement { style = { flexDirection = FlexDirection.Row, marginBottom = 4 } };
for (int i = 0; i < EnemyTypeLabels.Length; i++)
var typeRow = new VisualElement();
typeRow.style.flexDirection = FlexDirection.Row;
typeRow.style.flexWrap = Wrap.Wrap;
typeRow.style.marginBottom = 4;
for (int i = 0; i < EnemyTypes.Length; i++)
{
int captured = i;
var (id, name) = EnemyTypes[i];
var btn = new Button(() =>
{
_enemyTypeIndex = captured;
// 高亮激活按钮(简单刷新所有同类按钮样式)
RefreshEnemyTypeButtons(root);
RefreshEnemyTypeButtons(typeRow);
RefreshEnemyTabContent(_enemyContentArea);
RefreshSOStatus();
})
{ text = EnemyTypeLabels[i] };
{ text = $"{id} {name}" };
btn.name = $"enemy-type-{i}";
btn.EnableInClassList("type-btn--active", i == _enemyTypeIndex);
typeRow.Add(btn);
}
root.Add(typeRow);
root.Add(MakeSectionHeader("▶ SO 资产工厂"));
root.Add(MakeHelpBox("每个敌人建议独立命名,便于 Loot / BD 资产管理。"));
var idRow = MakeLabeledTextField("敌人 ID", _enemyId, v => _enemyId = v);
root.Add(idRow);
var factory = MakeActionGroup();
factory.Add(MakeFactoryButton("EnemyStatsSO", () => CreateEnemyStat()));
factory.Add(MakeFactoryButton("LootTableSO", () => CreateLootTable()));
factory.Add(MakeFactoryButton("AttackPatternSO × 2", () => CreateEnemyAttackPatterns()));
factory.Add(MakeFactoryButton("DamageSourceSO", () => CreateEnemyDamageSource()));
root.Add(factory);
root.Add(MakeSeparator());
root.Add(MakeSectionHeader("▶ 场景搭建"));
root.Add(MakeHelpBox("根据选中的类型在场景中生成对应的敌人 GameObject。"));
var sceneGroup = MakeActionGroup();
sceneGroup.Add(MakeSceneButton("放置敌人到场景", PlaceSelectedEnemyType));
root.Add(sceneGroup);
_enemyContentArea = new VisualElement();
root.Add(_enemyContentArea);
RefreshEnemyTabContent(_enemyContentArea);
root.Add(MakeSeparator());
root.Add(MakeSectionHeader("▶ 专项编辑器"));
var jumpGroup = MakeActionGroup();
jumpGroup.Add(MakeJumpButton("Data Hub敌人数据", DataHubWindow.Open));
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
root.Add(jumpGroup);
return root;
}
private void RefreshEnemyTabContent(VisualElement container)
{
if (container == null) return;
container.Clear();
var (id, name) = EnemyTypes[_enemyTypeIndex];
string dir = $"{DataRoot}/Enemies/{id}";
string ablDir = $"{dir}/Abilities";
container.Add(MakeSectionHeader($"▶ SO 资产工厂({id} {name}"));
container.Add(MakeHelpBox($"统计 SO 路径:{dir}\n能力配置{ablDir}"));
var factory = MakeActionGroup();
factory.Add(MakeFactoryButton($"ENM_{id}_Stats.asset", () => { CreateEnemyStatsSO(id); RefreshSOStatus(); }));
factory.Add(MakeFactoryButton($"ENM_{id}_AnimConfig.asset", () => { CreateEnemyAnimConfigSO(id); RefreshSOStatus(); }));
foreach (var (ablName, ablId) in GetEnemyAbilityDefs(id))
{
string capturedName = ablName;
string capturedId = ablId;
factory.Add(MakeFactoryButton($"ABL_{id}_{capturedName}.asset",
() => { CreateEnemyAbilitySO(id, capturedName, capturedId); RefreshSOStatus(); }));
}
container.Add(factory);
var createAllBtn = new Button(() => { CreateAllEnemySOs(id); RefreshSOStatus(); })
{ text = $"★ 一键创建全部 {id} SO" };
createAllBtn.AddToClassList("wizard-create-all-btn");
container.Add(createAllBtn);
container.Add(MakeSeparator());
container.Add(MakeSectionHeader("▶ 场景搭建"));
container.Add(MakeHelpBox("在当前活动场景中放置完整组件树并自动绑定已有 SO。"));
var sceneGroup = MakeActionGroup();
string sceneLabel = $"放置 {id} {name} 到场景";
sceneGroup.Add(MakeSceneButton(sceneLabel, () => PlaceSpecificEnemy(id)));
container.Add(sceneGroup);
}
// ════════════════════════════════════════════════════════════════════════
// Boss 标签页
// Boss 标签页(嘲风专属)
// ════════════════════════════════════════════════════════════════════════
private VisualElement BuildBossTab()
@@ -265,27 +306,30 @@ namespace BaseGames.Editor
_bossStatusPanel = new VisualElement();
root.Add(_bossStatusPanel);
root.Add(MakeSectionHeader("▶ SO 资产工厂"));
root.Add(MakeHelpBox("每个 Boss 独立目录Assets/_Game/Data/Enemies/<BossId>/"));
var idRow = MakeLabeledTextField("Boss ID", _bossId, v => _bossId = v);
root.Add(idRow);
root.Add(MakeSectionHeader("▶ SO 资产工厂(嘲风 ChaoFeng"));
root.Add(MakeHelpBox("路径Assets/_Game/Data/Enemies/ChaoFeng/\n能力配置Assets/_Game/Data/Enemies/ChaoFeng/Abilities/"));
var factory = MakeActionGroup();
factory.Add(MakeFactoryButton("EnemyStatsSOBoss", () => CreateBossStat()));
factory.Add(MakeFactoryButton("LootTableSOBoss", () => CreateBossLoot()));
factory.Add(MakeFactoryButton("AttackPatternSO × 3阶段", () => CreateBossAttackPatterns()));
factory.Add(MakeFactoryButton("BossSkillSO × 3", () => CreateBossSkills()));
factory.Add(MakeFactoryButton("SkillSequenceSOPhase 1", () => CreateBossSkillSequence(1)));
factory.Add(MakeFactoryButton("SkillSequenceSOPhase 2", () => CreateBossSkillSequence(2)));
factory.Add(MakeFactoryButton("DamageSourceSO × 3", () => CreateBossDamageSources()));
factory.Add(MakeFactoryButton("ENM_ChaoFeng_Stats.asset", () => { CreateChaoFengStatsSO(); RefreshSOStatus(); }));
factory.Add(MakeFactoryButton("ENM_ChaoFeng_AnimConfig.asset",() => { CreateChaoFengAnimConfigSO(); RefreshSOStatus(); }));
factory.Add(MakeFactoryButton("ABL_ChaoFeng_Idle.asset", () => { CreateChaoFengSkillSO("Idle", "chaofeng_idle"); RefreshSOStatus(); }));
factory.Add(MakeFactoryButton("ABL_ChaoFeng_Slam.asset", () => { CreateChaoFengSkillSO("Slam", "chaofeng_slam"); RefreshSOStatus(); }));
factory.Add(MakeFactoryButton("ABL_ChaoFeng_Sweep.asset", () => { CreateChaoFengSkillSO("Sweep", "chaofeng_sweep"); RefreshSOStatus(); }));
factory.Add(MakeFactoryButton("ABL_ChaoFeng_WindBlade.asset", () => { CreateChaoFengSkillSO("WindBlade", "chaofeng_windblade"); RefreshSOStatus(); }));
factory.Add(MakeFactoryButton("ABL_ChaoFeng_Summon.asset", () => { CreateChaoFengSkillSO("Summon", "chaofeng_summon"); RefreshSOStatus(); }));
root.Add(factory);
var createAllBtn = new Button(() => { CreateAllChaoFengSOs(); RefreshSOStatus(); })
{ text = "★ 一键创建全部 ChaoFeng SO" };
createAllBtn.AddToClassList("wizard-create-all-btn");
root.Add(createAllBtn);
root.Add(MakeSeparator());
root.Add(MakeSectionHeader("▶ 场景搭建"));
root.Add(MakeHelpBox("放置嘲风完整组件树ChaoFengBoss + 浮空控制器 + 击倒计数 + Phase1 HitBox × 4 + 炮口 × 3。"));
var sceneGroup = MakeActionGroup();
sceneGroup.Add(MakeSceneButton("放置 Boss 到场景", SceneObjectPlacerTool.PlaceBossEnemy));
sceneGroup.Add(MakeSceneButton("放置嘲风到场景并绑定 SO", SceneObjectPlacerTool.PlaceChaoFeng));
root.Add(sceneGroup);
root.Add(MakeSeparator());
@@ -294,7 +338,7 @@ namespace BaseGames.Editor
var jumpGroup = MakeActionGroup();
jumpGroup.Add(MakeJumpButton("Boss 技能序列查看器", BossSkillSequenceWindow.OpenWindow));
jumpGroup.Add(MakeJumpButton("Data HubBoss技能", DataHubWindow.Open));
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
root.Add(jumpGroup);
return root;
@@ -507,96 +551,113 @@ namespace BaseGames.Editor
EditorUtility.DisplayDialog("指定完成", msg, "确定");
}
// ── SO 资产工厂:小怪 ────────────────────────────────────────────────
// ── SO 资产工厂:小怪(按类型) ───────────────────────────────────────────
private void CreateEnemyStat()
/// <summary>返回指定敌人类型的 (abilityFileName, abilityId) 定义列表。</summary>
private static (string ablName, string ablId)[] GetEnemyAbilityDefs(string enemyId) => enemyId switch
{
string dir = $"{DataRoot}/Enemies/{_enemyId}";
var asset = EditorScaffoldUtils.CreateSOAsset<EnemyStatsSO>(dir, $"ENM_{_enemyId}_Stats");
if (asset != null) RefreshSOStatus();
"E001" => new[] { ("Alert", "e001_alert"), ("Chase", "e001_chase") },
"E002" => new[] { ("Strike", "e002_strike") },
"E003" => new[] { ("Fall", "e003_fall") },
"E004" => new[] { ("Bite", "e004_bite"), ("Slam", "e004_slam"), ("Acid", "e004_acid"),
("Charge", "e004_charge"), ("Chase", "e004_chase") },
"E005" => new[] { ("Bite", "e005_bite"), ("Acid", "e005_acid") },
"E006" => new[] { ("Leap", "e006_leap"), ("Chase", "e006_chase") },
_ => System.Array.Empty<(string, string)>(),
};
private static void CreateEnemyStatsSO(string id)
{
string dir = $"Assets/_Game/Data/Enemies/{id}";
EditorScaffoldUtils.CreateSOAsset<EnemyStatsSO>(dir, $"ENM_{id}_Stats");
}
private void CreateLootTable()
private static void CreateEnemyAnimConfigSO(string id)
{
string dir = $"{DataRoot}/Enemies/{_enemyId}";
var asset = EditorScaffoldUtils.CreateSOAsset<LootTableSO>(dir, $"ENM_{_enemyId}_Loot");
if (asset != null) RefreshSOStatus();
string dir = $"Assets/_Game/Data/Enemies/{id}";
EditorScaffoldUtils.CreateSOAsset<EnemyAnimationConfigSO>(dir, $"ENM_{id}_AnimConfig");
}
private void CreateEnemyAttackPatterns()
private static void CreateEnemyAbilitySO(string enemyId, string ablName, string ablId)
{
string dir = $"{DataRoot}/Enemies/{_enemyId}";
foreach (var label in new[] { "Melee", "Ranged" })
EditorScaffoldUtils.CreateSOAsset<AttackPatternSO>(dir, $"ENM_{_enemyId}_Pattern_{label}");
RefreshSOStatus();
}
private void CreateEnemyDamageSource()
{
string dir = $"{DataRoot}/Enemies/{_enemyId}";
EditorScaffoldUtils.CreateSOAsset<DamageSourceSO>(dir, $"ENM_{_enemyId}_DS");
RefreshSOStatus();
}
// ── SO 资产工厂Boss ─────────────────────────────────────────────────
private void CreateBossStat()
{
string dir = $"{DataRoot}/Enemies/{_bossId}";
EditorScaffoldUtils.CreateSOAsset<EnemyStatsSO>(dir, $"ENM_{_bossId}_Stats");
RefreshSOStatus();
}
private void CreateBossLoot()
{
string dir = $"{DataRoot}/Enemies/{_bossId}";
EditorScaffoldUtils.CreateSOAsset<LootTableSO>(dir, $"ENM_{_bossId}_Loot");
RefreshSOStatus();
}
private void CreateBossAttackPatterns()
{
string dir = $"{DataRoot}/Enemies/{_bossId}/Patterns";
foreach (var label in new[] { "Phase1", "Phase2_A", "Phase2_B" })
EditorScaffoldUtils.CreateSOAsset<AttackPatternSO>(dir, $"ENM_{_bossId}_Pattern_{label}");
RefreshSOStatus();
}
private void CreateBossSkills()
{
string dir = $"{DataRoot}/Enemies/{_bossId}/Skills";
foreach (var label in new[] { "Skill_Slam", "Skill_Sweep", "Skill_Summon" })
EditorScaffoldUtils.CreateSOAsset<BossSkillSO>(dir, $"SKL_{_bossId}_{label}");
RefreshSOStatus();
}
private void CreateBossSkillSequence(int phase)
{
string dir = $"{DataRoot}/Enemies/{_bossId}/Skills";
EditorScaffoldUtils.CreateSOAsset<SkillSequenceSO>(dir, $"SKL_{_bossId}_Phase{phase}_Sequence");
RefreshSOStatus();
}
private void CreateBossDamageSources()
{
string dir = $"{DataRoot}/Enemies/{_bossId}/DamageSources";
foreach (var label in new[] { "Slam", "Sweep", "Projectile" })
EditorScaffoldUtils.CreateSOAsset<DamageSourceSO>(dir, $"ENM_{_bossId}_DS_{label}");
RefreshSOStatus();
}
// ── 场景搭建 ──────────────────────────────────────────────────────────
private void PlaceSelectedEnemyType()
{
switch (_enemyTypeIndex)
string dir = $"Assets/_Game/Data/Enemies/{enemyId}/Abilities";
string name = $"ABL_{enemyId}_{ablName}";
var so = EditorScaffoldUtils.CreateSOAsset<EnemyAbilitySO>(dir, name);
// Set abilityId on newly-created SO (skip if already existed = null returned)
if (so != null)
{
case 0: SceneObjectPlacerTool.PlaceEnemy(); break;
case 1: SceneObjectPlacerTool.PlaceEnemy(); break; // 复用,类型通过 SO 区分
case 2: SceneObjectPlacerTool.PlaceEnemy(); break;
so.abilityId = ablId;
EditorUtility.SetDirty(so);
AssetDatabase.SaveAssets();
}
}
private static void CreateAllEnemySOs(string id)
{
CreateEnemyStatsSO(id);
CreateEnemyAnimConfigSO(id);
foreach (var (ablName, ablId) in GetEnemyAbilityDefs(id))
CreateEnemyAbilitySO(id, ablName, ablId);
AssetDatabase.SaveAssets();
EditorUtility.DisplayDialog("创建完成",
$"全部 {id} SO 已创建(已存在的跳过)。\n请放置到场景后检查组件绑定。", "确定");
}
private static void PlaceSpecificEnemy(string id)
{
switch (id)
{
case "E001": SceneObjectPlacerTool.PlaceE001_CaoZhi(); break;
case "E002": SceneObjectPlacerTool.PlaceE002_HuangZhi(); break;
case "E003": SceneObjectPlacerTool.PlaceE003_YouZhi_Enemy(); break;
case "E004": SceneObjectPlacerTool.PlaceE004_ZhiMu_Enemy(); break;
case "E005": SceneObjectPlacerTool.PlaceE005_FeiZhi_Enemy(); break;
case "E006": SceneObjectPlacerTool.PlaceE006_Huan(); break;
default: SceneObjectPlacerTool.PlaceEnemy(); break;
}
}
// ── SO 资产工厂:嘲风 Boss ─────────────────────────────────────────────
private static void CreateChaoFengStatsSO()
{
string dir = "Assets/_Game/Data/Enemies/ChaoFeng";
EditorScaffoldUtils.CreateSOAsset<EnemyStatsSO>(dir, "ENM_ChaoFeng_Stats");
}
private static void CreateChaoFengAnimConfigSO()
{
string dir = "Assets/_Game/Data/Enemies/ChaoFeng";
EditorScaffoldUtils.CreateSOAsset<EnemyAnimationConfigSO>(dir, "ENM_ChaoFeng_AnimConfig");
}
private static void CreateChaoFengSkillSO(string skillName, string skillId)
{
string dir = "Assets/_Game/Data/Enemies/ChaoFeng/Abilities";
string name = $"ABL_ChaoFeng_{skillName}";
var so = EditorScaffoldUtils.CreateSOAsset<BossSkillSO>(dir, name);
if (so != null)
{
EditorUtility.SetDirty(so);
AssetDatabase.SaveAssets();
}
}
private static void CreateAllChaoFengSOs()
{
CreateChaoFengStatsSO();
CreateChaoFengAnimConfigSO();
foreach (var (n, id) in new[] { ("Idle","chaofeng_idle"), ("Slam","chaofeng_slam"),
("Sweep","chaofeng_sweep"), ("WindBlade","chaofeng_windblade"),
("Summon","chaofeng_summon") })
CreateChaoFengSkillSO(n, id);
AssetDatabase.SaveAssets();
EditorUtility.DisplayDialog("创建完成",
"全部嘲风 SO 已创建(已存在的跳过)。\n放置到场景后检查 BossSkillExecutor._skills 绑定。", "确定");
}
// ── 场景搭建(已移至 RefreshEnemyTabContent 内的内联按钮) ─────────────
// ── SO 状态面板刷新 ───────────────────────────────────────────────────
private void RefreshSOStatus()
@@ -636,16 +697,19 @@ namespace BaseGames.Editor
if (_enemyStatusPanel == null) return;
_enemyStatusPanel.Clear();
string dir = $"{DataRoot}/Enemies/{_enemyId}";
var checks = new (string label, UnityEngine.Object asset)[]
{
("EnemyStatsSO", FindAtPath<EnemyStatsSO>($"{dir}/ENM_{_enemyId}_Stats.asset")),
("LootTableSO", FindAtPath<LootTableSO>($"{dir}/ENM_{_enemyId}_Loot.asset")),
("AttackPatternSO×2", FindAtPath<AttackPatternSO>($"{dir}/ENM_{_enemyId}_Pattern_Melee.asset")),
("DamageSourceSO", FindAtPath<DamageSourceSO>($"{dir}/ENM_{_enemyId}_DS.asset")),
};
var (id, name) = EnemyTypes[_enemyTypeIndex];
string dir = $"{DataRoot}/Enemies/{id}";
string ablDir = $"{dir}/Abilities";
_enemyStatusPanel.Add(MakeStatusGrid(checks));
var items = new List<(string label, UnityEngine.Object asset)>
{
($"ENM_{id}_Stats", FindAtPath<EnemyStatsSO>($"{dir}/ENM_{id}_Stats.asset")),
($"ENM_{id}_AnimConfig",FindAtPath<EnemyAnimationConfigSO>($"{dir}/ENM_{id}_AnimConfig.asset")),
};
foreach (var (ablName, _) in GetEnemyAbilityDefs(id))
items.Add(($"ABL_{id}_{ablName}", FindAtPath<EnemyAbilitySO>($"{ablDir}/ABL_{id}_{ablName}.asset")));
_enemyStatusPanel.Add(MakeStatusGrid(items.ToArray()));
}
private void BuildBossStatus()
@@ -653,15 +717,17 @@ namespace BaseGames.Editor
if (_bossStatusPanel == null) return;
_bossStatusPanel.Clear();
string dir = $"{DataRoot}/Enemies/{_bossId}";
const string dir = "Assets/_Game/Data/Enemies/ChaoFeng";
const string ablDir = "Assets/_Game/Data/Enemies/ChaoFeng/Abilities";
var checks = new (string label, UnityEngine.Object asset)[]
{
("EnemyStatsSOBoss", FindAtPath<EnemyStatsSO>($"{dir}/ENM_{_bossId}_Stats.asset")),
("LootTableSO", FindAtPath<LootTableSO>($"{dir}/ENM_{_bossId}_Loot.asset")),
("AttackPatternSOPhase1", FindAtPath<AttackPatternSO>($"{dir}/Patterns/ENM_{_bossId}_Pattern_Phase1.asset")),
("BossSkillSO≥1", EditorScaffoldUtils.FindAllAssetsOfType<BossSkillSO>()
.FirstOrDefault(s => s.name.StartsWith("SKL_" + _bossId, StringComparison.OrdinalIgnoreCase))),
("SkillSequenceSOPhase1", FindAtPath<SkillSequenceSO>($"{dir}/Skills/SKL_{_bossId}_Phase1_Sequence.asset")),
("ENM_ChaoFeng_Stats", FindAtPath<EnemyStatsSO>($"{dir}/ENM_ChaoFeng_Stats.asset")),
("ENM_ChaoFeng_AnimConfig",FindAtPath<EnemyAnimationConfigSO>($"{dir}/ENM_ChaoFeng_AnimConfig.asset")),
("ABL_ChaoFeng_Idle", FindAtPath<BossSkillSO>($"{ablDir}/ABL_ChaoFeng_Idle.asset")),
("ABL_ChaoFeng_Slam", FindAtPath<BossSkillSO>($"{ablDir}/ABL_ChaoFeng_Slam.asset")),
("ABL_ChaoFeng_Sweep", FindAtPath<BossSkillSO>($"{ablDir}/ABL_ChaoFeng_Sweep.asset")),
("ABL_ChaoFeng_WindBlade", FindAtPath<BossSkillSO>($"{ablDir}/ABL_ChaoFeng_WindBlade.asset")),
("ABL_ChaoFeng_Summon", FindAtPath<BossSkillSO>($"{ablDir}/ABL_ChaoFeng_Summon.asset")),
};
_bossStatusPanel.Add(MakeStatusGrid(checks));
@@ -782,11 +848,11 @@ namespace BaseGames.Editor
return row;
}
private void RefreshEnemyTypeButtons(VisualElement tabRoot)
private void RefreshEnemyTypeButtons(VisualElement typeRow)
{
for (int i = 0; i < EnemyTypeLabels.Length; i++)
for (int i = 0; i < EnemyTypes.Length; i++)
{
var btn = tabRoot.Q<Button>($"enemy-type-{i}");
var btn = typeRow.Q<Button>($"enemy-type-{i}");
btn?.EnableInClassList("type-btn--active", i == _enemyTypeIndex);
}
}

View File

@@ -1,11 +1,16 @@
using System.Collections.Generic;
using System.Reflection;
using Animancer;
using BaseGames.Boss;
using BaseGames.Camera;
using BaseGames.Combat;
using BaseGames.Combat.StatusEffects;
using BaseGames.Dialogue;
using BaseGames.Enemies;
using BaseGames.Enemies.Abilities;
using BaseGames.Enemies.Boss;
using BaseGames.Enemies.Navigation;
using BaseGames.Enemies.Perception;
using BaseGames.Equipment;
using BaseGames.Parry;
using BaseGames.Player;
@@ -376,6 +381,741 @@ namespace BaseGames.Editor
MarkDirtyAndLog("Boss Enemy", go, report);
}
// ══ 具体敌人快速放置 ════════════════════════════════════════════════════
[MenuItem("BaseGames/Scene/Place/Enemy E001 (草蛭)", priority = 111)]
public static void PlaceE001_CaoZhi()
{
var report = new List<string>();
int undoGroup = Undo.GetCurrentGroup();
Undo.SetCurrentGroupName("Place E001 草蛭");
GameObject go = new GameObject("ENM_CaoZhi");
Undo.RegisterCreatedObjectUndo(go, "Place E001");
go.transform.position = GetDropPosition();
SetLayer(go, "Enemy", report);
Rigidbody2D rb = GetOrAddComponent<Rigidbody2D>(go);
rb.bodyType = RigidbodyType2D.Dynamic;
rb.gravityScale = 2f;
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
body.size = new Vector2(0.6f, 0.8f);
GetOrAddComponent<Animator>(go);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
SpriteRenderer sr1 = SetupSpriteRenderer(go);
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
GetOrAddComponent<EnemyNavAgent>(go);
GetOrAddComponent<NavAgent>(go);
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
CapsuleCollider2D hurtCap = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
hurtCap.isTrigger = true;
hurtCap.size = new Vector2(0.55f, 0.75f);
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
Transform contactT = GetOrCreateChild(go.transform, "ContactDamageZone");
SetLayer(contactT.gameObject, "EnemyHitBox", report);
CircleCollider2D contactCol = GetOrAddComponent<CircleCollider2D>(contactT.gameObject);
contactCol.isTrigger = true;
contactCol.radius = 0.4f;
HitBox contactHitBox = GetOrAddComponent<HitBox>(contactT.gameObject);
BodyContactDamage bodyContact = GetOrAddComponent<BodyContactDamage>(contactT.gameObject);
Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities");
Transform alertT = GetOrCreateChild(abilitiesT, "PlayClipAbility_Alert");
PlayClipAbility alertAbility = GetOrAddComponent<PlayClipAbility>(alertT.gameObject);
Transform chaseT = GetOrCreateChild(abilitiesT, "ContactChaseAbility_Chase");
ContactChaseAbility chaseAbility = GetOrAddComponent<ContactChaseAbility>(chaseT.gameObject);
// SOs — assign first so OnValidate doesn't warn during wiring
AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E001_Stats");
AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E001_AnimConfig");
// Component wiring
AssignReference(enemyBase, "_stats", enemyStats, report);
AssignReference(enemyBase, "_movement", movement, report);
AssignReference(enemyBase, "_animancer", animancer, report);
AssignReference(enemyBase, "_hurtBox", hurtBox, report);
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
AssignAsset(enemyBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
AssignAsset(enemyStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged");
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
AssignAsset(movement, "_config", report, false, "ENM_E001_Stats");
AssignAsset(movement, "_animConfig", report, false, "ENM_E001_AnimConfig");
AssignReference(movement, "_animancer", animancer, report);
AssignReference(movement, "_spriteRenderer", sr1, report);
AssignAsset(alertAbility, "_config", report, false, "ABL_E001_Alert");
AssignAsset(chaseAbility, "_config", report, false, "ABL_E001_Chase");
AssignReference(chaseAbility, "_contactDamage", bodyContact, report);
AssignReference(chaseAbility, "_sensorHub", sensorHub, report);
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
if (dmgSrc != null) AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);
SetupSensorHubSlotNames(sensorHub, new[] { "aggro", "wall_ahead", "ledge" }, report);
report.Add("★ 在 EnemySensorHub Inspector 中绑定 Sensor 子节点aggro/wall_ahead/ledge。");
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E001_CaoZhi.asset。");
Undo.CollapseUndoOperations(undoGroup);
Selection.activeGameObject = go;
MarkDirtyAndLog("E001 草蛭", go, report);
}
[MenuItem("BaseGames/Scene/Place/Enemy E002 (簧蛭)", priority = 112)]
public static void PlaceE002_HuangZhi()
{
var report = new List<string>();
int undoGroup = Undo.GetCurrentGroup();
Undo.SetCurrentGroupName("Place E002 簧蛭");
GameObject go = new GameObject("ENM_HuangZhi");
Undo.RegisterCreatedObjectUndo(go, "Place E002");
go.transform.position = GetDropPosition();
SetLayer(go, "Enemy", report);
Rigidbody2D rb = GetOrAddComponent<Rigidbody2D>(go);
rb.bodyType = RigidbodyType2D.Kinematic;
rb.gravityScale = 0f;
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
body.size = new Vector2(0.5f, 0.7f);
GetOrAddComponent<Animator>(go);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
SetupSpriteRenderer(go);
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
// HurtBox初始禁用附着天花板时不受伤
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
CapsuleCollider2D hurtCap = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
hurtCap.isTrigger = true;
hurtCap.size = new Vector2(0.45f, 0.65f);
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
hurtBoxT.gameObject.SetActive(false);
// AttackHitBox下坠发动时由能力启用
Transform atkT = GetOrCreateChild(go.transform, "AttackHitBox");
SetLayer(atkT.gameObject, "EnemyHitBox", report);
BoxCollider2D atkCol = GetOrAddComponent<BoxCollider2D>(atkT.gameObject);
atkCol.isTrigger = true;
atkCol.size = new Vector2(0.5f, 0.5f);
HitBox atkHitBox = GetOrAddComponent<HitBox>(atkT.gameObject);
atkT.gameObject.SetActive(false);
Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities");
Transform strikeT = GetOrCreateChild(abilitiesT, "CeilingHangStrikeAbility");
CeilingHangStrikeAbility strikeAbility = GetOrAddComponent<CeilingHangStrikeAbility>(strikeT.gameObject);
// SOs — assign first so OnValidate doesn't warn during wiring
AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E002_Stats");
AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E002_AnimConfig");
AssignReference(enemyBase, "_stats", enemyStats, report);
AssignReference(enemyBase, "_animancer", animancer, report);
AssignReference(enemyBase, "_hurtBox", hurtBox, report);
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
AssignAsset(enemyBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
AssignAsset(enemyStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged");
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
AssignAsset(strikeAbility, "_config", report, false, "ABL_E002_Strike");
AssignReference(strikeAbility, "_attackHitBox", atkHitBox, report);
AssignReference(strikeAbility, "_hurtBox", hurtBox, report);
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
if (dmgSrc != null) AssignReference(atkHitBox, "_defaultSource", dmgSrc, report);
SetupSensorHubSlotNames(sensorHub, new[] { "attack_range" }, report);
report.Add("★ 将此对象放置于天花板,调整位置使 CapsuleCollider 正好贴合天花板底面。");
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E002_HuangZhi.asset。");
Undo.CollapseUndoOperations(undoGroup);
Selection.activeGameObject = go;
MarkDirtyAndLog("E002 簧蛭", go, report);
}
[MenuItem("BaseGames/Scene/Place/Enemy E003 (幼蛭)", priority = 113)]
public static void PlaceE003_YouZhi_Enemy()
{
var report = new List<string>();
int undoGroup = Undo.GetCurrentGroup();
Undo.SetCurrentGroupName("Place E003 幼蛭");
GameObject go = new GameObject("ENM_YouZhi");
Undo.RegisterCreatedObjectUndo(go, "Place E003");
go.transform.position = GetDropPosition();
SetLayer(go, "Enemy", report);
Rigidbody2D rb = GetOrAddComponent<Rigidbody2D>(go);
rb.bodyType = RigidbodyType2D.Kinematic;
rb.gravityScale = 0f;
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
body.size = new Vector2(0.5f, 0.6f);
GetOrAddComponent<Animator>(go);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
SpriteRenderer sr3 = SetupSpriteRenderer(go);
E003_YouZhi enemyBase = GetOrAddComponent<E003_YouZhi>(go);
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
GetOrAddComponent<EnemyNavAgent>(go);
GetOrAddComponent<NavAgent>(go);
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
CapsuleCollider2D hurtCap = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
hurtCap.isTrigger = true;
hurtCap.size = new Vector2(0.45f, 0.55f);
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
Transform contactT = GetOrCreateChild(go.transform, "ContactDamageZone");
SetLayer(contactT.gameObject, "EnemyHitBox", report);
CircleCollider2D contactCol = GetOrAddComponent<CircleCollider2D>(contactT.gameObject);
contactCol.isTrigger = true;
contactCol.radius = 0.35f;
HitBox contactHitBox = GetOrAddComponent<HitBox>(contactT.gameObject);
BodyContactDamage bodyContact = GetOrAddComponent<BodyContactDamage>(contactT.gameObject);
Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities");
Transform fallT = GetOrCreateChild(abilitiesT, "AnimatedCeilingDropAbility");
AnimatedCeilingDropAbility fallAbility = GetOrAddComponent<AnimatedCeilingDropAbility>(fallT.gameObject);
// SOs — assign first so OnValidate doesn't warn during wiring
AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E003_Stats");
AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E003_AnimConfig");
AssignReference(enemyBase, "_stats", enemyStats, report);
AssignReference(enemyBase, "_movement", movement, report);
AssignReference(enemyBase, "_animancer", animancer, report);
AssignReference(enemyBase, "_hurtBox", hurtBox, report);
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
AssignAsset(enemyBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
AssignAsset(enemyStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged");
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
AssignAsset(movement, "_config", report, false, "ENM_E003_Stats");
AssignAsset(movement, "_animConfig", report, false, "ENM_E003_AnimConfig");
AssignReference(movement, "_animancer", animancer, report);
AssignReference(movement, "_spriteRenderer", sr3, report);
AssignAsset(fallAbility, "_config", report, false, "ABL_E003_Fall");
AssignReference(fallAbility, "_contactDamage", bodyContact, report);
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
if (dmgSrc != null) AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);
SetupSensorHubSlotNames(sensorHub, new[] { "aggro" }, report);
report.Add("★ 将此对象放置于天花板下方E003_YouZhi 会在 OnSpawn/ActivateFromCeiling 时执行下坠。");
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E003_YouZhi.asset。");
Undo.CollapseUndoOperations(undoGroup);
Selection.activeGameObject = go;
MarkDirtyAndLog("E003 幼蛭", go, report);
}
[MenuItem("BaseGames/Scene/Place/Enemy E004 (蛭母)", priority = 114)]
public static void PlaceE004_ZhiMu_Enemy()
{
var report = new List<string>();
int undoGroup = Undo.GetCurrentGroup();
Undo.SetCurrentGroupName("Place E004 蛭母");
GameObject go = new GameObject("ENM_ZhiMu");
Undo.RegisterCreatedObjectUndo(go, "Place E004");
go.transform.position = GetDropPosition();
SetLayer(go, "Enemy", report);
Rigidbody2D rb = GetOrAddComponent<Rigidbody2D>(go);
rb.bodyType = RigidbodyType2D.Dynamic;
rb.gravityScale = 2f;
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
body.size = new Vector2(0.8f, 1.2f);
GetOrAddComponent<Animator>(go);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
SpriteRenderer sr4 = SetupSpriteRenderer(go);
E004_ZhiMu enemyBase = GetOrAddComponent<E004_ZhiMu>(go);
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
EnemyFeedback feedback = GetOrAddComponent<EnemyFeedback>(go);
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
GetOrAddComponent<EnemyNavAgent>(go);
GetOrAddComponent<NavAgent>(go);
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
CapsuleCollider2D hurtCap = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
hurtCap.isTrigger = true;
hurtCap.size = new Vector2(0.75f, 1.1f);
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
Transform biteT = GetOrCreateChild(go.transform, "BiteHitBox");
SetLayer(biteT.gameObject, "EnemyHitBox", report);
BoxCollider2D biteCol = GetOrAddComponent<BoxCollider2D>(biteT.gameObject);
biteCol.isTrigger = true;
biteCol.size = new Vector2(0.6f, 0.4f);
HitBox biteHitBox = GetOrAddComponent<HitBox>(biteT.gameObject);
biteT.gameObject.SetActive(false);
Transform slamT = GetOrCreateChild(go.transform, "SlamHitBox");
SetLayer(slamT.gameObject, "EnemyHitBox", report);
CircleCollider2D slamCol = GetOrAddComponent<CircleCollider2D>(slamT.gameObject);
slamCol.isTrigger = true;
slamCol.radius = 0.7f;
HitBox slamHitBox = GetOrAddComponent<HitBox>(slamT.gameObject);
slamT.gameObject.SetActive(false);
Transform chargeHitBoxT = GetOrCreateChild(go.transform, "ChargeHitBox");
SetLayer(chargeHitBoxT.gameObject, "EnemyHitBox", report);
BoxCollider2D chargeHitCol = GetOrAddComponent<BoxCollider2D>(chargeHitBoxT.gameObject);
chargeHitCol.isTrigger = true;
chargeHitCol.size = new Vector2(0.9f, 0.8f);
HitBox chargeHitBox = GetOrAddComponent<HitBox>(chargeHitBoxT.gameObject);
chargeHitBoxT.gameObject.SetActive(false);
Transform acidMuzzleT = GetOrCreateChild(go.transform, "AcidMuzzle");
Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities");
Transform biteAblT = GetOrCreateChild(abilitiesT, "MeleeAttackAbility_Bite");
MeleeAttackAbility biteAbl = GetOrAddComponent<MeleeAttackAbility>(biteAblT.gameObject);
Transform slamAblT = GetOrCreateChild(abilitiesT, "RepeatSlamAbility");
RepeatSlamAbility slamAbl = GetOrAddComponent<RepeatSlamAbility>(slamAblT.gameObject);
Transform acidAblT = GetOrCreateChild(abilitiesT, "ProjectileAttackAbility_Acid");
ProjectileAttackAbility acidAbl = GetOrAddComponent<ProjectileAttackAbility>(acidAblT.gameObject);
Transform chargeAblT = GetOrCreateChild(abilitiesT, "ChargeAbility");
ChargeAbility chargeAbl = GetOrAddComponent<ChargeAbility>(chargeAblT.gameObject);
Transform chaseAblT = GetOrCreateChild(abilitiesT, "ContactChaseAbility");
ContactChaseAbility chaseAbl = GetOrAddComponent<ContactChaseAbility>(chaseAblT.gameObject);
// SOs — assign first so OnValidate doesn't warn during wiring
AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E004_Stats");
AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E004_AnimConfig");
AssignReference(enemyBase, "_stats", enemyStats, report);
AssignReference(enemyBase, "_movement", movement, report);
AssignReference(enemyBase, "_animancer", animancer, report);
AssignReference(enemyBase, "_feedback", feedback, report);
AssignReference(enemyBase, "_hurtBox", hurtBox, report);
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
AssignAsset(enemyBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
AssignAsset(enemyStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged");
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
AssignAsset(movement, "_config", report, false, "ENM_E004_Stats");
AssignAsset(movement, "_animConfig", report, false, "ENM_E004_AnimConfig");
AssignReference(movement, "_animancer", animancer, report);
AssignReference(movement, "_spriteRenderer", sr4, report);
AssignAsset(biteAbl, "_config", report, false, "ABL_E004_Bite");
AssignAsset(slamAbl, "_config", report, false, "ABL_E004_Slam");
AssignAsset(acidAbl, "_config", report, false, "ABL_E004_Acid");
AssignAsset(chargeAbl, "_config", report, false, "ABL_E004_Charge");
AssignAsset(chaseAbl, "_config", report, false, "ABL_E004_Chase");
AssignMeleeHitBoxSlots(biteAbl, new[] { ("bite", biteHitBox) }, report);
AssignReference(slamAbl, "_hitBox", slamHitBox, report);
AssignReference(acidAbl, "_muzzle", acidMuzzleT, report);
AssignReference(chargeAbl, "_chargeHitBox", chargeHitBox, report);
AssignReference(chaseAbl, "_sensorHub", sensorHub, report);
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
if (dmgSrc != null)
{
AssignReference(biteHitBox, "_defaultSource", dmgSrc, report);
AssignReference(slamHitBox, "_defaultSource", dmgSrc, report);
AssignReference(chargeHitBox, "_defaultSource", dmgSrc, report);
}
SetupSensorHubSlotNames(sensorHub, new[] { "aggro", "los" }, report);
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E004_ZhiMu.asset。");
Undo.CollapseUndoOperations(undoGroup);
Selection.activeGameObject = go;
MarkDirtyAndLog("E004 蛭母", go, report);
}
[MenuItem("BaseGames/Scene/Place/Enemy E005 (肥蛭)", priority = 115)]
public static void PlaceE005_FeiZhi_Enemy()
{
var report = new List<string>();
int undoGroup = Undo.GetCurrentGroup();
Undo.SetCurrentGroupName("Place E005 肥蛭");
GameObject go = new GameObject("ENM_FeiZhi");
Undo.RegisterCreatedObjectUndo(go, "Place E005");
go.transform.position = GetDropPosition();
SetLayer(go, "Enemy", report);
Rigidbody2D rb = GetOrAddComponent<Rigidbody2D>(go);
rb.bodyType = RigidbodyType2D.Dynamic;
rb.gravityScale = 2f;
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
body.size = new Vector2(0.9f, 1.0f);
GetOrAddComponent<Animator>(go);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
SpriteRenderer sr5 = SetupSpriteRenderer(go);
E005_FeiZhi enemyBase = GetOrAddComponent<E005_FeiZhi>(go);
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
GetOrAddComponent<EnemyNavAgent>(go);
GetOrAddComponent<NavAgent>(go);
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
CapsuleCollider2D hurtCap = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
hurtCap.isTrigger = true;
hurtCap.size = new Vector2(0.85f, 0.95f);
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
Transform biteT = GetOrCreateChild(go.transform, "BiteHitBox");
SetLayer(biteT.gameObject, "EnemyHitBox", report);
BoxCollider2D biteCol = GetOrAddComponent<BoxCollider2D>(biteT.gameObject);
biteCol.isTrigger = true;
biteCol.size = new Vector2(0.7f, 0.45f);
HitBox biteHitBox = GetOrAddComponent<HitBox>(biteT.gameObject);
biteT.gameObject.SetActive(false);
Transform acidMuzzleT = GetOrCreateChild(go.transform, "AcidMuzzle");
Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities");
Transform biteAblT = GetOrCreateChild(abilitiesT, "MeleeAttackAbility_Bite");
MeleeAttackAbility biteAbl = GetOrAddComponent<MeleeAttackAbility>(biteAblT.gameObject);
Transform acidAblT = GetOrCreateChild(abilitiesT, "ProjectileAttackAbility_Acid");
ProjectileAttackAbility acidAbl = GetOrAddComponent<ProjectileAttackAbility>(acidAblT.gameObject);
// SOs — assign first so OnValidate doesn't warn during wiring
AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E005_Stats");
AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E005_AnimConfig");
AssignReference(enemyBase, "_stats", enemyStats, report);
AssignReference(enemyBase, "_movement", movement, report);
AssignReference(enemyBase, "_animancer", animancer, report);
AssignReference(enemyBase, "_hurtBox", hurtBox, report);
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
AssignAsset(enemyBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
AssignAsset(enemyStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged");
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
AssignAsset(movement, "_config", report, false, "ENM_E005_Stats");
AssignAsset(movement, "_animConfig", report, false, "ENM_E005_AnimConfig");
AssignReference(movement, "_animancer", animancer, report);
AssignReference(movement, "_spriteRenderer", sr5, report);
AssignAsset(biteAbl, "_config", report, false, "ABL_E005_Bite");
AssignAsset(acidAbl, "_config", report, false, "ABL_E005_Acid");
AssignMeleeHitBoxSlots(biteAbl, new[] { ("bite", biteHitBox) }, report);
AssignReference(acidAbl, "_muzzle", acidMuzzleT, report);
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
if (dmgSrc != null) AssignReference(biteHitBox, "_defaultSource", dmgSrc, report);
SetupSensorHubSlotNames(sensorHub, new[] { "aggro" }, report);
report.Add("★ 在 E005_FeiZhi._deathPreClip 上添加 AnimationEvent 调用 SpawnProjectile(\"spawn_e003\")。");
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E005_FeiZhi.asset。");
Undo.CollapseUndoOperations(undoGroup);
Selection.activeGameObject = go;
MarkDirtyAndLog("E005 肥蛭", go, report);
}
[MenuItem("BaseGames/Scene/Place/Enemy E006 (讙)", priority = 116)]
public static void PlaceE006_Huan()
{
var report = new List<string>();
int undoGroup = Undo.GetCurrentGroup();
Undo.SetCurrentGroupName("Place E006 讙");
GameObject go = new GameObject("ENM_Huan");
Undo.RegisterCreatedObjectUndo(go, "Place E006");
go.transform.position = GetDropPosition();
SetLayer(go, "Enemy", report);
Rigidbody2D rb = GetOrAddComponent<Rigidbody2D>(go);
rb.bodyType = RigidbodyType2D.Dynamic;
rb.gravityScale = 2f;
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
body.size = new Vector2(0.7f, 1.0f);
GetOrAddComponent<Animator>(go);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
SpriteRenderer sr6 = SetupSpriteRenderer(go);
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
GetOrAddComponent<EnemyNavAgent>(go);
GetOrAddComponent<NavAgent>(go);
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
CapsuleCollider2D hurtCap = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
hurtCap.isTrigger = true;
hurtCap.size = new Vector2(0.65f, 0.95f);
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
Transform contactT = GetOrCreateChild(go.transform, "ContactDamageZone");
SetLayer(contactT.gameObject, "EnemyHitBox", report);
CircleCollider2D contactCol = GetOrAddComponent<CircleCollider2D>(contactT.gameObject);
contactCol.isTrigger = true;
contactCol.radius = 0.4f;
HitBox contactHitBox = GetOrAddComponent<HitBox>(contactT.gameObject);
BodyContactDamage bodyContact = GetOrAddComponent<BodyContactDamage>(contactT.gameObject);
Transform landT = GetOrCreateChild(go.transform, "LandingHitBox");
SetLayer(landT.gameObject, "EnemyHitBox", report);
CircleCollider2D landCol = GetOrAddComponent<CircleCollider2D>(landT.gameObject);
landCol.isTrigger = true;
landCol.radius = 0.8f;
HitBox landHitBox = GetOrAddComponent<HitBox>(landT.gameObject);
landT.gameObject.SetActive(false);
Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities");
Transform leapT = GetOrCreateChild(abilitiesT, "LeapAttackAbility");
LeapAttackAbility leapAbl = GetOrAddComponent<LeapAttackAbility>(leapT.gameObject);
Transform chaseT = GetOrCreateChild(abilitiesT, "ContactChaseAbility");
ContactChaseAbility chaseAbl = GetOrAddComponent<ContactChaseAbility>(chaseT.gameObject);
// SOs — assign first so OnValidate doesn't warn during wiring
AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E006_Stats");
AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E006_AnimConfig");
AssignReference(enemyBase, "_stats", enemyStats, report);
AssignReference(enemyBase, "_movement", movement, report);
AssignReference(enemyBase, "_animancer", animancer, report);
AssignReference(enemyBase, "_hurtBox", hurtBox, report);
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
AssignAsset(enemyBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
AssignAsset(enemyStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged");
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
AssignAsset(movement, "_config", report, false, "ENM_E006_Stats");
AssignAsset(movement, "_animConfig", report, false, "ENM_E006_AnimConfig");
AssignReference(movement, "_animancer", animancer, report);
AssignReference(movement, "_spriteRenderer", sr6, report);
AssignAsset(leapAbl, "_config", report, false, "ABL_E006_Leap");
AssignAsset(chaseAbl, "_config", report, false, "ABL_E006_Chase");
AssignReference(leapAbl, "_landingHitBox", landHitBox, report);
AssignReference(chaseAbl, "_contactDamage", bodyContact, report);
AssignReference(chaseAbl, "_sensorHub", sensorHub, report);
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
if (dmgSrc != null)
{
AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);
AssignReference(landHitBox, "_defaultSource", dmgSrc, report);
}
SetupSensorHubSlotNames(sensorHub, new[] { "aggro", "wall_ahead", "ledge" }, report);
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E006_Huan.asset。");
Undo.CollapseUndoOperations(undoGroup);
Selection.activeGameObject = go;
MarkDirtyAndLog("E006 讙", go, report);
}
[MenuItem("BaseGames/Scene/Place/Boss 嘲风 (ChaoFeng)", priority = 117)]
public static void PlaceChaoFeng()
{
var report = new List<string>();
int undoGroup = Undo.GetCurrentGroup();
Undo.SetCurrentGroupName("Place Boss 嘲风");
GameObject go = new GameObject("ENM_ChaoFeng");
Undo.RegisterCreatedObjectUndo(go, "Place ChaoFeng");
go.transform.position = GetDropPosition();
SetLayer(go, "Enemy", report);
Rigidbody2D rb = GetOrAddComponent<Rigidbody2D>(go);
rb.bodyType = RigidbodyType2D.Dynamic;
rb.gravityScale = 2f;
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
rb.interpolation = RigidbodyInterpolation2D.Interpolate;
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
body.size = new Vector2(1.2f, 2.0f);
GetOrAddComponent<Animator>(go);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
SpriteRenderer srBoss = SetupSpriteRenderer(go);
ChaoFengBoss bossBase = GetOrAddComponent<ChaoFengBoss>(go);
EnemyStats bossStats = GetOrAddComponent<EnemyStats>(go);
EnemyFeedback feedback = GetOrAddComponent<EnemyFeedback>(go);
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
GetOrAddComponent<EnemyNavAgent>(go);
GetOrAddComponent<NavAgent>(go);
BossSkillExecutor skillExec = GetOrAddComponent<BossSkillExecutor>(go);
ChaoFengFloatController floatCtrl = GetOrAddComponent<ChaoFengFloatController>(go);
ChaoFengKnockdownCounter knockdown = GetOrAddComponent<ChaoFengKnockdownCounter>(go);
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
// HurtBox
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
CapsuleCollider2D hurtCap = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
hurtCap.isTrigger = true;
hurtCap.size = new Vector2(1.1f, 1.9f);
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
// Phase1 attack hitboxes (disabled by default; abilities enable/disable as needed)
HitBox biteHB = CreateDisabledHitBox(go.transform, "Phase1_BiteHitBox", "EnemyHitBox",
true, report, size: new Vector2(0.8f, 0.5f));
HitBox swipeR = CreateDisabledHitBox(go.transform, "Phase1_SwipeHitBox_R","EnemyHitBox",
true, report, size: new Vector2(1.2f, 0.4f));
HitBox swipeL = CreateDisabledHitBox(go.transform, "Phase1_SwipeHitBox_L","EnemyHitBox",
true, report, size: new Vector2(1.2f, 0.4f));
HitBox stompHB = CreateDisabledHitBox(go.transform, "Phase1_StompHitBox", "EnemyHitBox",
false, report, radius: 1.0f);
// Muzzle transforms for Phase 2 skills
GetOrCreateChild(go.transform, "WindBladeMuzzle");
GetOrCreateChild(go.transform, "TornadoMuzzle");
GetOrCreateChild(go.transform, "SummonSpawnPoint");
// 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");
// Component wiring
AssignReference(bossBase, "_stats", bossStats, report);
AssignReference(bossBase, "_movement", movement, report);
AssignReference(bossBase, "_animancer", animancer, report);
AssignReference(bossBase, "_feedback", feedback, report);
AssignReference(bossBase, "_hurtBox", hurtBox, report);
AssignReference(skillExec, "_animancer", animancer, report);
AssignAsset(bossBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
AssignAsset(bossBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
AssignAsset(bossBase, "_onBossFightEnded", report, false, "EVT_BossFightEnded");
AssignAsset(bossBase, "_onBossPhaseChanged", report, false, "EVT_BossPhaseChanged");
AssignAsset(bossStats, "_onDifficultyChanged",report, false, "EVT_DifficultyChanged");
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
AssignAsset(movement, "_config", report, false, "ENM_ChaoFeng_Stats");
AssignAsset(movement, "_animConfig", report, false, "ENM_ChaoFeng_AnimConfig");
AssignReference(movement, "_animancer", animancer, report);
AssignReference(movement, "_spriteRenderer", srBoss, report);
// Collect BossSkillSOs and assign to executor
var skillAssets = new System.Collections.Generic.List<Object>();
foreach (var n in new[] { "ABL_ChaoFeng_Idle", "ABL_ChaoFeng_Slam", "ABL_ChaoFeng_Sweep",
"ABL_ChaoFeng_WindBlade", "ABL_ChaoFeng_Summon" })
{
Object sk = FindFirstAsset(n);
if (sk != null) skillAssets.Add(sk);
else report.Add($"未找到 BossSkillSO{n},请先一键创建 SO 后再重新运行此放置操作。");
}
if (skillAssets.Count > 0)
AssignObjectArray(skillExec, "_skills", skillAssets.ToArray(), report);
Object dmgSrc = FindFirstAsset("CMB_DS_BossBody", "CMB_DS_EnemyBody");
if (dmgSrc != null)
{
foreach (var hb in new[] { biteHB, swipeR, swipeL, stompHB })
if (hb != null) AssignReference(hb, "_defaultSource", dmgSrc, report);
}
SetupSensorHubSlotNames(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "los" }, report);
report.Add("★ 设置 BossSkillExecutor._bossId = \"ChaoFeng\"。");
report.Add("★ 将各 Phase1 HitBox 引用拖入 BossSkillExecutor._hitBoxes 数组。");
report.Add("★ 将 WindBladeMuzzle / TornadoMuzzle / SummonSpawnPoint 拖入对应 BossSkillSO 字段。");
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 Boss_ChaoFeng.asset。");
Undo.CollapseUndoOperations(undoGroup);
Selection.activeGameObject = go;
MarkDirtyAndLog("Boss 嘲风 (ChaoFeng)", go, report);
}
// ══ 敌人放置辅助方法 ═══════════════════════════════════════════════════
/// <summary>
/// 创建禁用的 HitBox 子节点Box 或 Circle 碰撞体)。
/// </summary>
private static HitBox CreateDisabledHitBox(Transform parent, string childName, string layer,
bool isBox, List<string> report,
Vector2 size = default, float radius = 0.5f)
{
Transform t = GetOrCreateChild(parent, childName);
SetLayer(t.gameObject, layer, report);
if (isBox)
{
BoxCollider2D col = GetOrAddComponent<BoxCollider2D>(t.gameObject);
col.isTrigger = true;
col.size = size == default ? new Vector2(0.5f, 0.5f) : size;
}
else
{
CircleCollider2D col = GetOrAddComponent<CircleCollider2D>(t.gameObject);
col.isTrigger = true;
col.radius = radius;
}
HitBox hb = GetOrAddComponent<HitBox>(t.gameObject);
t.gameObject.SetActive(false);
return hb;
}
/// <summary>
/// 设置 EnemySensorHub._slots 的 slotName 字段Sensor 引用需在 Inspector 中手工绑定)。
/// </summary>
private static void SetupSensorHubSlotNames(EnemySensorHub hub, string[] slotNames, List<string> report)
{
var so = new SerializedObject(hub);
var slots = so.FindProperty("_slots");
if (slots == null || !slots.isArray)
{
report?.Add("EnemySensorHub._slots 字段未找到,传感器槽位需手工配置。");
return;
}
slots.arraySize = slotNames.Length;
for (int i = 0; i < slotNames.Length; i++)
slots.GetArrayElementAtIndex(i).FindPropertyRelative("slotName").stringValue = slotNames[i];
so.ApplyModifiedPropertiesWithoutUndo();
}
[MenuItem("BaseGames/Scene/Place/Hazard (LethalTrap)", priority = 120)]
public static void PlaceLethalTrap()
{
@@ -1084,6 +1824,30 @@ namespace BaseGames.Editor
so.ApplyModifiedPropertiesWithoutUndo();
}
/// <summary>
/// 为 MeleeAttackAbility._hitBoxSlots 赋值struct 数组 {slotName, hitBox})。
/// </summary>
private static void AssignMeleeHitBoxSlots(MeleeAttackAbility ability, (string slot, HitBox hb)[] slots, List<string> report)
{
if (ability == null) return;
var so = new SerializedObject(ability);
var prop = so.FindProperty("_hitBoxSlots");
if (prop == null || !prop.isArray)
{
report?.Add($"[WARN] MeleeAttackAbility._hitBoxSlots 属性未找到,请检查字段名。");
return;
}
prop.arraySize = slots.Length;
for (int i = 0; i < slots.Length; i++)
{
var elem = prop.GetArrayElementAtIndex(i);
elem.FindPropertyRelative("slotName").stringValue = slots[i].slot;
elem.FindPropertyRelative("hitBox").objectReferenceValue = slots[i].hb;
}
so.ApplyModifiedPropertiesWithoutUndo();
report?.Add($"[OK] MeleeAttackAbility._hitBoxSlots 已配置 {slots.Length} 个槽位。");
}
private static Object FindFirstAsset(params string[] candidates)
{
foreach (string candidate in candidates)