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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 22de97a32c867fd429c1814853d61ec6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user