Files
zeling_v2/Assets/_Game/Scripts/Editor/CharacterWizardWindow.cs
2026-05-19 11:50:21 +08:00

836 lines
37 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.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/Tools/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[] EnemyTypeLabels = { "普通(近战)", "远程", "飞行" };
// Boss 命名字段
private string _bossId = "NewBoss";
private string _enemyId = "NewEnemy";
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.style.marginTop = 6;
createAllBtn.style.height = 26;
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("武器编辑器", () => Combat.WeaponEditorWindow.Open()));
jumpGroup.Add(MakeJumpButton("技能编辑器", () => Skills.SkillEditorWindow.Open()));
jumpGroup.Add(MakeJumpButton("形态编辑器", () => FormEditorWindow.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("▶ 敌人类型选择"));
var typeRow = new VisualElement { style = { flexDirection = FlexDirection.Row, marginBottom = 4 } };
for (int i = 0; i < EnemyTypeLabels.Length; i++)
{
int captured = i;
var btn = new Button(() =>
{
_enemyTypeIndex = captured;
// 高亮激活按钮(简单刷新所有同类按钮样式)
RefreshEnemyTypeButtons(root);
})
{ text = EnemyTypeLabels[i] };
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);
root.Add(MakeSeparator());
root.Add(MakeSectionHeader("▶ 专项编辑器"));
var jumpGroup = MakeActionGroup();
jumpGroup.Add(MakeJumpButton("敌人数据管理", () => Enemies.EnemyDataWindow.Open()));
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
root.Add(jumpGroup);
return root;
}
// ════════════════════════════════════════════════════════════════════════
// Boss 标签页
// ════════════════════════════════════════════════════════════════════════
private VisualElement BuildBossTab()
{
var root = MakeTabContent();
root.Add(MakeSectionHeader("▶ SO 资产状态"));
_bossStatusPanel = new VisualElement();
root.Add(_bossStatusPanel);
root.Add(MakeSectionHeader("▶ SO 资产工厂"));
root.Add(MakeHelpBox("每个 Boss 独立目录Assets/_Game/Data/Boss/<BossId>/"));
var idRow = MakeLabeledTextField("Boss ID", _bossId, v => _bossId = v);
root.Add(idRow);
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()));
root.Add(factory);
root.Add(MakeSeparator());
root.Add(MakeSectionHeader("▶ 场景搭建"));
var sceneGroup = MakeActionGroup();
sceneGroup.Add(MakeSceneButton("放置 Boss 到场景", SceneObjectPlacerTool.PlaceBossEnemy));
root.Add(sceneGroup);
root.Add(MakeSeparator());
root.Add(MakeSectionHeader("▶ 专项编辑器"));
var jumpGroup = MakeActionGroup();
jumpGroup.Add(MakeJumpButton("Boss 技能序列查看器", BossSkillSequenceWindow.OpenWindow));
jumpGroup.Add(MakeJumpButton("敌人数据管理", () => Enemies.EnemyDataWindow.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", "PLY_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 资产工厂:小怪 ────────────────────────────────────────────────
private void CreateEnemyStat()
{
string dir = $"{DataRoot}/Enemies/{_enemyId}";
var asset = EditorScaffoldUtils.CreateSOAsset<EnemyStatsSO>(dir, $"ENM_{_enemyId}_Stats");
if (asset != null) RefreshSOStatus();
}
private void CreateLootTable()
{
string dir = $"{DataRoot}/Enemies/{_enemyId}";
var asset = EditorScaffoldUtils.CreateSOAsset<LootTableSO>(dir, $"ENM_{_enemyId}_Loot");
if (asset != null) RefreshSOStatus();
}
private void CreateEnemyAttackPatterns()
{
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}/Boss/{_bossId}";
EditorScaffoldUtils.CreateSOAsset<EnemyStatsSO>(dir, $"ENM_{_bossId}_Stats");
RefreshSOStatus();
}
private void CreateBossLoot()
{
string dir = $"{DataRoot}/Boss/{_bossId}";
EditorScaffoldUtils.CreateSOAsset<LootTableSO>(dir, $"ENM_{_bossId}_Loot");
RefreshSOStatus();
}
private void CreateBossAttackPatterns()
{
string dir = $"{DataRoot}/Boss/{_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}/Boss/{_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}/Boss/{_bossId}/Skills";
EditorScaffoldUtils.CreateSOAsset<SkillSequenceSO>(dir, $"SKL_{_bossId}_Phase{phase}_Sequence");
RefreshSOStatus();
}
private void CreateBossDamageSources()
{
string dir = $"{DataRoot}/Boss/{_bossId}/DamageSources";
foreach (var label in new[] { "Slam", "Sweep", "Projectile" })
EditorScaffoldUtils.CreateSOAsset<DamageSourceSO>(dir, $"ENM_{_bossId}_DS_{label}");
RefreshSOStatus();
}
// ── 场景搭建 ──────────────────────────────────────────────────────────
private void PlaceSelectedEnemyType()
{
switch (_enemyTypeIndex)
{
case 0: SceneObjectPlacerTool.PlaceEnemy(); break;
case 1: SceneObjectPlacerTool.PlaceEnemy(); break; // 复用,类型通过 SO 区分
case 2: SceneObjectPlacerTool.PlaceEnemy(); break;
}
}
// ── 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();
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")),
};
_enemyStatusPanel.Add(MakeStatusGrid(checks));
}
private void BuildBossStatus()
{
if (_bossStatusPanel == null) return;
_bossStatusPanel.Clear();
string dir = $"{DataRoot}/Boss/{_bossId}";
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")),
};
_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}" };
chip = btn;
}
else
{
chip = new Label($"✘ {label}");
}
chip.style.marginRight = 6;
chip.style.marginBottom = 4;
chip.style.paddingLeft = 6;
chip.style.paddingRight = 6;
chip.style.paddingTop = 2;
chip.style.paddingBottom = 2;
chip.style.borderTopLeftRadius = 4;
chip.style.borderTopRightRadius = 4;
chip.style.borderBottomLeftRadius = 4;
chip.style.borderBottomRightRadius = 4;
if (exists)
{
chip.style.backgroundColor = new Color(0.18f, 0.55f, 0.22f, 0.85f);
chip.style.color = new Color(0.85f, 1.00f, 0.85f);
}
else
{
chip.style.backgroundColor = new Color(0.65f, 0.35f, 0.05f, 0.85f);
chip.style.color = new Color(1.00f, 0.90f, 0.70f);
}
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.3f, 0.3f, 0.3f, 0.6f);
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 tabRoot)
{
for (int i = 0; i < EnemyTypeLabels.Length; i++)
{
var btn = tabRoot.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);
}
}