using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.UIElements; using UnityEngine; 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; using BaseGames.Player; using BaseGames.Player.States; using BaseGames.Skills; namespace BaseGames.Editor { /// /// 角色创建向导(W-01)— 统一入口窗口。 /// 技术:UI Toolkit,三标签页(玩家 / 小怪 / Boss)。 /// 菜单:BaseGames / Tools / Character Wizard /// /// 各标签页均提供: /// ① 当前 SO 资产状态速览(绿色=已存在 / 橙色=缺失) /// ② SO 资产工厂(一键创建所需的所有 ScriptableObject) /// ③ 场景搭建快捷按钮(调用 SceneObjectPlacerTool.PlaceXxx) /// ④ 跳转到对应专项编辑器窗口 /// public class CharacterWizardWindow : EditorWindow { // ── 常量 ────────────────────────────────────────────────────────────── private const string UssPath = "Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss"; private const string DataRoot = "Assets/_Game/Data"; private static StyleSheet _uss; private static StyleSheet Uss => _uss != null ? _uss : (_uss = AssetDatabase.LoadAssetAtPath(UssPath)); [MenuItem("BaseGames/Data/Character Wizard", priority = 1)] public static void Open() { var wnd = GetWindow(); wnd.titleContent = new GUIContent("Character Wizard", EditorGUIUtility.IconContent("d_Prefab Icon").image); wnd.minSize = new Vector2(520, 600); } // ── 状态 ────────────────────────────────────────────────────────────── private int _activeTab = 0; private Button _btnPlayer, _btnEnemy, _btnBoss; // SO 状态缓存(避免每帧重查) private List<(string label, bool exists)> _playerSOStatus = new(); private List<(string label, bool exists)> _enemySOStatus = new(); private List<(string label, bool exists)> _bossSOStatus = new(); private double _lastRefreshTime; // 小怪类型选择 — 具体敌人类型 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"; // kept for legacy SkillSequenceSO queries if any private string _enemyId = "E001"; // kept for legacy status calls if any private string _playerId = "Player"; // SO 状态面板(按标签页缓存) private VisualElement _playerStatusPanel; private VisualElement _enemyStatusPanel; private VisualElement _bossStatusPanel; // ── 生命周期 ────────────────────────────────────────────────────────── public void CreateGUI() { if (Uss != null) rootVisualElement.styleSheets.Add(Uss); rootVisualElement.style.flexDirection = FlexDirection.Column; BuildTabBar(); BuildTabContents(); RefreshSOStatus(); SwitchTab(0); } private void OnFocus() => RefreshSOStatus(); // ── 标签栏 ──────────────────────────────────────────────────────────── private void BuildTabBar() { var bar = new VisualElement(); bar.AddToClassList("tab-bar"); _btnPlayer = MakeTabButton("玩家", () => SwitchTab(0)); _btnEnemy = MakeTabButton("小怪", () => SwitchTab(1)); _btnBoss = MakeTabButton("Boss", () => SwitchTab(2)); bar.Add(_btnPlayer); bar.Add(_btnEnemy); bar.Add(_btnBoss); rootVisualElement.Add(bar); } private Button MakeTabButton(string label, Action onClick) { var btn = new Button(onClick) { text = label }; btn.AddToClassList("tab-btn"); return btn; } private void SwitchTab(int idx) { _activeTab = idx; var tabs = rootVisualElement.Query(className: "tab-content").ToList(); for (int i = 0; i < tabs.Count; i++) tabs[i].style.display = (i == idx) ? DisplayStyle.Flex : DisplayStyle.None; _btnPlayer.EnableInClassList("tab-btn--active", idx == 0); _btnEnemy .EnableInClassList("tab-btn--active", idx == 1); _btnBoss .EnableInClassList("tab-btn--active", idx == 2); } // ── 标签页内容 ──────────────────────────────────────────────────────── private void BuildTabContents() { rootVisualElement.Add(BuildPlayerTab()); rootVisualElement.Add(BuildEnemyTab()); rootVisualElement.Add(BuildBossTab()); } // ════════════════════════════════════════════════════════════════════════ // 玩家标签页 // ════════════════════════════════════════════════════════════════════════ private VisualElement BuildPlayerTab() { var root = MakeTabContent(); root.Add(MakeSectionHeader("▶ SO 资产状态")); _playerStatusPanel = new VisualElement(); root.Add(_playerStatusPanel); root.Add(MakeSectionHeader("▶ SO 资产工厂")); root.Add(MakeHelpBox("在 Project 中创建下列 ScriptableObject 资产。若已存在则跳过。")); var idRow = MakeLabeledTextField("资产名称前缀", _playerId, v => _playerId = v); root.Add(idRow); var factory = MakeActionGroup(); factory.Add(MakeFactoryButton("PlayerStatsSO", () => CreatePlayerStat())); factory.Add(MakeFactoryButton("PlayerMovementConfigSO", () => CreateMovementConfig())); factory.Add(MakeFactoryButton("PlayerAnimationConfigSO", () => CreateAnimConfig())); factory.Add(MakeFactoryButton("FormConfigSO + 3 FormSO", () => CreateFormConfig())); factory.Add(MakeFactoryButton("WeaponSO × 3(形态)", () => CreateFormWeapons())); factory.Add(MakeFactoryButton("DamageSourceSO(连击×3)", () => CreatePlayerDamageSources())); factory.Add(MakeFactoryButton("ParryConfigSO", () => CreateParryConfig())); factory.Add(MakeFactoryButton("ShieldConfigSO", () => CreateShieldConfig())); factory.Add(MakeFactoryButton("EquipmentConfigSO", () => CreateEquipmentConfig())); factory.Add(MakeFactoryButton("CharmCatalogSO", () => CreateCharmCatalog())); root.Add(factory); var createAllBtn = new Button(CreateAllPlayerSOs) { text = "★ 一键创建全部 Player SO" }; createAllBtn.AddToClassList("wizard-create-all-btn"); root.Add(createAllBtn); root.Add(MakeSeparator()); root.Add(MakeSectionHeader("▶ 场景搭建")); root.Add(MakeHelpBox("在当前活动场景中放置玩家 GameObject(带完整组件树)。")); var sceneGroup = MakeActionGroup(); sceneGroup.Add(MakeSceneButton("放置玩家到场景", SceneObjectPlacerTool.PlacePlayer)); sceneGroup.Add(MakeSceneButton("指定所有 SO 到场景角色", AssignAllPlayerSOsToScene)); sceneGroup.Add(MakeSceneButton("放置地面平台", SceneObjectPlacerTool.PlaceGroundPlatform)); sceneGroup.Add(MakeSceneButton("放置存档点", SceneObjectPlacerTool.PlaceSavePoint)); root.Add(sceneGroup); root.Add(MakeSeparator()); root.Add(MakeSectionHeader("▶ 专项编辑器")); var jumpGroup = MakeActionGroup(); jumpGroup.Add(MakeJumpButton("Data Hub(武器/技能/形态)", DataHubWindow.Open)); jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu)); root.Add(jumpGroup); return root; } // ════════════════════════════════════════════════════════════════════════ // 小怪标签页 // ════════════════════════════════════════════════════════════════════════ private VisualElement BuildEnemyTab() { var root = MakeTabContent(); root.Add(MakeSectionHeader("▶ SO 资产状态")); _enemySOStatus.Clear(); _enemyStatusPanel = new VisualElement(); root.Add(_enemyStatusPanel); root.Add(MakeSectionHeader("▶ 敌人类型")); root.Add(MakeHelpBox("选择要创建的具体敌人类型,对应 SO 工厂与场景放置按钮会自动切换。")); 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(typeRow); RefreshEnemyTabContent(_enemyContentArea); RefreshSOStatus(); }) { text = $"{id} {name}" }; btn.name = $"enemy-type-{i}"; btn.EnableInClassList("type-btn--active", i == _enemyTypeIndex); typeRow.Add(btn); } root.Add(typeRow); _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)); 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 标签页(嘲风专属) // ════════════════════════════════════════════════════════════════════════ private VisualElement BuildBossTab() { var root = MakeTabContent(); root.Add(MakeSectionHeader("▶ SO 资产状态")); _bossStatusPanel = new VisualElement(); root.Add(_bossStatusPanel); 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("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("放置嘲风到场景并绑定 SO", SceneObjectPlacerTool.PlaceChaoFeng)); root.Add(sceneGroup); root.Add(MakeSeparator()); root.Add(MakeSectionHeader("▶ 专项编辑器")); var jumpGroup = MakeActionGroup(); jumpGroup.Add(MakeJumpButton("Boss 技能序列查看器", BossSkillSequenceWindow.OpenWindow)); jumpGroup.Add(MakeJumpButton("Data Hub(Boss技能)", DataHubWindow.Open)); jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu)); root.Add(jumpGroup); return root; } // ── SO 资产工厂:玩家 ──────────────────────────────────────────────── private void CreatePlayerStat() { var asset = EditorScaffoldUtils.CreateSOAsset( $"{DataRoot}/Player", "PLY_PlayerStats"); if (asset != null) RefreshSOStatus(); } private void CreateMovementConfig() { var asset = EditorScaffoldUtils.CreateSOAsset( $"{DataRoot}/Player", "PLY_PlayerMovementConfig"); if (asset != null) RefreshSOStatus(); } private void CreateAnimConfig() { var asset = EditorScaffoldUtils.CreateSOAsset( $"{DataRoot}/Player", "PLY_PlayerAnimationConfig"); if (asset != null) RefreshSOStatus(); } private void CreateFormConfig() { string configDir = $"{DataRoot}/Player"; string formsDir = $"{DataRoot}/Player/Forms"; EditorScaffoldUtils.EnsureFolder(formsDir); var cfg = EditorScaffoldUtils.CreateSOAsset(configDir, "PLY_FormConfig"); if (cfg == null) cfg = AssetDatabase.LoadAssetAtPath($"{configDir}/PLY_FormConfig.asset"); var formTypes = new[] { ("TianHun", FormType.TianHun, "天魂"), ("DiHun", FormType.DiHun, "地魂"), ("MingHun", FormType.MingHun, "命魂") }; var forms = new List(); foreach (var (id, ftype, dname) in formTypes) { string path = $"{formsDir}/PLY_Form_{id}.asset"; var form = AssetDatabase.LoadAssetAtPath(path); if (form == null) { form = ScriptableObject.CreateInstance(); form.formId = $"Form_{id}"; form.displayName = dname; form.formType = ftype; AssetDatabase.CreateAsset(form, path); } forms.Add(form); } if (cfg != null && (cfg.forms == null || cfg.forms.Length == 0)) { cfg.forms = forms.ToArray(); EditorUtility.SetDirty(cfg); } AssetDatabase.SaveAssets(); EditorGUIUtility.PingObject(cfg); RefreshSOStatus(); } private void CreateFormWeapons() { string dir = $"{DataRoot}/Combat/Weapons"; foreach (var id in new[] { "TianHun", "DiHun", "MingHun" }) EditorScaffoldUtils.CreateSOAsset(dir, $"WPN_{id}"); RefreshSOStatus(); } private void CreatePlayerDamageSources() { string dir = $"{DataRoot}/Combat/DamageSources"; foreach (var label in new[] { "Attack1", "Attack2", "Attack3" }) EditorScaffoldUtils.CreateSOAsset(dir, $"CMB_Player_{label}"); RefreshSOStatus(); } // ── SO 资产工厂:玩家(Config 类)──────────────────────────────────────── private void CreateParryConfig() { var asset = EditorScaffoldUtils.CreateSOAsset( $"{DataRoot}/Player", "PLY_ParryConfig"); if (asset != null) RefreshSOStatus(); } private void CreateShieldConfig() { var asset = EditorScaffoldUtils.CreateSOAsset( $"{DataRoot}/Player", "PLY_ShieldConfig"); if (asset != null) RefreshSOStatus(); } private void CreateEquipmentConfig() { var asset = EditorScaffoldUtils.CreateSOAsset( $"{DataRoot}/Player", "PLY_EquipmentConfig"); if (asset != null) RefreshSOStatus(); } private void CreateCharmCatalog() { var asset = EditorScaffoldUtils.CreateSOAsset( $"{DataRoot}/Progression/Charms", "CHM_CharmCatalog"); if (asset != null) RefreshSOStatus(); } /// /// 一键创建全部 Player 所需 SO(已存在则跳过)。 /// 完成后提示用户点击"指定所有 SO 到场景角色"完成绑定。 /// private void CreateAllPlayerSOs() { CreatePlayerStat(); CreateMovementConfig(); CreateAnimConfig(); CreateFormConfig(); CreateFormWeapons(); CreatePlayerDamageSources(); CreateParryConfig(); CreateShieldConfig(); CreateEquipmentConfig(); CreateCharmCatalog(); AssetDatabase.SaveAssets(); EditorUtility.DisplayDialog("创建完成", "全部 Player SO 已创建(已存在的跳过)。\n" + "请在场景中放置角色后,点击「▣ 指定所有 SO 到场景角色」完成绑定。", "确定"); } /// /// 查找场景中的 PlayerController,将项目中已存在的配置 SO 全部指定给对应组件字段。 /// 使用 SerializedObject 赋值,自动标记 dirty 并保存。 /// private void AssignAllPlayerSOsToScene() { var pc = UnityEngine.Object.FindObjectOfType(); if (pc == null) { EditorUtility.DisplayDialog("未找到角色", "场景中没有 PlayerController。\n请先使用「▣ 放置玩家到场景」。", "确定"); return; } var plyGo = pc.gameObject; int count = 0; // 通过 SerializedObject 写入 SerializeField(支持撤销) var missing = new System.Collections.Generic.List(); void TryAssign(Component comp, string field) where T : ScriptableObject { if (comp == null) return; var so = EditorScaffoldUtils.FindAllAssetsOfType().FirstOrDefault(); if (so == null) { missing.Add($"{typeof(T).Name}({comp.GetType().Name}.{field})"); return; } var sObj = new SerializedObject(comp); var prop = sObj.FindProperty(field); if (prop == null) return; prop.objectReferenceValue = so; sObj.ApplyModifiedProperties(); count++; } var stats = plyGo.GetComponent(); var movement = plyGo.GetComponent(); var form = plyGo.GetComponent(); var parry = plyGo.GetComponent(); var shield = plyGo.GetComponent(); var equip = plyGo.GetComponent(); var wall = plyGo.GetComponent(); // PlayerStats TryAssign (stats, "_config"); // PlayerMovement TryAssign (movement, "_config"); // PlayerController(多个字段) TryAssign (pc, "_movementConfig"); TryAssign (pc, "_animConfig"); TryAssign (pc, "_inputReader"); TryAssign (pc, "_formConfig"); // FormController TryAssign (form, "_config"); // ParrySystem TryAssign (parry, "_config"); // ShieldComponent TryAssign (shield, "_config"); // EquipmentManager TryAssign (equip, "_config"); TryAssign (equip, "_charmCatalog"); // PlayerWallDetector(复用移动配置) TryAssign (wall, "_config"); EditorUtility.SetDirty(plyGo); RefreshSOStatus(); string msg = $"已将 {count} 个 SO 引用指定到场景角色 [{plyGo.name}]。"; if (missing.Count > 0) msg += $"\n\n★ 以下 SO 尚未创建,请先点击工厂按钮创建后再次指定:\n • " + string.Join("\n • ", missing); else msg += "\n全部 SO 绑定完成,可在 Inspector 中确认各组件字段。"; EditorUtility.DisplayDialog("指定完成", msg, "确定"); } // ── SO 资产工厂:小怪(按类型) ─────────────────────────────────────────── /// 返回指定敌人类型的 (abilityFileName, abilityId) 定义列表。 private static (string ablName, string ablId)[] GetEnemyAbilityDefs(string enemyId) => enemyId switch { "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(dir, $"ENM_{id}_Stats"); } private static void CreateEnemyAnimConfigSO(string id) { string dir = $"Assets/_Game/Data/Enemies/{id}"; EditorScaffoldUtils.CreateSOAsset(dir, $"ENM_{id}_AnimConfig"); } private static void CreateEnemyAbilitySO(string enemyId, string ablName, string ablId) { string dir = $"Assets/_Game/Data/Enemies/{enemyId}/Abilities"; string name = $"ABL_{enemyId}_{ablName}"; var so = EditorScaffoldUtils.CreateSOAsset(dir, name); // Set abilityId on newly-created SO (skip if already existed = null returned) if (so != null) { 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(dir, "ENM_ChaoFeng_Stats"); } private static void CreateChaoFengAnimConfigSO() { string dir = "Assets/_Game/Data/Enemies/ChaoFeng"; EditorScaffoldUtils.CreateSOAsset(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(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() { _lastRefreshTime = EditorApplication.timeSinceStartup; BuildPlayerStatus(); BuildEnemyStatus(); BuildBossStatus(); } private void BuildPlayerStatus() { if (_playerStatusPanel == null) return; _playerStatusPanel.Clear(); var checks = new (string label, UnityEngine.Object asset)[] { ("PlayerStatsSO", FindFirst()), ("PlayerMovementConfigSO", FindFirst()), ("PlayerAnimationConfigSO", FindFirst()), ("FormConfigSO", FindFirst()), ("FormSO(天魂)", FindFormOfType(FormType.TianHun)), ("FormSO(地魂)", FindFormOfType(FormType.DiHun)), ("FormSO(命魂)", FindFormOfType(FormType.MingHun)), ("WeaponSO(≥3)", FindFirstIfCount(3)), ("ParryConfigSO", FindFirst()), ("ShieldConfigSO", FindFirst()), ("EquipmentConfigSO", FindFirst()), ("CharmCatalogSO", FindFirst()), }; _playerStatusPanel.Add(MakeStatusGrid(checks)); } private void BuildEnemyStatus() { if (_enemyStatusPanel == null) return; _enemyStatusPanel.Clear(); var (id, name) = EnemyTypes[_enemyTypeIndex]; string dir = $"{DataRoot}/Enemies/{id}"; string ablDir = $"{dir}/Abilities"; var items = new List<(string label, UnityEngine.Object asset)> { ($"ENM_{id}_Stats", FindAtPath($"{dir}/ENM_{id}_Stats.asset")), ($"ENM_{id}_AnimConfig",FindAtPath($"{dir}/ENM_{id}_AnimConfig.asset")), }; foreach (var (ablName, _) in GetEnemyAbilityDefs(id)) items.Add(($"ABL_{id}_{ablName}", FindAtPath($"{ablDir}/ABL_{id}_{ablName}.asset"))); _enemyStatusPanel.Add(MakeStatusGrid(items.ToArray())); } private void BuildBossStatus() { if (_bossStatusPanel == null) return; _bossStatusPanel.Clear(); 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)[] { ("ENM_ChaoFeng_Stats", FindAtPath($"{dir}/ENM_ChaoFeng_Stats.asset")), ("ENM_ChaoFeng_AnimConfig",FindAtPath($"{dir}/ENM_ChaoFeng_AnimConfig.asset")), ("ABL_ChaoFeng_Idle", FindAtPath($"{ablDir}/ABL_ChaoFeng_Idle.asset")), ("ABL_ChaoFeng_Slam", FindAtPath($"{ablDir}/ABL_ChaoFeng_Slam.asset")), ("ABL_ChaoFeng_Sweep", FindAtPath($"{ablDir}/ABL_ChaoFeng_Sweep.asset")), ("ABL_ChaoFeng_WindBlade", FindAtPath($"{ablDir}/ABL_ChaoFeng_WindBlade.asset")), ("ABL_ChaoFeng_Summon", FindAtPath($"{ablDir}/ABL_ChaoFeng_Summon.asset")), }; _bossStatusPanel.Add(MakeStatusGrid(checks)); } // ── 辅助:状态格 ───────────────────────────────────────────────────── private static VisualElement MakeStatusGrid((string label, UnityEngine.Object asset)[] items) { var grid = new VisualElement(); grid.style.flexDirection = FlexDirection.Row; grid.style.flexWrap = Wrap.Wrap; grid.style.marginBottom = 6; foreach (var (label, asset) in items) { bool exists = asset != null; VisualElement chip; if (exists) { var captured = asset; var btn = new Button(() => { EditorGUIUtility.PingObject(captured); Selection.activeObject = captured; }) { text = $"✔ {label}" }; btn.AddToClassList("status-chip--ok"); chip = btn; } else { var lbl = new Label($"✘ {label}"); lbl.AddToClassList("status-chip--missing"); chip = lbl; } grid.Add(chip); } return grid; } // ── 辅助:UI 组件构建 ───────────────────────────────────────────────── private static VisualElement MakeTabContent() { var root = new ScrollView(ScrollViewMode.Vertical); root.AddToClassList("tab-content"); root.style.flexGrow = 1; root.contentContainer.style.paddingLeft = 8; root.contentContainer.style.paddingRight = 8; root.contentContainer.style.paddingTop = 8; root.contentContainer.style.paddingBottom = 8; return root; } private static Label MakeSectionHeader(string text) { var lbl = new Label(text); lbl.AddToClassList("section-header"); return lbl; } private static VisualElement MakeActionGroup() { var group = new VisualElement(); group.AddToClassList("action-buttons"); return group; } private static Button MakeFactoryButton(string label, Action onClick) { var btn = new Button(onClick) { text = $"+ {label}" }; btn.AddToClassList("wizard-factory-btn"); return btn; } private static Button MakeSceneButton(string label, Action onClick) { var btn = new Button(onClick) { text = $"▣ {label}" }; btn.AddToClassList("wizard-scene-btn"); return btn; } private static Button MakeJumpButton(string label, Action onClick) { var btn = new Button(onClick) { text = $"⇒ {label}" }; btn.AddToClassList("wizard-jump-btn"); return btn; } private static HelpBox MakeHelpBox(string text) { return new HelpBox(text, HelpBoxMessageType.Info); } private static VisualElement MakeSeparator() { var sep = new VisualElement(); sep.style.height = 1; sep.style.backgroundColor = new Color(0.5f, 0.5f, 0.5f, 0.25f); sep.style.marginTop = 8; sep.style.marginBottom = 8; return sep; } private static VisualElement MakeLabeledTextField(string label, string value, Action onChange) { var row = new VisualElement(); row.style.flexDirection = FlexDirection.Row; row.style.marginBottom = 4; row.style.alignItems = Align.Center; var lbl = new Label(label) { style = { minWidth = 110, marginRight = 6 } }; var field = new TextField { value = value, style = { flexGrow = 1 } }; field.RegisterValueChangedCallback(e => onChange(e.newValue)); row.Add(lbl); row.Add(field); return row; } private void RefreshEnemyTypeButtons(VisualElement typeRow) { for (int i = 0; i < EnemyTypes.Length; i++) { var btn = typeRow.Q