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

@@ -1,277 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using BaseGames.Combat;
using BaseGames.Skills;
namespace BaseGames.Editor.Skills
{
/// <summary>
/// 技能数据管理窗口W-03
/// 技术UI Toolkit TwoPaneSplitView。
/// 菜单BaseGames / Data / Skill Editor
///
/// 左栏:可搜索的 FormSkillSO 列表,按 SkillEffectType 分组过滤。
/// 右栏:选中技能的完整属性编辑 + HitBox Prefab 结构校验 + 底部资源消耗预览。
/// </summary>
public class SkillEditorWindow : EditorWindow
{
private static readonly StyleSheet _sharedUSS;
private static readonly string[] _effectTypeOptions;
static SkillEditorWindow()
{
_sharedUSS = AssetDatabase.LoadAssetAtPath<StyleSheet>(
"Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss");
var names = Enum.GetNames(typeof(SkillEffectType));
_effectTypeOptions = new string[names.Length + 1];
_effectTypeOptions[0] = "全部";
Array.Copy(names, 0, _effectTypeOptions, 1, names.Length);
}
[MenuItem("BaseGames/Data/Skill Editor", priority = 101)]
public static void Open()
{
var wnd = GetWindow<SkillEditorWindow>();
wnd.titleContent = new GUIContent("Skill Editor");
wnd.minSize = new Vector2(700, 400);
}
// ── 状态 ─────────────────────────────────────────────────────────────
private List<FormSkillSO> _skills = new();
private List<FormSkillSO> _filtered = new();
private ListView _listView;
private VisualElement _detailRoot;
private string _searchText = "";
private string _filterType = "全部";
private InspectorElement _currentInspector;
// ── 生命周期 ──────────────────────────────────────────────────────────
public void CreateGUI()
{
if (_sharedUSS != null)
rootVisualElement.styleSheets.Add(_sharedUSS);
// Toolbar
var toolbar = new Toolbar();
var searchField = new ToolbarSearchField { style = { flexGrow = 1 } };
searchField.RegisterValueChangedCallback(e =>
{
_searchText = e.newValue;
RefreshFilter();
});
toolbar.Add(searchField);
// SkillEffectType 过滤下拉框
var typeFilter = new ToolbarMenu { text = "类型:全部", style = { minWidth = 100 } };
foreach (var opt in _effectTypeOptions)
{
string captured = opt;
typeFilter.menu.AppendAction(opt, _ =>
{
_filterType = captured;
typeFilter.text = $"类型:{captured}";
RefreshFilter();
});
}
toolbar.Add(typeFilter);
var btnCreate = new ToolbarButton(CreateNewSkill) { text = "+ 新建技能" };
var btnRefresh = new ToolbarButton(RefreshAll) { text = "↺" };
btnRefresh.tooltip = "重新扫描 Project 中的 FormSkillSO 资产";
toolbar.Add(btnCreate);
toolbar.Add(btnRefresh);
rootVisualElement.Add(toolbar);
// Split view
var split = new TwoPaneSplitView(0, 220, TwoPaneSplitViewOrientation.Horizontal);
// ── 左栏 ──────────────────────────────────────────────────────
var leftPane = new VisualElement { style = { minWidth = 140 } };
_listView = new ListView
{
selectionType = SelectionType.Single,
fixedItemHeight = 22,
makeItem = MakeListItem,
bindItem = BindListItem,
style = { flexGrow = 1 },
};
_listView.selectionChanged += OnSelectionChanged;
leftPane.Add(_listView);
split.Add(leftPane);
// ── 右栏 ──────────────────────────────────────────────────────
_detailRoot = new ScrollView { style = { flexGrow = 1 } };
_detailRoot.AddToClassList("detail-panel");
split.Add(_detailRoot);
rootVisualElement.Add(split);
RefreshAll();
}
private void OnFocus() => RefreshAll();
// ── 列表构建 ──────────────────────────────────────────────────────────
private void RefreshAll()
{
_skills = EditorScaffoldUtils.FindAllAssetsOfType<FormSkillSO>();
_skills.Sort((a, b) => string.Compare(
a.skillId, b.skillId, StringComparison.OrdinalIgnoreCase));
RefreshFilter();
}
private void RefreshFilter()
{
IEnumerable<FormSkillSO> query = _skills;
if (_filterType != "全部" && Enum.TryParse(_filterType, out SkillEffectType filterEnum))
query = query.Where(s => s.effectType == filterEnum);
if (!string.IsNullOrEmpty(_searchText))
{
string s = _searchText;
query = query.Where(sk => sk != null &&
(sk.skillId?.Contains(s, StringComparison.OrdinalIgnoreCase) == true ||
sk.displayNameKey?.Contains(s, StringComparison.OrdinalIgnoreCase) == true));
}
_filtered = query.ToList();
_listView.itemsSource = _filtered;
_listView.Rebuild();
}
private static VisualElement MakeListItem()
{
var label = new Label();
label.AddToClassList("list-item");
label.enableRichText = true;
return label;
}
private void BindListItem(VisualElement element, int index)
{
var label = (Label)element;
var skill = _filtered.Count > index ? _filtered[index] : null;
if (skill == null) { label.text = "(null)"; return; }
label.text = string.IsNullOrEmpty(skill.displayNameKey)
? skill.skillId
: $"{skill.skillId} <color=#888>[{skill.effectType}]</color>";
}
// ── 详情面板 ──────────────────────────────────────────────────────────
private void OnSelectionChanged(IEnumerable<object> items)
{
_detailRoot.Clear();
_currentInspector = null;
var skill = items.FirstOrDefault() as FormSkillSO;
if (skill == null) return;
// 标题
var title = new Label($"{skill.skillId} [{skill.effectType}]")
{
style =
{
fontSize = 14,
unityFontStyleAndWeight = FontStyle.Bold,
marginBottom = 6,
}
};
_detailRoot.Add(title);
// 资源消耗快览
BuildCostPreview(skill);
// HitBox Prefab 状态
BuildHitBoxStatus(skill);
// 完整属性编辑
_currentInspector = new InspectorElement(skill);
_detailRoot.Add(_currentInspector);
// 操作按钮
var btnRow = new VisualElement();
btnRow.AddToClassList("action-buttons");
btnRow.Add(new Button(() => EditorScaffoldUtils.PingAndSelect(skill))
{ text = "在 Project 中定位" });
btnRow.Add(new Button(() => Selection.activeObject = skill)
{ text = "在 Inspector 中打开" });
btnRow.Add(new Button(SkillHitBoxWizard.Open)
{ text = "HitBox Prefab 向导…" });
_detailRoot.Add(btnRow);
}
private void BuildCostPreview(FormSkillSO skill)
{
var box = new VisualElement();
box.AddToClassList("stats-preview");
void AddStat(string label, string value)
{
box.Add(new Label(label) { style = { marginRight = 4 } });
box.Add(new Label(value) { style = { marginRight = 16, unityFontStyleAndWeight = FontStyle.Bold } });
}
AddStat("消耗:", $"{skill.baseCost} {skill.resourceType}");
AddStat("冷却:", $"{skill.cooldown:F1}s");
AddStat("施放锁:", $"{skill.castLockDuration:F2}s");
_detailRoot.Add(box);
}
private void BuildHitBoxStatus(FormSkillSO skill)
{
// 投射物技能不需要近战 HitBox Prefab
if (skill.effectType == SkillEffectType.Projectile) return;
HelpBoxMessageType msgType;
string msg;
if (skill.SkillHitBoxPrefab == null)
{
msgType = HelpBoxMessageType.Warning;
msg = "SkillHitBoxPrefab 未赋值!近战/爆炸技能需要关联 HitBox Prefab。";
}
else if (skill.SkillHitBoxPrefab.GetComponent<SkillHitBoxInstance>() == null)
{
msgType = HelpBoxMessageType.Error;
msg = $"SkillHitBoxPrefab「{skill.SkillHitBoxPrefab.name}」缺少 SkillHitBoxInstance 组件!";
}
else
{
msgType = HelpBoxMessageType.Info;
msg = $"HitBox Prefab 结构正常:{skill.SkillHitBoxPrefab.name}";
}
_detailRoot.Add(new HelpBox(msg, msgType) { style = { marginBottom = 6 } });
}
// ── 新建技能 ──────────────────────────────────────────────────────────
private void CreateNewSkill()
{
var asset = EditorScaffoldUtils.CreateSOAsset<FormSkillSO>(
"Assets/_Game/Data/Progression/Skills", "SKL_New");
if (asset != null)
{
RefreshAll();
int idx = _filtered.IndexOf(asset);
if (idx >= 0)
_listView.SetSelection(idx);
}
}
}
}

View File

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