Files
zeling_v2/Assets/_Game/Scripts/Editor/Character/CharacterWizardWindow.cs
Joywayer bcd8b0e90b feat: Update enemy AI and movement systems
- Enhanced Physics2D layer collision report with new interactions between Player and Enemy layers.
- Refactored BD_InvestigateLastKnown to streamline animation handling and improve readability.
- Simplified BD_MaintainCombatDistance by consolidating movement stop logic.
- Updated BD_MoveToPlayer to set AI phase on start.
- Improved BD_Patrol logic with better handling of stuck states and path failures.
- Enhanced BD_PatrolWaypoints to manage stuck conditions and retry logic more effectively.
- Refined BD_ReturnToHome to remove unnecessary animation calls.
- Updated BD_WalkRandom to ensure AI phase is set correctly on start.
- Improved EnemyAbilityBase to delegate target facing to the movement system.
- Enhanced EnemyBase with new movement methods for better control.
- Refactored EnemyMovement to introduce a new input system for handling movement and facing.
- Added EnemyMoveInput struct to encapsulate movement intentions.
- Updated Physics2DSettings to reflect new layer collision matrix.
- Introduced RTK CLI instructions for optimized command usage.
2026-05-29 17:01:59 +08:00

883 lines
40 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// 角色创建向导W-01— 统一入口窗口。
/// 技术UI Toolkit三标签页玩家 / 小怪 / Boss
/// 菜单BaseGames / Tools / Character Wizard
///
/// 各标签页均提供:
/// ① 当前 SO 资产状态速览(绿色=已存在 / 橙色=缺失)
/// ② SO 资产工厂(一键创建所需的所有 ScriptableObject
/// ③ 场景搭建快捷按钮(调用 SceneObjectPlacerTool.PlaceXxx
/// ④ 跳转到对应专项编辑器窗口
/// </summary>
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<StyleSheet>(UssPath));
[MenuItem("BaseGames/Data/Character Wizard", priority = 1)]
public static void Open()
{
var wnd = GetWindow<CharacterWizardWindow>();
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<VisualElement>(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 HubBoss技能", DataHubWindow.Open));
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
root.Add(jumpGroup);
return root;
}
// ── SO 资产工厂:玩家 ────────────────────────────────────────────────
private void CreatePlayerStat()
{
var asset = EditorScaffoldUtils.CreateSOAsset<PlayerStatsSO>(
$"{DataRoot}/Player", "PLY_PlayerStats");
if (asset != null) RefreshSOStatus();
}
private void CreateMovementConfig()
{
var asset = EditorScaffoldUtils.CreateSOAsset<PlayerMovementConfigSO>(
$"{DataRoot}/Player", "PLY_PlayerMovementConfig");
if (asset != null) RefreshSOStatus();
}
private void CreateAnimConfig()
{
var asset = EditorScaffoldUtils.CreateSOAsset<PlayerAnimationConfigSO>(
$"{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<FormConfigSO>(configDir, "PLY_FormConfig");
if (cfg == null) cfg = AssetDatabase.LoadAssetAtPath<FormConfigSO>($"{configDir}/PLY_FormConfig.asset");
var formTypes = new[] { ("TianHun", FormType.TianHun, "天魂"), ("DiHun", FormType.DiHun, "地魂"), ("MingHun", FormType.MingHun, "命魂") };
var forms = new List<FormSO>();
foreach (var (id, ftype, dname) in formTypes)
{
string path = $"{formsDir}/PLY_Form_{id}.asset";
var form = AssetDatabase.LoadAssetAtPath<FormSO>(path);
if (form == null)
{
form = ScriptableObject.CreateInstance<FormSO>();
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<WeaponSO>(dir, $"WPN_{id}");
RefreshSOStatus();
}
private void CreatePlayerDamageSources()
{
string dir = $"{DataRoot}/Combat/DamageSources";
foreach (var label in new[] { "Attack1", "Attack2", "Attack3" })
EditorScaffoldUtils.CreateSOAsset<DamageSourceSO>(dir, $"CMB_Player_{label}");
RefreshSOStatus();
}
// ── SO 资产工厂玩家Config 类)────────────────────────────────────────
private void CreateParryConfig()
{
var asset = EditorScaffoldUtils.CreateSOAsset<ParryConfigSO>(
$"{DataRoot}/Player", "PLY_ParryConfig");
if (asset != null) RefreshSOStatus();
}
private void CreateShieldConfig()
{
var asset = EditorScaffoldUtils.CreateSOAsset<ShieldConfigSO>(
$"{DataRoot}/Player", "PLY_ShieldConfig");
if (asset != null) RefreshSOStatus();
}
private void CreateEquipmentConfig()
{
var asset = EditorScaffoldUtils.CreateSOAsset<EquipmentConfigSO>(
$"{DataRoot}/Player", "PLY_EquipmentConfig");
if (asset != null) RefreshSOStatus();
}
private void CreateCharmCatalog()
{
var asset = EditorScaffoldUtils.CreateSOAsset<CharmCatalogSO>(
$"{DataRoot}/Progression/Charms", "CHM_CharmCatalog");
if (asset != null) RefreshSOStatus();
}
/// <summary>
/// 一键创建全部 Player 所需 SO已存在则跳过
/// 完成后提示用户点击"指定所有 SO 到场景角色"完成绑定。
/// </summary>
private void CreateAllPlayerSOs()
{
CreatePlayerStat();
CreateMovementConfig();
CreateAnimConfig();
CreateFormConfig();
CreateFormWeapons();
CreatePlayerDamageSources();
CreateParryConfig();
CreateShieldConfig();
CreateEquipmentConfig();
CreateCharmCatalog();
AssetDatabase.SaveAssets();
EditorUtility.DisplayDialog("创建完成",
"全部 Player SO 已创建(已存在的跳过)。\n" +
"请在场景中放置角色后,点击「▣ 指定所有 SO 到场景角色」完成绑定。",
"确定");
}
/// <summary>
/// 查找场景中的 PlayerController将项目中已存在的配置 SO 全部指定给对应组件字段。
/// 使用 SerializedObject 赋值,自动标记 dirty 并保存。
/// </summary>
private void AssignAllPlayerSOsToScene()
{
var pc = UnityEngine.Object.FindObjectOfType<PlayerController>();
if (pc == null)
{
EditorUtility.DisplayDialog("未找到角色",
"场景中没有 PlayerController。\n请先使用「▣ 放置玩家到场景」。", "确定");
return;
}
var plyGo = pc.gameObject;
int count = 0;
// 通过 SerializedObject 写入 SerializeField支持撤销
var missing = new System.Collections.Generic.List<string>();
void TryAssign<T>(Component comp, string field) where T : ScriptableObject
{
if (comp == null) return;
var so = EditorScaffoldUtils.FindAllAssetsOfType<T>().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<PlayerStats>();
var movement = plyGo.GetComponent<PlayerMovement>();
var form = plyGo.GetComponent<FormController>();
var parry = plyGo.GetComponent<ParrySystem>();
var shield = plyGo.GetComponent<ShieldComponent>();
var equip = plyGo.GetComponent<EquipmentManager>();
var wall = plyGo.GetComponent<PlayerWallDetector>();
// PlayerStats
TryAssign<PlayerStatsSO> (stats, "_config");
// PlayerMovement
TryAssign<PlayerMovementConfigSO> (movement, "_config");
// PlayerController多个字段
TryAssign<PlayerMovementConfigSO> (pc, "_movementConfig");
TryAssign<PlayerAnimationConfigSO> (pc, "_animConfig");
TryAssign<InputReaderSO> (pc, "_inputReader");
TryAssign<FormConfigSO> (pc, "_formConfig");
// FormController
TryAssign<FormConfigSO> (form, "_config");
// ParrySystem
TryAssign<ParryConfigSO> (parry, "_config");
// ShieldComponent
TryAssign<ShieldConfigSO> (shield, "_config");
// EquipmentManager
TryAssign<EquipmentConfigSO> (equip, "_config");
TryAssign<CharmCatalogSO> (equip, "_charmCatalog");
// PlayerWallDetector复用移动配置
TryAssign<PlayerMovementConfigSO> (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 资产工厂:小怪(按类型) ───────────────────────────────────────────
/// <summary>返回指定敌人类型的 (abilityFileName, abilityId) 定义列表。</summary>
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<EnemyStatsSO>(dir, $"ENM_{id}_Stats");
}
private static void CreateEnemyAnimConfigSO(string id)
{
string dir = $"Assets/_Game/Data/Enemies/{id}";
EditorScaffoldUtils.CreateSOAsset<EnemyAnimationConfigSO>(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<EnemyAbilitySO>(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:
Debug.LogError($"[CharacterWizardWindow] 未注册的敌人 id '{id}',请在 SceneObjectPlacerTool 中实现对应 PlaceE...() 方法并注册。");
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()
{
_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<PlayerStatsSO>()),
("PlayerMovementConfigSO", FindFirst<PlayerMovementConfigSO>()),
("PlayerAnimationConfigSO", FindFirst<PlayerAnimationConfigSO>()),
("FormConfigSO", FindFirst<FormConfigSO>()),
("FormSO天魂", FindFormOfType(FormType.TianHun)),
("FormSO地魂", FindFormOfType(FormType.DiHun)),
("FormSO命魂", FindFormOfType(FormType.MingHun)),
("WeaponSO≥3", FindFirstIfCount<WeaponSO>(3)),
("ParryConfigSO", FindFirst<ParryConfigSO>()),
("ShieldConfigSO", FindFirst<ShieldConfigSO>()),
("EquipmentConfigSO", FindFirst<EquipmentConfigSO>()),
("CharmCatalogSO", FindFirst<CharmCatalogSO>()),
};
_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<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()
{
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<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));
}
// ── 辅助:状态格 ─────────────────────────────────────────────────────
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<string> 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<Button>($"enemy-type-{i}");
btn?.EnableInClassList("type-btn--active", i == _enemyTypeIndex);
}
}
// ── 资产查找辅助 ──────────────────────────────────────────
private static T FindFirst<T>() where T : ScriptableObject
=> EditorScaffoldUtils.FindAllAssetsOfType<T>().FirstOrDefault();
/// <summary>返回第一个,仅当总数达到 minCount 时才认为满足。</summary>
private static T FindFirstIfCount<T>(int minCount) where T : ScriptableObject
{
var list = EditorScaffoldUtils.FindAllAssetsOfType<T>();
return list.Count >= minCount ? list[0] : null;
}
private static T FindAtPath<T>(string path) where T : UnityEngine.Object
=> AssetDatabase.LoadAssetAtPath<T>(path);
private static FormSO FindFormOfType(FormType type)
=> EditorScaffoldUtils.FindAllAssetsOfType<FormSO>()
.FirstOrDefault(f => f.formType == type);
}
}