删 5 个空 BossSkillSO,用向导重建 6 个 EnemyAbilitySO(含入场演出)。 预制体经放置工具重新产出:EnemyAiBrain(_definitionId=ChaoFeng) + BossPhaseAbilityGate + 6 个能力组件,不再有 BossSkillExecutor。 TestRoomA 加开战触发区。三件自检 0 error,EditMode 260/260。 首次真正跑通两条脚手架,暴露并修掉三个它们的缺陷: 1. 放置工具漏挂 EnemyLocomotion(PlaceChaoFeng 及其余 7 个敌人放置入口)。 EnemyBase.Awake 找不到它即报错且 Locomotion 为 null, AiStateFragments.ApplyLocomotion 直接 NRE、AiRuntime 构造失败—— Boss 建不出决策层,完全不动。已在所有敌人放置入口补齐。 2. 向导把空中阶段的 wind_stone 建成 requiresGrounded=true(SO 默认值)。 Boss 浮空后 IsGrounded 恒 false,选招器把它滤掉,空中阶段站着不打。 能力定义表加 grounded 列,阶段 1 的招显式置 false。 3. BossPhaseAbilityGate.ApplyPhase 只在 EnterPhase 与 OnSpawn 调用, 直接摆场景里的 Boss 两条路径都不走,开局后续阶段的招仍在候选池里。 BossBase.Start 补一次初始阶段应用。 另:Phase1_Tornado_HitBox 确认零消费者(龙卷判定随弹体走), 从放置工具删除其创建与伤害源绑定; EnemyBrainContext.UseAbility 对解析不到的能力 id 改为 LogError, 不再静默返回 false(CLAUDE.md 第 6 条); 新增 PlaceBossFightTrigger 脚手架(此前无对应创建入口,属裸建)。 播放模式实测状态序列(TestRoomA,玩家走进触发区): Wait →(Engaged) Intro → Ground⇄GroundAttack →(hp<50%) PhaseTransition → Air⇄AirAttack,浮空生效,阶段门按阶段换池,全程 0 报错。 动画 Clip 未接入,PlayClipAbility 空 clip 立即完成,故只验证了状态流转, 判定时序待美术接入后再校。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
989 lines
47 KiB
C#
989 lines
47 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using UnityEditor;
|
||
|
||
using UnityEngine;
|
||
using UnityEngine.UIElements;
|
||
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;
|
||
using BaseGames.Editor.Combat;
|
||
using BaseGames.Editor.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 SceneObjectPlacerTool.EnemyBodyColliderType _enemyBodyCollider = SceneObjectPlacerTool.EnemyBodyColliderType.Box;
|
||
private Sprite _defaultSprite; // 敌人/Boss 默认外观 Sprite:碰撞体尺寸依据(可留空)
|
||
private static readonly (string id, string name)[] EnemyTypes =
|
||
{
|
||
("E001", "草蛭"),
|
||
("E002", "簧蛭"),
|
||
("E003", "幼蛭"),
|
||
("E004", "蛭母"),
|
||
("E005", "肥蛭"),
|
||
("E006", "讙"),
|
||
};
|
||
|
||
// 动态内容区(类型切换时重建)
|
||
private VisualElement _enemyContentArea;
|
||
|
||
// 命名字段
|
||
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("武器 HitBox 向导", WeaponHitBoxWizard.Open));
|
||
jumpGroup.Add(MakeJumpButton("技能 HitBox 向导", SkillHitBoxWizard.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("武器 HitBox 向导", WeaponHitBoxWizard.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, soType) in GetEnemyAbilityDefs(id))
|
||
{
|
||
string capturedName = ablName;
|
||
string capturedId = ablId;
|
||
System.Type capturedType = soType;
|
||
factory.Add(MakeFactoryButton($"ABL_{id}_{capturedName}.asset",
|
||
() => { CreateEnemyAbilitySO(id, capturedName, capturedId, capturedType); 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("在当前活动场景中放置完整组件树(含 Visual 子节点对齐 Collider)并自动绑定已有 SO。"));
|
||
|
||
var colliderField = new UnityEngine.UIElements.EnumField("主体碰撞器类型", _enemyBodyCollider);
|
||
colliderField.RegisterValueChangedCallback(evt => _enemyBodyCollider = (SceneObjectPlacerTool.EnemyBodyColliderType)evt.newValue);
|
||
container.Add(colliderField);
|
||
|
||
var spriteField = new UnityEditor.UIElements.ObjectField("默认外观 Sprite(碰撞体尺寸依据,可留空)")
|
||
{ objectType = typeof(Sprite), allowSceneObjects = false, value = _defaultSprite };
|
||
spriteField.RegisterValueChangedCallback(evt => _defaultSprite = evt.newValue as Sprite);
|
||
container.Add(spriteField);
|
||
|
||
var sceneGroup = MakeActionGroup();
|
||
string sceneLabel = $"放置 {id} {name} 到场景";
|
||
sceneGroup.Add(MakeSceneButton(sceneLabel, () => PlaceSpecificEnemy(id, _enemyBodyCollider, _defaultSprite)));
|
||
sceneGroup.Add(MakeSceneButton("迁移选中对象 Visual 节点", MigrateSelectedEnemyVisualNode));
|
||
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(); }));
|
||
foreach (var (abName, abId, soType, r, cd, w, ph, g) in ChaoFengAbilityDefs)
|
||
{
|
||
string cName = abName; string cId = abId; System.Type cType = soType;
|
||
float cR = r, cCd = cd, cW = w; int cPh = ph; bool cG = g;
|
||
factory.Add(MakeFactoryButton($"ABL_ChaoFeng_{cName}.asset",
|
||
() => { CreateChaoFengAbilitySO(cName, cId, cType, cR, cCd, cW, cPh, cG); 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 + 浮空控制器 + 击倒计数 + " +
|
||
"EnemyAiBrain + BossPhaseAbilityGate + 6 个能力组件 + " +
|
||
"Phase1 HitBox × 4 + 炮口 × 3)。"));
|
||
|
||
var bossSpriteField = new UnityEditor.UIElements.ObjectField("默认外观 Sprite(碰撞体尺寸依据,可留空)")
|
||
{ objectType = typeof(Sprite), allowSceneObjects = false, value = _defaultSprite };
|
||
bossSpriteField.RegisterValueChangedCallback(evt => _defaultSprite = evt.newValue as Sprite);
|
||
root.Add(bossSpriteField);
|
||
|
||
var sceneGroup = MakeActionGroup();
|
||
sceneGroup.Add(MakeSceneButton("放置嘲风到场景并绑定 SO",
|
||
() => SceneObjectPlacerTool.PlaceChaoFeng(SceneObjectPlacerTool.EnemyBodyColliderType.Box, _defaultSprite)));
|
||
sceneGroup.Add(MakeSceneButton("迁移选中对象 Visual 节点", MigrateSelectedEnemyVisualNode));
|
||
root.Add(sceneGroup);
|
||
|
||
root.Add(MakeSeparator());
|
||
root.Add(MakeSectionHeader("▶ 专项编辑器"));
|
||
|
||
var jumpGroup = MakeActionGroup();
|
||
jumpGroup.Add(MakeJumpButton("武器 HitBox 向导", WeaponHitBoxWizard.Open));
|
||
jumpGroup.Add(MakeJumpButton("Data Hub(敌人 / 能力)", 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, soType) 定义列表。</summary>
|
||
private static (string ablName, string ablId, System.Type soType)[] GetEnemyAbilityDefs(string enemyId) => enemyId switch
|
||
{
|
||
// Alert/警觉张口已是纯 locomotion 模式(LocomotionMode.Face),非能力;起手张口并入 RushAbility 的 Skill_Start。
|
||
"E001" => new[] { ("Chase", "e001_chase", typeof(RushAbilitySO)) },
|
||
"E002" => new[] { ("CeilingStrike", "e002_ceiling_strike", typeof(CeilingHangStrikeAbilitySO)) },
|
||
"E003" => new[] { ("Fall", "e003_fall", typeof(AnimatedCeilingDropAbilitySO)) },
|
||
"E004" => new[] { ("Appear", "e004_appear", typeof(AppearAbilitySO)),
|
||
("Bite", "e004_bite", typeof(EnemyAbilitySO)),
|
||
("HeadSlam", "e004_headslam", typeof(RepeatSlamAbilitySO)),
|
||
("Acid", "e004_acid", typeof(EnemyAbilitySO)),
|
||
("Flip", "e004_flip", typeof(FacePlayerAbilitySO)) },
|
||
"E005" => new[] { ("Bite", "e005_bite", typeof(EnemyAbilitySO)),
|
||
("Acid", "e005_acid", typeof(EnemyAbilitySO)) },
|
||
"E006" => new[] { ("Leap", "e006_leap", typeof(EnemyAbilitySO)),
|
||
("Chase", "e006_chase", typeof(RushAbilitySO)) },
|
||
_ => System.Array.Empty<(string, string, System.Type)>(),
|
||
};
|
||
|
||
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, System.Type soType)
|
||
{
|
||
string dir = $"Assets/_Game/Data/Enemies/{enemyId}/Abilities";
|
||
string name = $"ABL_{enemyId}_{ablName}";
|
||
var so = EditorScaffoldUtils.CreateSOAsset(soType, dir, name) as EnemyAbilitySO;
|
||
// 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, soType) in GetEnemyAbilityDefs(id))
|
||
CreateEnemyAbilitySO(id, ablName, ablId, soType);
|
||
AssetDatabase.SaveAssets();
|
||
EditorUtility.DisplayDialog("创建完成",
|
||
$"全部 {id} SO 已创建(已存在的跳过)。\n请放置到场景后检查组件绑定。", "确定");
|
||
}
|
||
|
||
private static void PlaceSpecificEnemy(string id, SceneObjectPlacerTool.EnemyBodyColliderType bodyCollider, Sprite defaultSprite)
|
||
{
|
||
switch (id)
|
||
{
|
||
case "E001": SceneObjectPlacerTool.PlaceE001_CaoZhi(bodyCollider, defaultSprite); break;
|
||
case "E002": SceneObjectPlacerTool.PlaceE002_HuangZhi(bodyCollider, defaultSprite); break;
|
||
case "E003": SceneObjectPlacerTool.PlaceE003_YouZhi_Enemy(bodyCollider, defaultSprite); break;
|
||
case "E004": SceneObjectPlacerTool.PlaceE004_ZhiMu_Enemy(bodyCollider, defaultSprite); break;
|
||
case "E005": SceneObjectPlacerTool.PlaceE005_FeiZhi_Enemy(bodyCollider, defaultSprite); break;
|
||
case "E006": SceneObjectPlacerTool.PlaceE006_Huan(bodyCollider, defaultSprite); break;
|
||
default:
|
||
Debug.LogError($"[CharacterWizardWindow] 未注册的敌人 id '{id}',请在 SceneObjectPlacerTool 中实现对应 PlaceE...() 方法并注册。");
|
||
SceneObjectPlacerTool.PlaceEnemy(bodyCollider, defaultSprite);
|
||
break;
|
||
}
|
||
}
|
||
|
||
private static void MigrateSelectedEnemyVisualNode()
|
||
{
|
||
var targets = Selection.gameObjects;
|
||
if (targets == null || targets.Length == 0)
|
||
{
|
||
EditorUtility.DisplayDialog("迁移 Visual 节点",
|
||
"请先在 Hierarchy 中选中一个或多个 Enemy 对象。", "确定");
|
||
return;
|
||
}
|
||
int count = 0;
|
||
foreach (var go in targets)
|
||
{
|
||
var movement = go.GetComponent<EnemyMovement>();
|
||
if (movement != null) { movement.SetupVisualNode(); count++; }
|
||
}
|
||
if (count == 0)
|
||
EditorUtility.DisplayDialog("迁移 Visual 节点",
|
||
"选中的对象中没有找到 EnemyMovement 组件。", "确定");
|
||
else
|
||
Debug.Log($"[CharacterWizardWindow] 已对 {count} 个对象完成 Visual 节点迁移。");
|
||
}
|
||
|
||
// ── 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 CreateChaoFengAbilitySO(
|
||
string abilityName, string abilityId, System.Type soType,
|
||
float rangeRadius, float cooldown, float weight, int phase, bool requiresGrounded)
|
||
{
|
||
const string dir = "Assets/_Game/Data/Enemies/ChaoFeng/Abilities";
|
||
string name = $"ABL_ChaoFeng_{abilityName}";
|
||
|
||
var so = EditorScaffoldUtils.CreateSOAsset(soType, dir, name) as EnemyAbilitySO;
|
||
if (so == null) return; // 已存在则不覆盖
|
||
|
||
so.abilityId = abilityId;
|
||
so.designNote = phase < 0
|
||
? "入场演出,不进选招池"
|
||
: $"阶段 {phase} 招式;阶段可用性由 BossPhaseAbilityGate 配置";
|
||
so.cooldown = cooldown;
|
||
// 空中阶段的招必须放开这一项:选招器把 requiresGrounded 当硬门,
|
||
// Boss 浮空时 IsGrounded 恒为 false,留着默认 true 会让该招永远选不中。
|
||
so.requiresGrounded = requiresGrounded;
|
||
|
||
if (phase < 0)
|
||
{
|
||
so.category = AbilityCategory.None;
|
||
so.rangeRadius = 0f;
|
||
so.weight = 0f;
|
||
}
|
||
else
|
||
{
|
||
so.category = AbilityCategory.Attack;
|
||
so.rangeRadius = rangeRadius; // 必须 > 0,否则选招器永远选不中
|
||
so.weight = weight;
|
||
}
|
||
|
||
EditorUtility.SetDirty(so);
|
||
AssetDatabase.SaveAssets();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 嘲风能力集。阶段可用性不在 SO 上——由 BossPhaseAbilityGate 承载
|
||
/// (见放置工具 PlaceChaoFeng),这里只记录供说明文本使用。
|
||
/// rangeRadius 必须 > 0:category==Attack 且射程为 0 的招永远够不着玩家,
|
||
/// EnemyAbilitySO.Validate 与 EnemyBase.BuildAttackSelector 都会显式报错。
|
||
/// grounded 是选招器的硬门:阶段 1 是空中阶段(Boss 浮空,IsGrounded 恒 false),
|
||
/// 该阶段的招必须为 false,否则永远选不中,空中阶段会站着不打。
|
||
/// </summary>
|
||
private static readonly (string name, string id, System.Type soType,
|
||
float rangeRadius, float cooldown, float weight, int phase, bool grounded)[]
|
||
ChaoFengAbilityDefs =
|
||
{
|
||
("Intro", "chaofeng_intro", typeof(PlayClipAbilitySO), 0f, 0f, 0f, -1, true),
|
||
("Boomerang", "boomerang", typeof(PlayClipAbilitySO), 9f, 4.0f, 1.0f, 0, true),
|
||
("FanCombo", "fan_combo", typeof(EnemyAbilitySO), 2.5f, 2.5f, 1.5f, 0, true),
|
||
("TornadoSmall", "tornado_small", typeof(PlayClipAbilitySO), 8f, 3.5f, 1.2f, 0, true),
|
||
("TornadoLarge", "tornado_large", typeof(PlayClipAbilitySO), 12f, 6.0f, 0.8f, 0, true),
|
||
("WindStone", "wind_stone", typeof(PlayClipAbilitySO), 12f, 3.0f, 1.0f, 1, false),
|
||
};
|
||
|
||
private static void CreateAllChaoFengSOs()
|
||
{
|
||
CreateChaoFengStatsSO();
|
||
CreateChaoFengAnimConfigSO();
|
||
foreach (var (n, id, t, r, cd, w, ph, g) in ChaoFengAbilityDefs)
|
||
CreateChaoFengAbilitySO(n, id, t, r, cd, w, ph, g);
|
||
AssetDatabase.SaveAssets();
|
||
EditorUtility.DisplayDialog("创建完成",
|
||
"全部嘲风 SO 已创建(已存在的跳过)。\n" +
|
||
"接下来:\n" +
|
||
"1. 用「放置嘲风到场景并绑定 SO」生成组件树;\n" +
|
||
"2. 检查 EnemyAiBrain._definitionId = ChaoFeng;\n" +
|
||
"3. 检查各能力组件的 _config 已绑定对应 ABL_ 资产;\n" +
|
||
"4. 在 BossPhaseAbilityGate 上确认阶段表(地面四招=阶段0,风石=阶段1)。",
|
||
"确定");
|
||
}
|
||
|
||
// ── 场景搭建(已移至 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 List<(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")),
|
||
};
|
||
foreach (var (abName, _, _, _, _, _, _, _) in ChaoFengAbilityDefs)
|
||
checks.Add(($"ABL_ChaoFeng_{abName}",
|
||
FindAtPath<EnemyAbilitySO>($"{ablDir}/ABL_ChaoFeng_{abName}.asset")));
|
||
|
||
// AI 图是代码定义的(定制路径),没有资产可查——用注册表确认它被反射收集到了
|
||
bool hasAiGraph = BaseGames.AI.AiDefinitionRegistry.Has("ChaoFeng");
|
||
_bossStatusPanel.Add(MakeStatusGrid(checks.ToArray()));
|
||
_bossStatusPanel.Add(new Label(
|
||
hasAiGraph ? "✔ AI 图 ChaoFengAi 已注册(定制路径 _definitionId = ChaoFeng)"
|
||
: "✘ AI 图 ChaoFengAi 未注册——检查 [AiDefinition(\"ChaoFeng\")] 特性"));
|
||
}
|
||
|
||
// ── 辅助:状态格 ─────────────────────────────────────────────────────
|
||
|
||
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);
|
||
}
|
||
}
|