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