feat: Add SkillModule and WeaponModule for managing skills and weapons

- Implemented SkillModule to manage FormSkillSO assets with a detailed UI for editing and displaying skill properties.
- Implemented WeaponModule to manage WeaponSO assets with a detailed UI for editing and displaying weapon properties.
- Created AssetOperations class for centralized CRUD operations on ScriptableObject assets, including create, rename, delete, and clone functionalities.
- Added DetailHeader for displaying and renaming asset names in the UI.
- Introduced SoListPane for a reusable ScriptableObject list panel with search functionality and context menus.
- Added meta files for all new scripts to ensure proper asset management in Unity.
This commit is contained in:
2026-05-21 07:09:53 +08:00
parent f096105caf
commit bb3afd130f
41 changed files with 2417 additions and 1618 deletions

View File

@@ -0,0 +1,212 @@
using System;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using BaseGames.Boss;
namespace BaseGames.Editor.Modules
{
/// <summary>
/// DataHub Boss技能模块 —— Tab 切换管理 BossSkillSO 和 SkillSequenceSO。
/// </summary>
public class BossSkillModule : IDataModule
{
private const string SkillFolder = "Assets/_Game/Data/Boss/Skills";
private const string SeqFolder = "Assets/_Game/Data/Boss/Sequences";
public string ModuleId => "boss";
public string DisplayName => "Boss技能";
public string IconName => "d_SkinnedMeshRenderer Icon";
private int _activeTab = 0;
private SoListPane<BossSkillSO> _skillPane;
private SoListPane<SkillSequenceSO> _seqPane;
private Action<UnityEngine.Object> _onSelected;
private DetailHeader _header;
private BossSkillSO _selectedSkill;
private SkillSequenceSO _selectedSeq;
public void Initialize()
{
_skillPane = new SoListPane<BossSkillSO>(
SkillFolder, "ABL_Boss_",
s => s.category.ToString());
_skillPane.SelectionChanged = s => { _selectedSkill = s; _onSelected?.Invoke(s); };
_seqPane = new SoListPane<SkillSequenceSO>(SeqFolder, "ABL_Seq_");
_seqPane.SelectionChanged = s => { _selectedSeq = s; _onSelected?.Invoke(s); };
}
public void BuildListPane(VisualElement container, Action<UnityEngine.Object> onSelected)
{
_onSelected = onSelected;
container.style.flexDirection = FlexDirection.Column;
// Tab bar
var tabBar = new VisualElement();
tabBar.style.flexDirection = FlexDirection.Row;
tabBar.style.borderBottomWidth = 1;
tabBar.style.borderBottomColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.3f));
container.Add(tabBar);
var btnSkill = BuildTabBtn("技能 (Skill)", 0, tabBar);
var btnSeq = BuildTabBtn("序列 (Seq)", 1, tabBar);
var listArea = new VisualElement();
listArea.style.flexGrow = 1;
container.Add(listArea);
ShowTab(0, listArea, new[] { btnSkill, btnSeq });
btnSkill.clicked += () => ShowTab(0, listArea, new[] { btnSkill, btnSeq });
btnSeq.clicked += () => ShowTab(1, listArea, new[] { btnSkill, btnSeq });
_skillPane.Refresh();
_seqPane.Refresh();
}
public void BuildDetailPane(VisualElement container, UnityEngine.Object selected)
{
_header = new DetailHeader();
_header.SetAsset(selected);
_header.RenameRequested += name => OnRenameRequested(selected, name);
container.Add(_header);
if (selected == null) return;
if (selected is BossSkillSO skill)
{
container.Add(BuildSkillCard(skill));
container.Add(BuildActionBar(skill, SkillFolder, _skillPane));
container.Add(SkillModule.MakeDivider());
var insp = new InspectorElement(skill);
insp.style.flexGrow = 1;
container.Add(insp);
}
else if (selected is SkillSequenceSO seq)
{
container.Add(BuildSeqCard(seq));
container.Add(BuildActionBar(seq, SeqFolder, _seqPane));
container.Add(SkillModule.MakeDivider());
var insp = new InspectorElement(seq);
insp.style.flexGrow = 1;
container.Add(insp);
}
}
public void OnActivated()
{
_skillPane?.Refresh();
_seqPane?.Refresh();
}
// ── 内部 ─────────────────────────────────────────────────────────────
private Button BuildTabBtn(string text, int tabIdx, VisualElement bar)
{
var btn = new Button { text = text };
btn.style.flexGrow = 1;
btn.style.paddingTop = 5;
btn.style.paddingBottom = 5;
btn.style.borderTopLeftRadius = 0;
btn.style.borderTopRightRadius = 0;
btn.style.borderBottomLeftRadius = 0;
btn.style.borderBottomRightRadius = 0;
btn.style.borderLeftWidth = 0;
btn.style.borderRightWidth = 0;
btn.style.borderTopWidth = 0;
btn.style.borderBottomWidth = 0;
btn.style.backgroundColor = new StyleColor(Color.clear);
btn.userData = tabIdx;
bar.Add(btn);
return btn;
}
private void ShowTab(int tab, VisualElement area, Button[] tabBtns)
{
_activeTab = tab;
area.Clear();
for (int i = 0; i < tabBtns.Length; i++)
{
if (i == tab)
{
tabBtns[i].style.borderBottomWidth = 2;
tabBtns[i].style.borderBottomColor = new StyleColor(new Color(0.4f, 0.65f, 1f, 1f));
tabBtns[i].style.opacity = 1f;
}
else
{
tabBtns[i].style.borderBottomWidth = 0;
tabBtns[i].style.opacity = 0.65f;
}
}
if (tab == 0) { _skillPane.style.flexGrow = 1; area.Add(_skillPane); }
else { _seqPane.style.flexGrow = 1; area.Add(_seqPane); }
}
private void OnRenameRequested(UnityEngine.Object asset, string newName)
{
var (ok, err) = AssetOperations.Rename(asset, newName);
if (!ok) EditorUtility.DisplayDialog("重命名失败", err, "确定");
else
{
_header.SetAsset(asset);
if (_activeTab == 0) _skillPane.Invalidate();
else _seqPane.Invalidate();
}
}
private static VisualElement BuildSkillCard(BossSkillSO s)
{
var card = SkillModule.MakeCard();
SkillModule.AddChip(card, "分类", s.category.ToString());
SkillModule.AddChip(card, "类型", s.skillType.ToString());
SkillModule.AddChip(card, "模式数", (s.attackPatterns?.Length ?? 0).ToString());
SkillModule.AddChip(card, "弱点窗口", (s.vulnerabilityWindows?.Length ?? 0).ToString());
if (!string.IsNullOrEmpty(s.skillId))
SkillModule.AddChip(card, "ID", s.skillId);
return card;
}
private static VisualElement BuildSeqCard(SkillSequenceSO s)
{
var card = SkillModule.MakeCard();
SkillModule.AddChip(card, "步骤数", (s.steps?.Length ?? 0).ToString());
SkillModule.AddChip(card, "循环", s.RepeatIfPlayerInRange ? "是" : "否");
SkillModule.AddChip(card, "最大循环次数", s.MaxRepeatCount.ToString());
return card;
}
private VisualElement BuildActionBar<T>(T asset, string folder, SoListPane<T> pane)
where T : ScriptableObject
{
var bar = SkillModule.MakeActionBar();
new Button(() => { EditorGUIUtility.PingObject(asset); Selection.activeObject = asset; })
{ text = "定位" }.AlsoAddTo(bar);
new Button(() =>
{
var c = AssetOperations.Clone(asset, folder);
if (c != null) pane.Refresh(c);
}) { text = "克隆..." }.AlsoAddTo(bar);
var del = new Button(() =>
{
if (AssetOperations.Delete(asset)) pane.Refresh(null);
}) { text = "删除" };
del.style.borderLeftColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
del.style.borderRightColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
del.style.borderTopColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
del.style.borderBottomColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
del.style.borderLeftWidth = 1;
del.style.borderRightWidth = 1;
del.style.borderTopWidth = 1;
del.style.borderBottomWidth = 1;
del.style.marginLeft = 8;
del.AlsoAddTo(bar);
return bar;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f0d0425e529293e469da3762fe3bf8f0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,207 @@
using System;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using BaseGames.Enemies;
namespace BaseGames.Editor.Modules
{
/// <summary>
/// DataHub 敌人模块 —— Tab 切换管理 EnemyStatsSO 和 LootTableSO。
/// </summary>
public class EnemyModule : IDataModule
{
private const string StatsFolder = "Assets/_Game/Data/Enemies/Stats";
private const string LootFolder = "Assets/_Game/Data/Enemies/Loot";
public string ModuleId => "enemy";
public string DisplayName => "敌人";
public string IconName => "d_Avatar Icon";
private int _activeTab = 0; // 0=Stats, 1=Loot
private SoListPane<EnemyStatsSO> _statsPane;
private SoListPane<LootTableSO> _lootPane;
private VisualElement _listContainer;
private Action<UnityEngine.Object> _onSelected;
private DetailHeader _header;
private EnemyStatsSO _selectedStats;
private LootTableSO _selectedLoot;
public void Initialize()
{
_statsPane = new SoListPane<EnemyStatsSO>(StatsFolder, "ENM_");
_statsPane.SelectionChanged = s => { _selectedStats = s; _onSelected?.Invoke(s); };
_lootPane = new SoListPane<LootTableSO>(LootFolder, "ENM_Loot_");
_lootPane.SelectionChanged = l => { _selectedLoot = l; _onSelected?.Invoke(l); };
}
public void BuildListPane(VisualElement container, Action<UnityEngine.Object> onSelected)
{
_onSelected = onSelected;
_listContainer = container;
container.style.flexDirection = FlexDirection.Column;
// Tab bar
var tabBar = new VisualElement();
tabBar.style.flexDirection = FlexDirection.Row;
tabBar.style.borderBottomWidth = 1;
tabBar.style.borderBottomColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.3f));
container.Add(tabBar);
var btnStats = BuildTabBtn("属性 (Stats)", 0, tabBar);
var btnLoot = BuildTabBtn("掉落 (Loot)", 1, tabBar);
// 列表区域占位
var listArea = new VisualElement();
listArea.style.flexGrow = 1;
container.Add(listArea);
ShowTab(0, listArea, new[] { btnStats, btnLoot });
btnStats.clicked += () => ShowTab(0, listArea, new[] { btnStats, btnLoot });
btnLoot.clicked += () => ShowTab(1, listArea, new[] { btnStats, btnLoot });
_statsPane.Refresh();
_lootPane.Refresh();
}
public void BuildDetailPane(VisualElement container, UnityEngine.Object selected)
{
_header = new DetailHeader();
_header.SetAsset(selected);
_header.RenameRequested += name => OnRenameRequested(selected, name);
container.Add(_header);
if (selected == null) return;
if (selected is EnemyStatsSO stats)
{
container.Add(BuildStatsCard(stats));
container.Add(BuildActionBar(stats, StatsFolder, _statsPane));
container.Add(SkillModule.MakeDivider());
var insp = new InspectorElement(stats);
insp.style.flexGrow = 1;
container.Add(insp);
}
else if (selected is LootTableSO loot)
{
container.Add(BuildLootCard(loot));
container.Add(BuildActionBar(loot, LootFolder, _lootPane));
container.Add(SkillModule.MakeDivider());
var insp = new InspectorElement(loot);
insp.style.flexGrow = 1;
container.Add(insp);
}
}
public void OnActivated()
{
_statsPane?.Refresh();
_lootPane?.Refresh();
}
// ── 内部 ─────────────────────────────────────────────────────────────
private Button BuildTabBtn(string text, int tabIdx, VisualElement bar)
{
var btn = new Button { text = text };
btn.style.flexGrow = 1;
btn.style.paddingTop = 5;
btn.style.paddingBottom = 5;
btn.style.borderTopLeftRadius = 0;
btn.style.borderTopRightRadius = 0;
btn.style.borderBottomLeftRadius = 0;
btn.style.borderBottomRightRadius = 0;
btn.style.borderLeftWidth = 0;
btn.style.borderRightWidth = 0;
btn.style.borderTopWidth = 0;
btn.style.borderBottomWidth = 0;
btn.style.backgroundColor = new StyleColor(Color.clear);
btn.userData = tabIdx;
bar.Add(btn);
return btn;
}
private void ShowTab(int tab, VisualElement area, Button[] tabBtns)
{
_activeTab = tab;
area.Clear();
for (int i = 0; i < tabBtns.Length; i++)
{
if (i == tab)
{
tabBtns[i].style.borderBottomWidth = 2;
tabBtns[i].style.borderBottomColor = new StyleColor(new Color(0.4f, 0.65f, 1f, 1f));
tabBtns[i].style.opacity = 1f;
}
else
{
tabBtns[i].style.borderBottomWidth = 0;
tabBtns[i].style.opacity = 0.65f;
}
}
if (tab == 0) { _statsPane.style.flexGrow = 1; area.Add(_statsPane); }
else { _lootPane.style.flexGrow = 1; area.Add(_lootPane); }
}
private void OnRenameRequested(UnityEngine.Object asset, string newName)
{
var (ok, err) = AssetOperations.Rename(asset, newName);
if (!ok) EditorUtility.DisplayDialog("重命名失败", err, "确定");
else
{
_header.SetAsset(asset);
if (_activeTab == 0) _statsPane.Invalidate();
else _lootPane.Invalidate();
}
}
private static VisualElement BuildStatsCard(EnemyStatsSO s)
{
var card = SkillModule.MakeCard();
SkillModule.AddChip(card, "HP", s.MaxHP.ToString());
SkillModule.AddChip(card, "防御", s.Defense.ToString());
SkillModule.AddChip(card, "移速", $"{s.WalkSpeed}/{s.RunSpeed}");
SkillModule.AddChip(card, "攻击", s.AttackDamage.ToString());
SkillModule.AddChip(card, "感知", $"{s.DetectRange}m");
return card;
}
private static VisualElement BuildLootCard(LootTableSO l)
{
var card = SkillModule.MakeCard();
SkillModule.AddChip(card, "掉落项", (l.Entries?.Length ?? 0).ToString());
SkillModule.AddChip(card, "灵珠保底", $"{l.GuaranteedLingZhuMin}-{l.GuaranteedLingZhuMax}");
return card;
}
private VisualElement BuildActionBar<T>(T asset, string folder, SoListPane<T> pane)
where T : ScriptableObject
{
var bar = SkillModule.MakeActionBar();
new Button(() => { EditorGUIUtility.PingObject(asset); Selection.activeObject = asset; })
{ text = "定位" }.AlsoAddTo(bar);
new Button(() => { var c = AssetOperations.Clone(asset, folder); if (c != null) pane.Refresh(c); })
{ text = "克隆..." }.AlsoAddTo(bar);
var del = new Button(() => { if (AssetOperations.Delete(asset)) pane.Refresh(null); }) { text = "删除" };
del.style.borderLeftColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
del.style.borderRightColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
del.style.borderTopColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
del.style.borderBottomColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
del.style.borderLeftWidth = 1;
del.style.borderRightWidth = 1;
del.style.borderTopWidth = 1;
del.style.borderBottomWidth = 1;
del.style.marginLeft = 8;
del.AlsoAddTo(bar);
return bar;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 32772b7d7bbc5824889620b773c352a8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,209 @@
using System;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using BaseGames.Player;
namespace BaseGames.Editor.Modules
{
/// <summary>
/// DataHub 形态模块 —— 管理 FormConfigSO含三列 FormSO 预览)和 FormSO 资产。
/// </summary>
public class FormModule : IDataModule
{
private const string ConfigFolder = "Assets/_Game/Data/Player/Forms";
private const string FormFolder = "Assets/_Game/Data/Player/Forms";
public string ModuleId => "form";
public string DisplayName => "形态";
public string IconName => "d_AvatarPivot";
private SoListPane<FormConfigSO> _listPane;
private DetailHeader _header;
private FormConfigSO _selected;
private static readonly (FormType type, string label, Color accent)[] FormDefs =
{
(FormType.TianHun, "天魂", new Color(0.40f, 0.70f, 1.00f)),
(FormType.DiHun, "地魂", new Color(0.85f, 0.55f, 0.20f)),
(FormType.MingHun, "命魂", new Color(0.70f, 0.25f, 0.75f)),
};
public void Initialize()
{
_listPane = new SoListPane<FormConfigSO>(ConfigFolder, "PLY_FormConfig_");
}
public void BuildListPane(VisualElement container, Action<UnityEngine.Object> onSelected)
{
_listPane.SelectionChanged = sel =>
{
_selected = sel;
onSelected?.Invoke(sel);
};
container.Add(_listPane);
_listPane.Refresh();
}
public void BuildDetailPane(VisualElement container, UnityEngine.Object selected)
{
_selected = selected as FormConfigSO;
_header = new DetailHeader();
_header.SetAsset(_selected);
_header.RenameRequested += OnRenameRequested;
container.Add(_header);
if (_selected == null) return;
// 操作按钮
container.Add(BuildActionBar(_selected));
container.Add(SkillModule.MakeDivider());
// 三列形态网格
var grid = BuildFormGrid(_selected);
container.Add(grid);
container.Add(SkillModule.MakeDivider());
// Raw Inspector
var insp = new InspectorElement(_selected);
insp.style.flexGrow = 1;
container.Add(insp);
}
public void OnActivated() => _listPane?.Refresh();
// ── 内部 ─────────────────────────────────────────────────────────────
private void OnRenameRequested(string newName)
{
if (_selected == null) return;
var (ok, err) = AssetOperations.Rename(_selected, newName);
if (!ok) EditorUtility.DisplayDialog("重命名失败", err, "确定");
else { _header.SetAsset(_selected); _listPane.Invalidate(); }
}
private VisualElement BuildFormGrid(FormConfigSO config)
{
var grid = new VisualElement();
grid.style.flexDirection = FlexDirection.Row;
grid.style.paddingLeft = 8;
grid.style.paddingRight = 8;
grid.style.paddingTop = 8;
grid.style.paddingBottom = 8;
for (int i = 0; i < FormDefs.Length; i++)
{
var (ft, label, accent) = FormDefs[i];
var col = BuildFormColumn(config, ft, label, accent);
if (i < 2) col.style.marginRight = 8;
grid.Add(col);
}
return grid;
}
private static VisualElement BuildFormColumn(
FormConfigSO config, FormType ft, string label, Color accent)
{
var col = new VisualElement();
col.style.flexGrow = 1;
col.style.borderTopWidth = 2;
col.style.borderTopColor = new StyleColor(accent);
col.style.borderLeftWidth = 1;
col.style.borderRightWidth = 1;
col.style.borderBottomWidth = 1;
col.style.borderLeftColor = new StyleColor(new Color(accent.r, accent.g, accent.b, 0.35f));
col.style.borderRightColor = new StyleColor(new Color(accent.r, accent.g, accent.b, 0.35f));
col.style.borderBottomColor = new StyleColor(new Color(accent.r, accent.g, accent.b, 0.35f));
col.style.borderTopLeftRadius = 4;
col.style.borderTopRightRadius = 4;
col.style.borderBottomLeftRadius = 4;
col.style.borderBottomRightRadius = 4;
col.style.paddingLeft = 8;
col.style.paddingRight = 8;
col.style.paddingTop = 8;
col.style.paddingBottom = 8;
// 标题行
var titleRow = new VisualElement();
titleRow.style.flexDirection = FlexDirection.Row;
titleRow.style.alignItems = Align.Center;
titleRow.style.marginBottom = 6;
var dot = new VisualElement();
dot.style.width = 10;
dot.style.height = 10;
dot.style.borderTopLeftRadius = 5;
dot.style.borderTopRightRadius = 5;
dot.style.borderBottomLeftRadius = 5;
dot.style.borderBottomRightRadius = 5;
dot.style.backgroundColor = new StyleColor(accent);
dot.style.marginRight = 6;
titleRow.Add(dot);
titleRow.Add(new Label(label) { style = { unityFontStyleAndWeight = UnityEngine.FontStyle.Bold } });
col.Add(titleRow);
// FormSO 引用
FormSO current = config.GetFormByType(ft);
var formField = new ObjectField("FormSO") { objectType = typeof(FormSO), value = current };
formField.RegisterValueChangedCallback(e =>
{
var newForm = e.newValue as FormSO;
SetFormByType(config, ft, newForm);
});
col.Add(formField);
// 武器只读预览
var wpnField = new ObjectField("默认武器") { objectType = typeof(WeaponSO), value = current?.defaultWeapon };
wpnField.SetEnabled(false);
formField.RegisterValueChangedCallback(e =>
wpnField.value = (e.newValue as FormSO)?.defaultWeapon);
col.Add(wpnField);
return col;
}
private static void SetFormByType(FormConfigSO config, FormType ft, FormSO form)
{
Undo.RecordObject(config, "Set FormSO");
if (config.forms == null || config.forms.Length < 3)
{
var newArr = new FormSO[3];
if (config.forms != null)
Array.Copy(config.forms, newArr, Math.Min(config.forms.Length, 3));
config.forms = newArr;
}
config.forms[(int)ft] = form;
EditorUtility.SetDirty(config);
}
private VisualElement BuildActionBar(FormConfigSO config)
{
var bar = SkillModule.MakeActionBar();
new Button(() => { EditorGUIUtility.PingObject(config); Selection.activeObject = config; })
{ text = "定位" }.AlsoAddTo(bar);
new Button(() =>
{
var c = AssetOperations.Clone(config, ConfigFolder);
if (c != null) _listPane.Refresh(c);
}) { text = "克隆..." }.AlsoAddTo(bar);
var del = new Button(() =>
{
if (AssetOperations.Delete(config)) _listPane.Refresh(null);
}) { text = "删除" };
del.style.borderLeftColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
del.style.borderRightColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
del.style.borderTopColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
del.style.borderBottomColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
del.style.borderLeftWidth = 1;
del.style.borderRightWidth = 1;
del.style.borderTopWidth = 1;
del.style.borderBottomWidth = 1;
del.style.marginLeft = 8;
del.AlsoAddTo(bar);
return bar;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fa270e8dd563fbc429bdb342988b9a54
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,187 @@
using System;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using BaseGames.Skills;
namespace BaseGames.Editor.Modules
{
/// <summary>
/// DataHub 技能模块 —— 管理 FormSkillSO 资产。
/// </summary>
public class SkillModule : IDataModule
{
private const string Folder = "Assets/_Game/Data/Skills";
private const string Prefix = "SKL_";
public string ModuleId => "skill";
public string DisplayName => "技能";
public string IconName => "d_Lighting Icon";
private SoListPane<FormSkillSO> _listPane;
private DetailHeader _header;
private FormSkillSO _selected;
public void Initialize()
{
_listPane = new SoListPane<FormSkillSO>(
Folder, Prefix,
s => s.resourceType.ToString());
}
public void BuildListPane(VisualElement container, Action<UnityEngine.Object> onSelected)
{
_listPane.SelectionChanged = sel =>
{
_selected = sel;
onSelected?.Invoke(sel);
};
container.Add(_listPane);
_listPane.Refresh();
}
public void BuildDetailPane(VisualElement container, UnityEngine.Object selected)
{
_selected = selected as FormSkillSO;
_header = new DetailHeader();
_header.SetAsset(_selected);
_header.RenameRequested += OnRenameRequested;
container.Add(_header);
if (_selected == null) return;
// Stats Card
var card = BuildStatsCard(_selected);
container.Add(card);
// 操作按钮
container.Add(BuildActionBar(_selected));
container.Add(MakeDivider());
// Inspector
var insp = new InspectorElement(_selected);
insp.style.flexGrow = 1;
container.Add(insp);
}
public void OnActivated() => _listPane?.Refresh();
// ── 内部 ─────────────────────────────────────────────────────────────
private void OnRenameRequested(string newName)
{
if (_selected == null) return;
var (ok, err) = AssetOperations.Rename(_selected, newName);
if (!ok) EditorUtility.DisplayDialog("重命名失败", err, "确定");
else { _header.SetAsset(_selected); _listPane.Invalidate(); }
}
private static VisualElement BuildStatsCard(FormSkillSO s)
{
var card = MakeCard();
AddChip(card, "效果类型", s.effectType.ToString());
AddChip(card, "资源类型", s.resourceType.ToString());
AddChip(card, "冷却", $"{s.cooldown:F1}s");
AddChip(card, "消耗", s.baseCost.ToString());
if (!string.IsNullOrEmpty(s.skillId))
AddChip(card, "ID", s.skillId);
return card;
}
private VisualElement BuildActionBar(FormSkillSO s)
{
var bar = MakeActionBar();
new Button(() => { EditorGUIUtility.PingObject(s); Selection.activeObject = s; })
{ text = "定位" }.AlsoAddTo(bar);
new Button(() => { var c = AssetOperations.Clone(s, Folder); if (c != null) _listPane.Refresh(c); })
{ text = "克隆..." }.AlsoAddTo(bar);
var del = new Button(() => { if (AssetOperations.Delete(s)) _listPane.Refresh(null); }) { text = "删除" };
del.style.borderLeftColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
del.style.borderRightColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
del.style.borderTopColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
del.style.borderBottomColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
del.style.borderLeftWidth = 1;
del.style.borderRightWidth = 1;
del.style.borderTopWidth = 1;
del.style.borderBottomWidth = 1;
del.style.marginLeft = 8;
del.AlsoAddTo(bar);
return bar;
}
// ── 共享构建辅助 ─────────────────────────────────────────────────────
internal static VisualElement MakeCard()
{
var c = new VisualElement();
c.style.flexDirection = FlexDirection.Row;
c.style.flexWrap = Wrap.Wrap;
c.style.paddingLeft = 12;
c.style.paddingRight = 12;
c.style.paddingTop = 8;
c.style.paddingBottom = 8;
c.style.marginBottom = 4;
c.style.backgroundColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.08f));
c.style.borderBottomWidth = 1;
c.style.borderBottomColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.2f));
return c;
}
internal static void AddChip(VisualElement parent, string label, string value)
{
var chip = new VisualElement();
chip.style.flexDirection = FlexDirection.Row;
chip.style.alignItems = Align.Center;
chip.style.marginRight = 14;
chip.style.marginBottom = 2;
var l = new Label(label + ":");
l.style.opacity = 0.6f;
l.style.fontSize = 11;
l.style.marginRight = 3;
chip.Add(l);
var v = new Label(value);
v.style.fontSize = 11;
v.style.unityFontStyleAndWeight = UnityEngine.FontStyle.Bold;
chip.Add(v);
parent.Add(chip);
}
internal static VisualElement MakeActionBar()
{
var b = new VisualElement();
b.style.flexDirection = FlexDirection.Row;
b.style.paddingLeft = 12;
b.style.paddingRight = 12;
b.style.paddingTop = 6;
b.style.paddingBottom = 6;
b.style.flexWrap = Wrap.Wrap;
return b;
}
internal static VisualElement MakeDivider()
{
var d = new VisualElement();
d.style.height = 1;
d.style.backgroundColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.2f));
return d;
}
}
// ── Button 扩展(模块内共用)─────────────────────────────────────────────
internal static class ButtonExtensions
{
public static Button AlsoAddTo(this Button btn, VisualElement parent)
{
parent.Add(btn);
return btn;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7f77603c5dde1584eade768456618cef
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,184 @@
using System;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using BaseGames.Player;
namespace BaseGames.Editor.Modules
{
/// <summary>
/// DataHub 武器模块 —— 管理 WeaponSO 资产。
/// </summary>
public class WeaponModule : IDataModule
{
private const string Folder = "Assets/_Game/Data/Weapons";
private const string Prefix = "WPN_";
public string ModuleId => "weapon";
public string DisplayName => "武器";
public string IconName => "d_Sword Icon";
private SoListPane<WeaponSO> _listPane;
private DetailHeader _header;
private VisualElement _detailRoot;
private WeaponSO _selected;
public void Initialize()
{
_listPane = new SoListPane<WeaponSO>(
Folder, Prefix,
w => w.weaponType.ToString());
}
public void BuildListPane(VisualElement container, Action<UnityEngine.Object> onSelected)
{
_listPane.SelectionChanged = sel =>
{
_selected = sel;
onSelected?.Invoke(sel);
};
container.Add(_listPane);
_listPane.Refresh();
}
public void BuildDetailPane(VisualElement container, UnityEngine.Object selected)
{
_selected = selected as WeaponSO;
// Header重命名
_header = new DetailHeader();
_header.SetAsset(_selected);
_header.RenameRequested += OnRenameRequested;
container.Add(_header);
if (_selected == null) return;
// Stats Card
var statsCard = BuildStatsCard(_selected);
container.Add(statsCard);
// 操作按钮行
var toolbar = BuildActionBar(_selected);
container.Add(toolbar);
// 分隔线
container.Add(MakeDivider());
// Inspector
var insp = new InspectorElement(_selected);
insp.style.flexGrow = 1;
container.Add(insp);
}
public void OnActivated()
{
_listPane?.Refresh();
}
// ── 内部 ─────────────────────────────────────────────────────────────
private void OnRenameRequested(string newName)
{
if (_selected == null) return;
var (ok, err) = AssetOperations.Rename(_selected, newName);
if (!ok)
EditorUtility.DisplayDialog("重命名失败", err, "确定");
else
{
_header.SetAsset(_selected);
_listPane.Invalidate();
}
}
private static VisualElement BuildStatsCard(WeaponSO w)
{
var card = new VisualElement();
card.style.flexDirection = FlexDirection.Row;
card.style.flexWrap = Wrap.Wrap;
card.style.paddingLeft = 12;
card.style.paddingRight = 12;
card.style.paddingTop = 8;
card.style.paddingBottom = 8;
card.style.marginBottom = 4;
card.style.backgroundColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.08f));
card.style.borderBottomWidth = 1;
card.style.borderBottomColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.2f));
AddStatChip(card, "类型", w.weaponType.ToString());
AddStatChip(card, "地面段数", (w.groundComboSteps?.Length ?? 0).ToString());
AddStatChip(card, "空中段数", (w.airComboSteps?.Length ?? 0).ToString());
AddStatChip(card, "ID", string.IsNullOrEmpty(w.weaponId) ? "-" : w.weaponId);
return card;
}
private static void AddStatChip(VisualElement parent, string label, string value)
{
var chip = new VisualElement();
chip.style.flexDirection = FlexDirection.Row;
chip.style.alignItems = Align.Center;
chip.style.marginRight = 14;
chip.style.marginBottom = 2;
var lbl = new Label(label + ":");
lbl.style.opacity = 0.6f;
lbl.style.fontSize = 11;
lbl.style.marginRight = 3;
chip.Add(lbl);
var val = new Label(value);
val.style.fontSize = 11;
val.style.unityFontStyleAndWeight = UnityEngine.FontStyle.Bold;
chip.Add(val);
parent.Add(chip);
}
private VisualElement BuildActionBar(WeaponSO w)
{
var bar = new VisualElement();
bar.style.flexDirection = FlexDirection.Row;
bar.style.paddingLeft = 12;
bar.style.paddingRight = 12;
bar.style.paddingTop = 6;
bar.style.paddingBottom = 6;
bar.style.flexWrap = Wrap.Wrap;
var btnPing = new Button(() => { EditorGUIUtility.PingObject(w); Selection.activeObject = w; })
{ text = "在 Project 中定位", tooltip = "在 Project 窗口高亮此资产" };
bar.Add(btnPing);
var btnClone = new Button(() =>
{
var clone = AssetOperations.Clone(w, Folder);
if (clone != null) _listPane.Refresh(clone);
}) { text = "克隆..." };
bar.Add(btnClone);
var btnDel = new Button(() =>
{
if (AssetOperations.Delete(w)) _listPane.Refresh(null);
}) { text = "删除" };
btnDel.style.borderLeftColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
btnDel.style.borderRightColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
btnDel.style.borderTopColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
btnDel.style.borderBottomColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
btnDel.style.borderLeftWidth = 1;
btnDel.style.borderRightWidth = 1;
btnDel.style.borderTopWidth = 1;
btnDel.style.borderBottomWidth = 1;
btnDel.style.marginLeft = 8;
bar.Add(btnDel);
return bar;
}
private static VisualElement MakeDivider()
{
var d = new VisualElement();
d.style.height = 1;
d.style.backgroundColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.2f));
return d;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7b4131880a180b34d9f619b70813edb8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: