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,334 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Animancer;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using BaseGames.Combat;
using BaseGames.Player;
namespace BaseGames.Editor.Combat
{
/// <summary>
/// 武器数据管理窗口W-02
/// 技术UI Toolkit TwoPaneSplitView。
/// 菜单BaseGames / Data / Weapon Editor
///
/// 左栏:可搜索的 WeaponSO 列表 + [新建] 按钮。
/// 右栏:选中武器的完整属性编辑 + HitBox Prefab 结构校验 + 快速操作。
/// </summary>
public class WeaponEditorWindow : EditorWindow
{
private static readonly StyleSheet _sharedUSS;
static WeaponEditorWindow()
{
_sharedUSS = AssetDatabase.LoadAssetAtPath<StyleSheet>(
"Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss");
}
[MenuItem("BaseGames/Data/Weapon Editor", priority = 100)]
public static void Open()
{
var wnd = GetWindow<WeaponEditorWindow>();
wnd.titleContent = new GUIContent("Weapon Editor");
wnd.minSize = new Vector2(680, 400);
}
// ── 状态 ─────────────────────────────────────────────────────────────
private List<WeaponSO> _weapons = new();
private List<WeaponSO> _filtered = new();
private ListView _listView;
private VisualElement _detailRoot;
private string _searchText = "";
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);
var btnCreate = new ToolbarButton(CreateNewWeapon) { text = "+ 新建武器" };
toolbar.Add(btnCreate);
var btnRefresh = new ToolbarButton(RefreshAll) { text = "↺" };
btnRefresh.tooltip = "重新扫描 Project 中的 WeaponSO 资产";
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()
{
_weapons = EditorScaffoldUtils.FindAllAssetsOfType<WeaponSO>();
_weapons.Sort((a, b) => string.Compare(
a.weaponId, b.weaponId, StringComparison.OrdinalIgnoreCase));
RefreshFilter();
}
private void RefreshFilter()
{
if (string.IsNullOrEmpty(_searchText))
{
_filtered = new List<WeaponSO>(_weapons);
}
else
{
string s = _searchText;
_filtered = _weapons.Where(w => w != null &&
(w.weaponId?.Contains(s, StringComparison.OrdinalIgnoreCase) == true ||
w.displayName?.Contains(s, StringComparison.OrdinalIgnoreCase) == true)).ToList();
}
_listView.itemsSource = _filtered;
_listView.Rebuild();
}
private static VisualElement MakeListItem()
{
var label = new Label();
label.AddToClassList("list-item");
return label;
}
private void BindListItem(VisualElement element, int index)
{
var label = (Label)element;
var weapon = _filtered.Count > index ? _filtered[index] : null;
if (weapon == null) { label.text = "(null)"; return; }
label.text = string.IsNullOrEmpty(weapon.displayName)
? weapon.weaponId
: $"{weapon.weaponId} <color=#888>({weapon.displayName})</color>";
}
// ── 详情面板 ──────────────────────────────────────────────────────────
private void OnSelectionChanged(IEnumerable<object> items)
{
_detailRoot.Clear();
_currentInspector = null;
var weapon = items.FirstOrDefault() as WeaponSO;
if (weapon == null) return;
// 标题
var title = new Label(
string.IsNullOrEmpty(weapon.displayName) ? weapon.weaponId : $"{weapon.weaponId} · {weapon.displayName}")
{
style =
{
fontSize = 14,
unityFontStyleAndWeight = FontStyle.Bold,
marginBottom = 6,
}
};
_detailRoot.Add(title);
// HitBox Prefab 状态
BuildHitBoxStatus(weapon);
// 连击链预览
BuildComboPreview(weapon);
// Inspector 完整属性编辑
_currentInspector = new InspectorElement(weapon);
_detailRoot.Add(_currentInspector);
// 操作按钮
var btnRow = new VisualElement();
btnRow.AddToClassList("action-buttons");
var btnSelect = new Button(() => EditorScaffoldUtils.PingAndSelect(weapon))
{ text = "在 Project 中定位" };
var btnInspector = new Button(() => Selection.activeObject = weapon)
{ text = "在 Inspector 中打开" };
var btnWizard = new Button(WeaponHitBoxWizard.Open)
{ text = "HitBox Prefab 向导…" };
btnRow.Add(btnSelect);
btnRow.Add(btnInspector);
btnRow.Add(btnWizard);
_detailRoot.Add(btnRow);
}
/// <summary>连击序列数值横排预览。</summary>
private void BuildComboPreview(WeaponSO weapon)
{
if (weapon.groundComboSteps == null || weapon.groundComboSteps.Length == 0)
return;
var section = new Label("连击链预览") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginBottom = 4 } };
_detailRoot.Add(section);
var chain = new VisualElement();
chain.AddToClassList("stats-preview");
for (int i = 0; i < weapon.groundComboSteps.Length; i++)
{
var step = weapon.groundComboSteps[i];
bool addArrow = i < weapon.groundComboSteps.Length - 1;
var cell = new VisualElement
{
style =
{
alignItems = Align.Center,
marginRight = 4,
paddingLeft = 6,
paddingRight = 6,
paddingTop = 3,
paddingBottom = 3,
backgroundColor = new Color(0.25f, 0.25f, 0.28f, 1f),
borderTopLeftRadius = 3,
borderTopRightRadius = 3,
borderBottomLeftRadius = 3,
borderBottomRightRadius = 3,
}
};
cell.Add(new Label($"攻击{i + 1}")
{
style = { fontSize = 10, color = new Color(0.65f, 0.65f, 0.65f) }
});
string clipName = step.clip?.Clip != null ? step.clip.Clip.name : "<无动画>";
cell.Add(new Label(clipName)
{
style = { fontSize = 11, unityFontStyleAndWeight = FontStyle.Bold }
});
if (step.damageSource != null)
{
int dmg = Mathf.RoundToInt(step.damageSource.BaseDamage * step.damageSource.DamageMultiplier);
cell.Add(new Label($"伤害 {dmg} [{step.damageSource.BreakLevel}]")
{
style = { fontSize = 10, color = new Color(1f, 0.7f, 0.3f) }
});
}
else
{
cell.Add(new Label("(无 DamageSource)")
{
style = { fontSize = 10, color = new Color(0.8f, 0.3f, 0.3f) }
});
}
chain.Add(cell);
if (addArrow)
chain.Add(new Label("→") { style = { alignSelf = Align.Center, marginLeft = 2, marginRight = 2 } });
}
_detailRoot.Add(chain);
// 追加空中/上/下攻击的简要行
var extraRow = new VisualElement
{
style = { flexDirection = FlexDirection.Row, flexWrap = Wrap.Wrap, marginBottom = 6, paddingLeft = 6 }
};
void ExtraStat(string label, DamageSourceSO src)
{
if (src == null) return;
int dmg = Mathf.RoundToInt(src.BaseDamage * src.DamageMultiplier);
extraRow.Add(new Label($"{label}{dmg} [{src.BreakLevel}]")
{
style = { marginRight = 14, fontSize = 11, color = new Color(0.7f, 0.7f, 0.7f) }
});
}
ExtraStat("空中", weapon.airComboSteps?[0].damageSource);
ExtraStat("上劈", weapon.upStep.damageSource);
ExtraStat("下劈", weapon.downStep.damageSource);
if (extraRow.childCount > 0)
_detailRoot.Add(extraRow);
}
private void BuildHitBoxStatus(WeaponSO weapon)
{
HelpBoxMessageType msgType;
string msg;
if (weapon.hitBoxPrefab == null)
{
msgType = HelpBoxMessageType.Warning;
msg = "hitBoxPrefab 未赋值!请创建并关联武器 HitBox Prefab。";
}
else if (weapon.hitBoxPrefab.GetComponent<WeaponHitBoxInstance>() == null)
{
msgType = HelpBoxMessageType.Error;
msg = $"hitBoxPrefab「{weapon.hitBoxPrefab.name}」缺少 WeaponHitBoxInstance 组件!";
}
else
{
msgType = HelpBoxMessageType.Info;
msg = $"HitBox Prefab 结构正常:{weapon.hitBoxPrefab.name}";
}
_detailRoot.Add(new HelpBox(msg, msgType) { style = { marginBottom = 6 } });
}
// ── 新建武器 ──────────────────────────────────────────────────────────
private void CreateNewWeapon()
{
var asset = EditorScaffoldUtils.CreateSOAsset<WeaponSO>(
"Assets/_Game/Data/Combat/Weapons", "WPN_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: 7cc9d2828e2d3f9458e74befbb0e2b4e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: