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

@@ -10,7 +10,7 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d2443d04d1c179d4d8a4f36e7ca7156e, type: 3}
m_Name: Weapon_DiHun
m_Name: WPN_New
m_EditorClassIdentifier:
weaponId:
displayName:

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 76de8c0ba36dcce4db8990c5b62e9ed8
guid: ce7bc9bad6f58ec42baff08f5353340e
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000

View File

@@ -0,0 +1,28 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: ed4391dfa14c0304c8932f1ef9f8ce63, type: 3}
m_Name: ENM_New_Stats
m_EditorClassIdentifier:
MaxHP: 50
Defense: 0
WalkSpeed: 2
RunSpeed: 4
AttackDamage: 10
AttackRange: 1.5
AttackCooldown: 1
DetectRange: 6
KnockbackForce: 5
HitStunDuration: 0.3
EyeOffset: {x: 0, y: 0.8}
LOSBlockingMask:
serializedVersion: 2
m_Bits: 1

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: f80486c9cd3d1db459ce6df915b11546
guid: e0cf93e053ead744fa1876771ba0d081
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000

View File

@@ -0,0 +1,49 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: de92221c7c3fb4a42a7cd122a8f97632, type: 3}
m_Name: SKL_Boss_New
m_EditorClassIdentifier:
skillId:
displayName:
designNote:
category: 0
skillType: 0
availablePhaseIndices:
attackPatterns: []
vulnerabilityWindows: []
interactionTags: 0
sequenceOnHit: {fileID: 0}
sequenceOnMiss: {fileID: 0}
counterResponses: []
arenaEvents: []
resourceCost:
resourceId:
cost: 0
minRequired: 0
buildsRage: 0
poiseWindow:
Level: 0
NormalizedStart: 0
NormalizedEnd: 0
skillAnimation:
_FadeDuration: 0.25
_Speed: 1
_Events:
_NormalizedTimes: []
_Callbacks: []
_Names: []
_Clip: {fileID: 0}
_NormalizedStartTime: NaN
cooldown: 0
references:
version: 2
RefIds: []

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: d87ae01ed8a2b4f4cb27d159a52d1a14
guid: a5d737a5b9641124aafb375d8684e06a
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000

View File

@@ -1,87 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d2443d04d1c179d4d8a4f36e7ca7156e, type: 3}
m_Name: Weapon_MingHun
m_EditorClassIdentifier:
weaponId:
displayName:
icon: {fileID: 0}
weaponType: 0
attack1Clip:
_FadeDuration: 0.25
_Speed: 1
_Events:
_NormalizedTimes: []
_Callbacks: []
_Names: []
_Clip: {fileID: 0}
_NormalizedStartTime: NaN
attack2Clip:
_FadeDuration: 0.25
_Speed: 1
_Events:
_NormalizedTimes: []
_Callbacks: []
_Names: []
_Clip: {fileID: 0}
_NormalizedStartTime: NaN
attack3Clip:
_FadeDuration: 0.25
_Speed: 1
_Events:
_NormalizedTimes: []
_Callbacks: []
_Names: []
_Clip: {fileID: 0}
_NormalizedStartTime: NaN
airAttackClip:
_FadeDuration: 0.25
_Speed: 1
_Events:
_NormalizedTimes: []
_Callbacks: []
_Names: []
_Clip: {fileID: 0}
_NormalizedStartTime: NaN
upAttackClip:
_FadeDuration: 0.25
_Speed: 1
_Events:
_NormalizedTimes: []
_Callbacks: []
_Names: []
_Clip: {fileID: 0}
_NormalizedStartTime: NaN
downAttackClip:
_FadeDuration: 0.25
_Speed: 1
_Events:
_NormalizedTimes: []
_Callbacks: []
_Names: []
_Clip: {fileID: 0}
_NormalizedStartTime: NaN
attack1Source: {fileID: 0}
attack2Source: {fileID: 0}
attack3Source: {fileID: 0}
airAttackSource: {fileID: 0}
upAttackSource: {fileID: 0}
downAttackSource: {fileID: 0}
hitBoxPrefab: {fileID: 0}
vfxConfig:
onEquipPresetId:
weaponTrailPrefab: {fileID: 0}
trailColor: {r: 1, g: 1, b: 1, a: 1}
soulPowerGain: 10
references:
version: 2
RefIds: []

View File

@@ -1,87 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d2443d04d1c179d4d8a4f36e7ca7156e, type: 3}
m_Name: Weapon_TianHun
m_EditorClassIdentifier:
weaponId:
displayName:
icon: {fileID: 0}
weaponType: 0
attack1Clip:
_FadeDuration: 0.25
_Speed: 1
_Events:
_NormalizedTimes: []
_Callbacks: []
_Names: []
_Clip: {fileID: 0}
_NormalizedStartTime: NaN
attack2Clip:
_FadeDuration: 0.25
_Speed: 1
_Events:
_NormalizedTimes: []
_Callbacks: []
_Names: []
_Clip: {fileID: 0}
_NormalizedStartTime: NaN
attack3Clip:
_FadeDuration: 0.25
_Speed: 1
_Events:
_NormalizedTimes: []
_Callbacks: []
_Names: []
_Clip: {fileID: 0}
_NormalizedStartTime: NaN
airAttackClip:
_FadeDuration: 0.25
_Speed: 1
_Events:
_NormalizedTimes: []
_Callbacks: []
_Names: []
_Clip: {fileID: 0}
_NormalizedStartTime: NaN
upAttackClip:
_FadeDuration: 0.25
_Speed: 1
_Events:
_NormalizedTimes: []
_Callbacks: []
_Names: []
_Clip: {fileID: 0}
_NormalizedStartTime: NaN
downAttackClip:
_FadeDuration: 0.25
_Speed: 1
_Events:
_NormalizedTimes: []
_Callbacks: []
_Names: []
_Clip: {fileID: 0}
_NormalizedStartTime: NaN
attack1Source: {fileID: 0}
attack2Source: {fileID: 0}
attack3Source: {fileID: 0}
airAttackSource: {fileID: 0}
upAttackSource: {fileID: 0}
downAttackSource: {fileID: 0}
hitBoxPrefab: {fileID: 0}
vfxConfig:
onEquipPresetId:
weaponTrailPrefab: {fileID: 0}
trailColor: {r: 1, g: 1, b: 1, a: 1}
soulPowerGain: 10
references:
version: 2
RefIds: []

View File

@@ -0,0 +1,46 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a96f0270221c8444b8719f0f9b14c635, type: 3}
m_Name: SKL_New
m_EditorClassIdentifier:
skillId:
displayNameKey:
descriptionKey:
icon: {fileID: 0}
resourceType: 1
baseCost: 0
cooldown: 0
castAnimation:
_FadeDuration: 0.25
_Speed: 1
_Events:
_NormalizedTimes: []
_Callbacks: []
_Names: []
_Clip: {fileID: 0}
_NormalizedStartTime: NaN
castLockDuration: 0
effectType: 1
damageSource: {fileID: 0}
projectileConfig: {fileID: 0}
isHoming: 0
holdForContinuous: 0
dashForce: 0
dashDuration: 0
isInvincibleDuringDash: 0
explosionDelay: 0
explosionRadius: 0
castFeedback: {fileID: 0}
SkillHitBoxPrefab: {fileID: 0}
references:
version: 2
RefIds: []

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d00e0d6104f281345b8978d3a72eed13
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -2400,7 +2400,7 @@ Transform:
m_GameObject: {fileID: 123526430}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalPosition: {x: 0, y: -1.94, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -3885,7 +3885,7 @@ Transform:
m_GameObject: {fileID: 173935938}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalPosition: {x: 0, y: 0.93, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -5123,7 +5123,7 @@ Transform:
m_GameObject: {fileID: 225562483}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalPosition: {x: 2.1, y: 2.25, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -8598,6 +8598,7 @@ MonoBehaviour:
_onCharmEquipped: {fileID: 0}
_onCharmUnequipped: {fileID: 0}
_onEquipmentChanged: {fileID: 0}
_onAchievementNotchGranted: {fileID: 0}
--- !u!114 &430284915
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -18864,7 +18865,7 @@ Transform:
m_GameObject: {fileID: 1108462349}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalPosition: {x: 1.72, y: -1.15, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -18895,7 +18896,7 @@ Transform:
m_GameObject: {fileID: 1112393102}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalPosition: {x: 1.09, y: 0.6, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -21066,7 +21067,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
_linkedDoor: {fileID: 1167097821}
_spawnPoint: {fileID: 1655723310}
_facingDirectionOnArrive: 1
_facingDirectionOnArrive: -1
_autoTrigger: 1
_transitionOut: {fileID: 0}
_transitionIn: {fileID: 0}
@@ -27623,7 +27624,7 @@ Transform:
m_GameObject: {fileID: 1655723309}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalPosition: {x: -1.26, y: -1.09, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []

View File

@@ -182,9 +182,7 @@ namespace BaseGames.Editor
root.Add(MakeSectionHeader("▶ 专项编辑器"));
var jumpGroup = MakeActionGroup();
jumpGroup.Add(MakeJumpButton("武器编辑器", () => Combat.WeaponEditorWindow.Open()));
jumpGroup.Add(MakeJumpButton("技能编辑器", () => Skills.SkillEditorWindow.Open()));
jumpGroup.Add(MakeJumpButton("形态编辑器", () => FormEditorWindow.Open()));
jumpGroup.Add(MakeJumpButton("Data Hub武器/技能/形态)", DataHubWindow.Open));
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
root.Add(jumpGroup);
@@ -248,7 +246,7 @@ namespace BaseGames.Editor
root.Add(MakeSectionHeader("▶ 专项编辑器"));
var jumpGroup = MakeActionGroup();
jumpGroup.Add(MakeJumpButton("敌人数据管理", () => Enemies.EnemyDataWindow.Open()));
jumpGroup.Add(MakeJumpButton("Data Hub敌人数据", DataHubWindow.Open));
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
root.Add(jumpGroup);
@@ -295,7 +293,7 @@ namespace BaseGames.Editor
var jumpGroup = MakeActionGroup();
jumpGroup.Add(MakeJumpButton("Boss 技能序列查看器", BossSkillSequenceWindow.OpenWindow));
jumpGroup.Add(MakeJumpButton("敌人数据管理", () => Enemies.EnemyDataWindow.Open()));
jumpGroup.Add(MakeJumpButton("Data HubBoss技能", DataHubWindow.Open));
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
root.Add(jumpGroup);
@@ -763,7 +761,7 @@ namespace BaseGames.Editor
{
var sep = new VisualElement();
sep.style.height = 1;
sep.style.backgroundColor = new Color(0.3f, 0.3f, 0.3f, 0.6f);
sep.style.backgroundColor = new Color(0.5f, 0.5f, 0.5f, 0.25f);
sep.style.marginTop = 8;
sep.style.marginBottom = 8;
return sep;

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,347 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using BaseGames.Enemies;
namespace BaseGames.Editor.Enemies
{
/// <summary>
/// 敌人数据管理窗口W-05
/// 技术UI Toolkit TwoPaneSplitView + 手动标签页。
/// 菜单BaseGames / Data / Enemy Data Manager
///
/// 左栏:可搜索的 EnemyStatsSO 列表 + [新建] 按钮。
/// 右栏两个标签页:
/// Stats — EnemyStatsSO 完整属性编辑
/// Loot — LootTableSO 浏览与编辑
/// </summary>
public class EnemyDataWindow : EditorWindow
{
private static readonly StyleSheet _sharedUSS;
static EnemyDataWindow()
{
_sharedUSS = AssetDatabase.LoadAssetAtPath<StyleSheet>(
"Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss");
}
[MenuItem("BaseGames/Data/Enemy Data Manager", priority = 102)]
public static void Open()
{
var wnd = GetWindow<EnemyDataWindow>();
wnd.titleContent = new GUIContent("Enemy Data Manager");
wnd.minSize = new Vector2(720, 420);
}
// ── 状态 ─────────────────────────────────────────────────────────────
private List<EnemyStatsSO> _enemies = new();
private List<EnemyStatsSO> _filtered = new();
private List<LootTableSO> _lootTables = new();
private List<LootTableSO> _lootFiltered = new();
private ListView _enemyList;
private ListView _lootList;
private VisualElement _detailRoot; // Stats 标签页 Loot 详情区
private ScrollView _lootDetailRoot; // Loot 标签页 LootTable 详情区
private VisualElement _tabStats;
private VisualElement _tabLoot;
private Button _btnStats;
private Button _btnLoot;
private string _searchText = "";
private string _lootSearchText = "";
private int _activeTab = 0; // 0=Stats, 1=Loot
private InspectorElement _statsInspector;
private InspectorElement _lootInspector;
// ── 生命周期 ──────────────────────────────────────────────────────────
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; RefreshEnemyFilter(); });
searchField.tooltip = "按名称 / ID 过滤 EnemyStatsSO 列表";
toolbar.Add(searchField);
var btnCreate = new ToolbarButton(CreateNewEnemyStats) { text = "+ 新建敌人" };
var btnRefresh = new ToolbarButton(RefreshAll) { text = "↺" };
btnRefresh.tooltip = "重新扫描 Project 中的资产";
toolbar.Add(btnCreate);
toolbar.Add(btnRefresh);
rootVisualElement.Add(toolbar);
// Split view
var split = new TwoPaneSplitView(0, 230, TwoPaneSplitViewOrientation.Horizontal);
// ── 左栏:敌人列表 ────────────────────────────────────────────
var leftPane = new VisualElement { style = { minWidth = 150 } };
_enemyList = new ListView
{
selectionType = SelectionType.Single,
fixedItemHeight = 22,
makeItem = MakeEnemyItem,
bindItem = BindEnemyItem,
style = { flexGrow = 1 },
};
_enemyList.selectionChanged += OnEnemySelected;
leftPane.Add(_enemyList);
split.Add(leftPane);
// ── 右栏:标签页 + 内容 ───────────────────────────────────────
var rightPane = new VisualElement { style = { flexGrow = 1 } };
// 标签页按钮栏
var tabBar = new VisualElement();
tabBar.AddToClassList("tab-bar");
_btnStats = new Button(() => ActivateTab(0)) { text = "Stats" };
_btnLoot = new Button(() => ActivateTab(1)) { text = "Loot Table" };
_btnStats.AddToClassList("tab-button");
_btnLoot.AddToClassList("tab-button");
tabBar.Add(_btnStats);
tabBar.Add(_btnLoot);
rightPane.Add(tabBar);
// Stats 面板
_tabStats = new ScrollView { style = { flexGrow = 1 } };
_tabStats.AddToClassList("detail-panel");
rightPane.Add(_tabStats);
// Loot 面板(初始隐藏)
_tabLoot = BuildLootPanel();
_tabLoot.style.display = DisplayStyle.None;
rightPane.Add(_tabLoot);
split.Add(rightPane);
rootVisualElement.Add(split);
ActivateTab(0);
RefreshAll();
}
private void OnFocus() => RefreshAll();
// ── 标签页切换 ────────────────────────────────────────────────────────
private void ActivateTab(int tab)
{
_activeTab = tab;
_tabStats.style.display = tab == 0 ? DisplayStyle.Flex : DisplayStyle.None;
_tabLoot.style.display = tab == 1 ? DisplayStyle.Flex : DisplayStyle.None;
_btnStats.EnableInClassList("tab-button--active", tab == 0);
_btnLoot.EnableInClassList("tab-button--active", tab == 1);
}
// ── 敌人列表 ──────────────────────────────────────────────────────────
private void RefreshAll()
{
_enemies = EditorScaffoldUtils.FindAllAssetsOfType<EnemyStatsSO>();
_enemies.Sort((a, b) => string.Compare(a.name, b.name, StringComparison.OrdinalIgnoreCase));
_lootTables = EditorScaffoldUtils.FindAllAssetsOfType<LootTableSO>();
_lootTables.Sort((a, b) => string.Compare(a.name, b.name, StringComparison.OrdinalIgnoreCase));
RefreshEnemyFilter();
RefreshLootFilter();
}
private void RefreshEnemyFilter()
{
_filtered = string.IsNullOrEmpty(_searchText)
? new List<EnemyStatsSO>(_enemies)
: _enemies.Where(e => e != null &&
e.name.Contains(_searchText, StringComparison.OrdinalIgnoreCase)).ToList();
_enemyList.itemsSource = _filtered;
_enemyList.Rebuild();
}
private static VisualElement MakeEnemyItem()
{
var label = new Label();
label.AddToClassList("list-item");
return label;
}
private void BindEnemyItem(VisualElement element, int index)
{
var label = (Label)element;
var enemy = _filtered.Count > index ? _filtered[index] : null;
label.text = enemy != null ? enemy.name : "(null)";
}
private void OnEnemySelected(IEnumerable<object> items)
{
_tabStats.Clear();
_statsInspector = null;
var enemy = items.FirstOrDefault() as EnemyStatsSO;
if (enemy == null) return;
// 数值快览条
BuildStatsPreview(enemy);
// 完整属性编辑
_statsInspector = new InspectorElement(enemy);
_tabStats.Add(_statsInspector);
// 操作按钮
var btnRow = new VisualElement();
btnRow.AddToClassList("action-buttons");
btnRow.Add(new Button(() => EditorScaffoldUtils.PingAndSelect(enemy)) { text = "在 Project 中定位" });
btnRow.Add(new Button(() => Selection.activeObject = enemy) { text = "在 Inspector 中打开" });
btnRow.Add(new Button(() => CloneEnemy(enemy)) { text = "克隆为变体…" });
_tabStats.Add(btnRow);
}
private void BuildStatsPreview(EnemyStatsSO e)
{
var row = new VisualElement();
row.AddToClassList("stats-preview");
void Stat(string label, string val)
{
row.Add(new Label(label) { style = { color = new Color(0.65f, 0.65f, 0.65f), marginRight = 3 } });
row.Add(new Label(val) { style = { marginRight = 14, unityFontStyleAndWeight = FontStyle.Bold } });
}
Stat("HP", $"{e.MaxHP}");
Stat("DEF", $"{e.Defense}");
Stat("ATK", $"{e.AttackDamage}");
Stat("SPD", $"{e.WalkSpeed}/{e.RunSpeed}");
Stat("范围:", $"{e.AttackRange:F1}");
Stat("视野:", $"{e.DetectRange:F1}");
_tabStats.Add(row);
}
private void CloneEnemy(EnemyStatsSO source)
{
string name = source.name;
string clone = EditorUtility.SaveFilePanelInProject(
"克隆敌人配置", $"{name}_Clone", "asset",
"选择克隆 EnemyStatsSO 的保存路径");
if (string.IsNullOrEmpty(clone)) return;
var asset = Instantiate(source);
AssetDatabase.CreateAsset(asset, clone);
AssetDatabase.SaveAssets();
EditorScaffoldUtils.PingAndSelect(asset);
RefreshAll();
}
// ── Loot Table 面板 ───────────────────────────────────────────────────
private VisualElement BuildLootPanel()
{
var container = new VisualElement { style = { flexGrow = 1 } };
// Loot 搜索栏
var lootToolbar = new Toolbar();
var lootSearch = new ToolbarSearchField { style = { flexGrow = 1 } };
lootSearch.RegisterValueChangedCallback(e => { _lootSearchText = e.newValue; RefreshLootFilter(); });
lootSearch.tooltip = "过滤 LootTableSO 列表";
lootToolbar.Add(lootSearch);
var btnCreateLoot = new ToolbarButton(CreateNewLootTable) { text = "+ 新建 LootTable" };
lootToolbar.Add(btnCreateLoot);
container.Add(lootToolbar);
// 左右分割Loot 列表 + Loot 详情
var lootSplit = new TwoPaneSplitView(0, 200, TwoPaneSplitViewOrientation.Horizontal);
var lootLeft = new VisualElement { style = { minWidth = 120 } };
_lootList = new ListView
{
selectionType = SelectionType.Single,
fixedItemHeight = 22,
makeItem = () => { var l = new Label(); l.AddToClassList("list-item"); return l; },
bindItem = (el, idx) =>
{
var lbl = (Label)el;
var loot = _lootFiltered.Count > idx ? _lootFiltered[idx] : null;
lbl.text = loot?.name ?? "(null)";
},
style = { flexGrow = 1 },
};
_lootList.selectionChanged += OnLootSelected;
lootLeft.Add(_lootList);
lootSplit.Add(lootLeft);
_lootDetailRoot = new ScrollView { style = { flexGrow = 1 } };
_lootDetailRoot.AddToClassList("detail-panel");
lootSplit.Add(_lootDetailRoot);
container.Add(lootSplit);
return container;
}
private void RefreshLootFilter()
{
_lootFiltered = string.IsNullOrEmpty(_lootSearchText)
? new List<LootTableSO>(_lootTables)
: _lootTables.Where(l => l != null &&
l.name.Contains(_lootSearchText, StringComparison.OrdinalIgnoreCase)).ToList();
_lootList.itemsSource = _lootFiltered;
_lootList.Rebuild();
}
private void OnLootSelected(IEnumerable<object> items)
{
_lootDetailRoot.Clear();
_lootInspector = null;
var loot = items.FirstOrDefault() as LootTableSO;
if (loot == null) return;
var title = new Label($"Loot{loot.name}")
{
style = { fontSize = 13, unityFontStyleAndWeight = FontStyle.Bold, marginBottom = 6 }
};
_lootDetailRoot.Add(title);
// 简要统计
int entryCount = loot.Entries?.Length ?? 0;
_lootDetailRoot.Add(new Label($"条目数:{entryCount} 保底 LingZhu{loot.GuaranteedLingZhuMin}{loot.GuaranteedLingZhuMax}")
{
style = { color = new Color(0.7f, 0.7f, 0.7f), marginBottom = 4 }
});
_lootInspector = new InspectorElement(loot);
_lootDetailRoot.Add(_lootInspector);
var btnRow = new VisualElement();
btnRow.AddToClassList("action-buttons");
btnRow.Add(new Button(() => EditorScaffoldUtils.PingAndSelect(loot)) { text = "在 Project 中定位" });
btnRow.Add(new Button(() => Selection.activeObject = loot) { text = "在 Inspector 中打开" });
_lootDetailRoot.Add(btnRow);
}
// ── 新建资产 ──────────────────────────────────────────────────────────
private void CreateNewEnemyStats()
{
var asset = EditorScaffoldUtils.CreateSOAsset<EnemyStatsSO>(
"Assets/_Game/Data/Enemies", "ENM_New_Stats");
if (asset != null) RefreshAll();
}
private void CreateNewLootTable()
{
var asset = EditorScaffoldUtils.CreateSOAsset<LootTableSO>(
"Assets/_Game/Data/Enemies", "ENM_New_Loot");
if (asset != null) RefreshAll();
}
}
}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 54a83daf1b31e4c4e98beff7506eecb2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,262 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using BaseGames.Editor.Modules;
namespace BaseGames.Editor
{
/// <summary>
/// 数据管理总枢纽窗口DataHub
/// 布局导航侧边栏120px | TwoPaneSplitView → 列表区220px + 详情区flex
/// 菜单BaseGames / Data Hub (priority=50)
/// </summary>
public class DataHubWindow : EditorWindow
{
private const string UssPath = "Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss";
private const string PrefKey = "DataHub.ActiveModuleId";
private const float NavWidth = 120f;
private const float ListWidth = 220f;
private const float MinWinWidth = 680f;
private const float MinWinHeight = 420f;
[MenuItem("BaseGames/Data Hub", priority = 50)]
public static void Open()
{
var wnd = GetWindow<DataHubWindow>();
wnd.titleContent = new GUIContent("Data Hub", EditorGUIUtility.IconContent("d_ScriptableObject Icon").image);
wnd.minSize = new Vector2(MinWinWidth, MinWinHeight);
}
// ── 状态 ─────────────────────────────────────────────────────────────
private readonly List<IDataModule> _modules = new();
private readonly HashSet<string> _initializedIds = new();
private IDataModule _activeModule;
private VisualElement _navSidebar;
// 缓存:列表区和详情区引用(由 TwoPaneSplitView 子节点提供)
private VisualElement _listWrapper;
private VisualElement _detailWrapper;
// 当前选中资产
private UnityEngine.Object _selected;
// ── 生命周期 ──────────────────────────────────────────────────────────
public void CreateGUI()
{
// USS
var uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
if (uss != null) rootVisualElement.styleSheets.Add(uss);
// 注册模块
RegisterModules();
// 构建 UI
BuildLayout();
// 恢复上次激活的模块
string savedId = EditorPrefs.GetString(PrefKey, string.Empty);
var toActivate = _modules.Find(m => m.ModuleId == savedId) ?? _modules.FirstOrDefault();
if (toActivate != null) ActivateModule(toActivate);
}
// ── 模块注册 ──────────────────────────────────────────────────────────
private void RegisterModules()
{
_modules.Clear();
_modules.Add(new WeaponModule());
_modules.Add(new SkillModule());
_modules.Add(new EnemyModule());
_modules.Add(new FormModule());
_modules.Add(new BossSkillModule());
}
// ── 布局 ─────────────────────────────────────────────────────────────
private void BuildLayout()
{
var root = rootVisualElement;
root.style.flexDirection = FlexDirection.Row;
root.style.flexGrow = 1;
// 导航侧边栏
_navSidebar = BuildNavSidebar();
root.Add(_navSidebar);
// 垂直分隔线
var divider = new VisualElement();
divider.style.width = 1;
divider.style.backgroundColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.25f));
root.Add(divider);
// TwoPaneSplitView列表 + 详情)
var split = new TwoPaneSplitView(0, ListWidth, TwoPaneSplitViewOrientation.Horizontal);
split.style.flexGrow = 1;
root.Add(split);
// 列表区容器
_listWrapper = new VisualElement();
_listWrapper.style.flexGrow = 1;
split.Add(_listWrapper);
// 详情区容器
_detailWrapper = new VisualElement();
_detailWrapper.style.flexGrow = 1;
split.Add(_detailWrapper);
}
private VisualElement BuildNavSidebar()
{
var sidebar = new VisualElement();
sidebar.style.width = NavWidth;
sidebar.style.flexShrink = 0;
sidebar.style.flexDirection = FlexDirection.Column;
sidebar.style.paddingTop = 8;
// 标题
var title = new Label("DATA HUB");
title.style.fontSize = 10;
title.style.opacity = 0.5f;
title.style.paddingLeft = 10;
title.style.marginBottom = 6;
title.style.unityFontStyleAndWeight = FontStyle.Bold;
sidebar.Add(title);
foreach (var module in _modules)
{
var btn = BuildNavItem(module);
sidebar.Add(btn);
}
// 弹性填充
var spacer = new VisualElement();
spacer.style.flexGrow = 1;
sidebar.Add(spacer);
return sidebar;
}
private Button BuildNavItem(IDataModule module)
{
var btn = new Button(() => ActivateModule(module));
btn.name = "nav-" + module.ModuleId;
btn.style.flexDirection = FlexDirection.Row;
btn.style.alignItems = Align.Center;
btn.style.paddingLeft = 10;
btn.style.paddingRight = 8;
btn.style.paddingTop = 8;
btn.style.paddingBottom = 8;
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.style.marginBottom = 2;
// 图标
if (!string.IsNullOrEmpty(module.IconName))
{
var icon = new Image { image = EditorGUIUtility.IconContent(module.IconName).image };
icon.style.width = 16;
icon.style.height = 16;
icon.style.marginRight = 6;
btn.Add(icon);
}
var label = new Label(module.DisplayName);
label.style.flexGrow = 1;
btn.Add(label);
return btn;
}
// ── 模块切换 ──────────────────────────────────────────────────────────
private void ActivateModule(IDataModule module)
{
if (_activeModule == module) return;
_activeModule = module;
_selected = null;
// 更新导航项视觉状态
foreach (var m in _modules)
{
var navBtn = _navSidebar.Q<Button>("nav-" + m.ModuleId);
if (navBtn == null) continue;
if (m == module)
{
navBtn.style.backgroundColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.18f));
navBtn.style.borderLeftWidth = 3;
navBtn.style.borderLeftColor = new StyleColor(new Color(0.4f, 0.65f, 1f, 1f));
}
else
{
navBtn.style.backgroundColor = new StyleColor(Color.clear);
navBtn.style.borderLeftWidth = 0;
}
}
// 初始化模块(首次激活时调用一次)
if (!_initializedIds.Contains(module.ModuleId))
{
_initializedIds.Add(module.ModuleId);
module.Initialize();
}
module.OnActivated();
// 重建列表区
_listWrapper.Clear();
module.BuildListPane(_listWrapper, OnModuleSelected);
// 清空详情区
RebuildDetailPane(null);
EditorPrefs.SetString(PrefKey, module.ModuleId);
}
private void OnModuleSelected(UnityEngine.Object selected)
{
_selected = selected;
RebuildDetailPane(selected);
}
private void RebuildDetailPane(UnityEngine.Object selected)
{
_detailWrapper.Clear();
if (_activeModule == null) return;
if (selected == null)
{
var placeholder = new Label("← 从左侧列表选择一项");
placeholder.style.opacity = 0.45f;
placeholder.style.marginTop = 60;
placeholder.style.unityTextAlign = TextAnchor.MiddleCenter;
_detailWrapper.Add(placeholder);
return;
}
_activeModule.BuildDetailPane(_detailWrapper, selected);
}
// ── 公共辅助(供 Module 回调使用)────────────────────────────────────
/// <summary>通知 Hub 已完成重命名,需要刷新详情区标题。</summary>
public void NotifyRenamed(UnityEngine.Object asset)
{
if (_activeModule == null || asset == null) return;
RebuildDetailPane(asset);
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 3a95bf3e8be76e44881b0efa6a42f753
guid: 95a89dac2a3cc7e439be075586617c88
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -0,0 +1,28 @@
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace BaseGames.Editor
{
/// <summary>
/// 数据模块接口 —— DataHubWindow 中每个资产管理标签页实现此接口。
/// </summary>
public interface IDataModule
{
string ModuleId { get; } // 持久化 EditorPrefs 用唯一 key
string DisplayName { get; } // 导航侧边栏显示名称
string IconName { get; } // Unity 内置图标名 or null
/// <summary>初始化模块,加载数据(首次激活时调用一次)。</summary>
void Initialize();
/// <summary>构建列表区内容onSelected 在选中资产时由模块调用。</summary>
void BuildListPane(VisualElement container, Action<UnityEngine.Object> onSelected);
/// <summary>构建详情区内容selected 为当前选中资产(可为 null。</summary>
void BuildDetailPane(VisualElement container, UnityEngine.Object selected);
/// <summary>切换到本模块时调用,可用于刷新数据。</summary>
void OnActivated();
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 15618e4fc32a98346a68e945428fcb47
guid: 2cd7579b2889e0943883000232e468dc
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 231b80ce7e59248449a7431b00a05b59
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

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

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 7cc9d2828e2d3f9458e74befbb0e2b4e
guid: f0d0425e529293e469da3762fe3bf8f0
MonoImporter:
externalObjects: {}
serializedVersion: 2

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

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 22de97a32c867fd429c1814853d61ec6
guid: 32772b7d7bbc5824889620b773c352a8
MonoImporter:
externalObjects: {}
serializedVersion: 2

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:

View File

@@ -1,415 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using BaseGames.Player;
namespace BaseGames.Editor
{
/// <summary>
/// 形态系统可视化编辑器W-06
/// 技术UI Toolkit TwoPaneSplitView + 三列形态网格。
/// 菜单BaseGames / Data / Form Editor
///
/// 左栏FormConfigSO 列表 + [新建] 按钮。
/// 右栏:
/// · 三列网格(天魂 / 地魂 / 命魂),每列显示对应 FormSO 详情及武器引用。
/// · 各列可独立新建/重新绑定 FormSO。
/// · "一键自动填充" 按钮:按 formType 枚举在 Project 中搜索已有 FormSO 并赋值。
/// · 底部:在 Project 中选中 FormConfigSO / 在 Inspector 中编辑 原始字段。
/// </summary>
public class FormEditorWindow : EditorWindow
{
// ── 常量 ──────────────────────────────────────────────────────────────
private const string UssPath = "Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss";
private const string DataRoot = "Assets/_Game/Data/Player/Forms";
private const string WeaponDir = "Assets/_Game/Data/Combat/Weapons";
private static readonly StyleSheet _uss;
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.55f, 0.85f, 0.40f)),
(FormType.MingHun, "命魂", new Color(0.80f, 0.30f, 0.30f)),
};
static FormEditorWindow()
{
_uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
}
[MenuItem("BaseGames/Data/Form Editor", priority = 103)]
public static void Open()
{
var wnd = GetWindow<FormEditorWindow>();
wnd.titleContent = new GUIContent("Form Editor");
wnd.minSize = new Vector2(760, 420);
}
// ── 状态 ──────────────────────────────────────────────────────────────
private List<FormConfigSO> _configs = new();
private List<FormConfigSO> _filtered = new();
private FormConfigSO _selected;
private string _searchText = "";
private ListView _configList;
private VisualElement _detailRoot;
private InspectorElement _rawInspector;
// 三列形态格(每列一个 FormSO ObjectField
private readonly ObjectField[] _formFields = new ObjectField[3];
private readonly ObjectField[] _weaponFields = new ObjectField[3];
private readonly VisualElement[]_columnRoots = new VisualElement[3];
// ── 生命周期 ──────────────────────────────────────────────────────────
public void CreateGUI()
{
if (_uss != null)
rootVisualElement.styleSheets.Add(_uss);
// ── Toolbar ───────────────────────────────────────────────────────
var toolbar = new Toolbar();
var search = new ToolbarSearchField { style = { flexGrow = 1 } };
search.RegisterValueChangedCallback(e => { _searchText = e.newValue; RefreshFilter(); });
search.tooltip = "按名称过滤 FormConfigSO";
toolbar.Add(search);
var btnNew = new ToolbarButton(CreateNewFormConfig) { text = "+ 新建 FormConfig" };
var btnRefresh = new ToolbarButton(RefreshAll) { text = "↺" };
btnRefresh.tooltip = "重新扫描 Project 中的 FormConfigSO 资产";
toolbar.Add(btnNew);
toolbar.Add(btnRefresh);
rootVisualElement.Add(toolbar);
// ── 分栏 ──────────────────────────────────────────────────────────
var split = new TwoPaneSplitView(0, 200, TwoPaneSplitViewOrientation.Horizontal);
// 左栏
var leftPane = new VisualElement { style = { minWidth = 140 } };
_configList = new ListView
{
selectionType = SelectionType.Single,
makeItem = MakeListItem,
bindItem = BindListItem,
style = { flexGrow = 1 },
showAlternatingRowBackgrounds = AlternatingRowBackground.ContentOnly,
};
_configList.selectionChanged += OnConfigSelected;
leftPane.Add(_configList);
split.Add(leftPane);
// 右栏ScrollView
_detailRoot = new ScrollView(ScrollViewMode.Vertical) { style = { flexGrow = 1 } };
_detailRoot.contentContainer.style.paddingLeft = 10;
_detailRoot.contentContainer.style.paddingRight = 10;
_detailRoot.contentContainer.style.paddingTop = 10;
_detailRoot.contentContainer.style.paddingBottom = 10;
_detailRoot.Add(new HelpBox("← 在左侧选择一个 FormConfigSO 开始编辑", HelpBoxMessageType.Info));
split.Add(_detailRoot);
rootVisualElement.Add(split);
RefreshAll();
}
// ── 列表 ──────────────────────────────────────────────────────────────
private VisualElement MakeListItem()
{
var label = new Label();
label.AddToClassList("list-item");
return label;
}
private void BindListItem(VisualElement ve, int idx)
{
if (ve is Label lbl && idx < _filtered.Count)
lbl.text = _filtered[idx].name;
}
private void OnConfigSelected(IEnumerable<object> objs)
{
_selected = objs.FirstOrDefault() as FormConfigSO;
RebuildDetail();
}
private void RefreshFilter()
{
string s = _searchText.ToLowerInvariant();
_filtered = string.IsNullOrEmpty(s)
? new List<FormConfigSO>(_configs)
: _configs.Where(c => c.name.ToLowerInvariant().Contains(s)).ToList();
_configList.itemsSource = _filtered;
_configList.Rebuild();
}
private void RefreshAll()
{
_configs = EditorScaffoldUtils.FindAllAssetsOfType<FormConfigSO>();
_configs.Sort((a, b) => string.Compare(a.name, b.name, System.StringComparison.Ordinal));
RefreshFilter();
}
// ── 右栏详情 ──────────────────────────────────────────────────────────
private void RebuildDetail()
{
_detailRoot.Clear();
if (_selected == null) return;
// 标题 + 快捷操作
var header = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginBottom = 8 } };
header.Add(new Label(_selected.name) { style = { fontSize = 14, unityFontStyleAndWeight = FontStyle.Bold, flexGrow = 1 } });
var btnPing = new Button(() => EditorGUIUtility.PingObject(_selected)) { text = "⌖ Ping" };
var btnAuto = new Button(AutoFillForms) { text = "自动填充形态", tooltip = "按 formType 枚举在 Project 中搜索已有 FormSO 并赋值到 forms[] 数组" };
header.Add(btnAuto);
header.Add(btnPing);
_detailRoot.Add(header);
// 三列形态网格
var formGrid = new VisualElement();
formGrid.style.flexDirection = FlexDirection.Row;
formGrid.style.marginBottom = 10;
_detailRoot.Add(formGrid);
for (int i = 0; i < 3; i++)
formGrid.Add(BuildFormColumn(i));
// 分割线
_detailRoot.Add(MakeSeparator());
// 原始 Inspector完整字段编辑
var inspHeader = new Label("原始 Inspector 编辑") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginBottom = 4 } };
_detailRoot.Add(inspHeader);
_rawInspector = new InspectorElement(_selected);
_detailRoot.Add(_rawInspector);
}
private VisualElement BuildFormColumn(int colIdx)
{
var (formType, label, accent) = FormDefs[colIdx];
var col = new VisualElement();
col.style.flexGrow = 1;
col.style.marginRight = colIdx < 2 ? 8 : 0;
col.style.borderTopWidth = 2;
col.style.borderTopColor = accent;
col.style.borderLeftWidth = 1;
col.style.borderRightWidth = 1;
col.style.borderBottomWidth = 1;
col.style.borderLeftColor = new Color(accent.r, accent.g, accent.b, 0.35f);
col.style.borderRightColor = new Color(accent.r, accent.g, accent.b, 0.35f);
col.style.borderBottomColor = 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;
_columnRoots[colIdx] = col;
// 列标题 + 色块
var titleRow = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginBottom = 6 } };
var colorDot = new VisualElement();
colorDot.style.width = 12;
colorDot.style.height = 12;
colorDot.style.borderTopLeftRadius = 6;
colorDot.style.borderTopRightRadius = 6;
colorDot.style.borderBottomLeftRadius = 6;
colorDot.style.borderBottomRightRadius = 6;
colorDot.style.backgroundColor = accent;
colorDot.style.marginRight = 6;
titleRow.Add(colorDot);
titleRow.Add(new Label(label) { style = { unityFontStyleAndWeight = FontStyle.Bold } });
col.Add(titleRow);
// 当前 FormSO 显示
FormSO current = GetFormByType(formType);
var formField = new ObjectField("FormSO") { objectType = typeof(FormSO), value = current };
formField.RegisterValueChangedCallback(e =>
{
SetFormByType(formType, e.newValue as FormSO);
// 联动刷新武器字段
if (_weaponFields[colIdx] != null)
_weaponFields[colIdx].value = (e.newValue as FormSO)?.defaultWeapon;
});
_formFields[colIdx] = formField;
col.Add(formField);
// 武器预览(只读,跟随 FormSO.defaultWeapon
var weaponField = new ObjectField("默认武器") { objectType = typeof(WeaponSO), value = current?.defaultWeapon };
weaponField.SetEnabled(false);
_weaponFields[colIdx] = weaponField;
col.Add(weaponField);
// 新建该形态 FormSO
var btnCreate = new Button(() => CreateFormForType(formType, colIdx))
{
text = current == null ? $"+ 新建 {label} FormSO" : $"↺ 重新绑定 {label} FormSO",
tooltip = current == null
? $"在 {DataRoot} 创建 PLY_Form_{formType}.asset"
: $"将 {DataRoot}/PLY_Form_{formType}.asset 重新赋值到当前 FormConfig不会覆盖已有文件",
style = { marginTop = 8 }
};
col.Add(btnCreate);
// 新建该形态 WeaponSO
var btnWeapon = new Button(() => CreateWeaponForType(formType, colIdx))
{
text = $"+ 新建 {label} WeaponSO",
tooltip = $"在 {WeaponDir} 创建 WPN_{formType}.asset",
style = { marginTop = 2 }
};
col.Add(btnWeapon);
return col;
}
// ── 操作:新建 FormConfigSO ───────────────────────────────────────────
private void CreateNewFormConfig()
{
var cfg = EditorScaffoldUtils.CreateSOAsset<FormConfigSO>(DataRoot, "PLY_FormConfig");
if (cfg != null)
{
RefreshAll();
// 选中新建项
int idx = _filtered.IndexOf(cfg);
if (idx >= 0) _configList.SetSelection(idx);
}
}
// ── 操作:新建 FormSO ──────────────────────────────────────────────────
private void CreateFormForType(FormType formType, int colIdx)
{
if (_selected == null) return;
EditorScaffoldUtils.EnsureFolder(DataRoot);
string path = $"{DataRoot}/PLY_Form_{formType}.asset";
var form = AssetDatabase.LoadAssetAtPath<FormSO>(path);
if (form == null)
{
form = ScriptableObject.CreateInstance<FormSO>();
form.formId = $"Form_{formType}";
form.displayName = FormDefs[colIdx].label;
form.formType = formType;
AssetDatabase.CreateAsset(form, path);
AssetDatabase.SaveAssets();
}
SetFormByType(formType, form);
_formFields[colIdx].value = form;
EditorGUIUtility.PingObject(form);
}
// ── 操作:新建 WeaponSO ───────────────────────────────────────────────
private void CreateWeaponForType(FormType formType, int colIdx)
{
var weapon = EditorScaffoldUtils.CreateSOAsset<WeaponSO>(WeaponDir, $"WPN_{formType}");
if (weapon == null) weapon = AssetDatabase.LoadAssetAtPath<WeaponSO>($"{WeaponDir}/WPN_{formType}.asset");
if (weapon == null) return;
// 赋值到对应 FormSO.defaultWeapon
var form = GetFormByType(formType);
if (form != null)
{
form.defaultWeapon = weapon;
EditorUtility.SetDirty(form);
AssetDatabase.SaveAssets();
_weaponFields[colIdx].value = weapon;
}
}
// ── 操作:自动填充形态 ────────────────────────────────────────────────
private void AutoFillForms()
{
if (_selected == null) return;
var allForms = EditorScaffoldUtils.FindAllAssetsOfType<FormSO>();
bool changed = false;
for (int i = 0; i < 3; i++)
{
var (ftype, _, _) = FormDefs[i];
if (GetFormByType(ftype) != null) continue;
var match = allForms.FirstOrDefault(f => f.formType == ftype);
if (match != null)
{
SetFormByType(ftype, match);
_formFields[i].value = match;
changed = true;
}
}
if (changed)
{
EditorUtility.SetDirty(_selected);
AssetDatabase.SaveAssets();
Debug.Log("[FormEditorWindow] 自动填充完成。");
}
else
{
Debug.Log("[FormEditorWindow] 未发现需要填充的空槽,或 Project 中无匹配 FormSO。");
}
}
// ── 数据访问(操作 FormConfigSO.forms[])────────────────────────────
private FormSO GetFormByType(FormType type)
{
if (_selected == null || _selected.forms == null) return null;
return _selected.forms.FirstOrDefault(f => f != null && f.formType == type);
}
private void SetFormByType(FormType type, FormSO form)
{
if (_selected == null) return;
if (_selected.forms == null || _selected.forms.Length < 3)
{
var arr = new FormSO[3];
if (_selected.forms != null)
for (int i = 0; i < _selected.forms.Length && i < 3; i++)
arr[i] = _selected.forms[i];
_selected.forms = arr;
}
// 按 FormDefs 顺序TianHun=0, DiHun=1, MingHun=2
for (int i = 0; i < FormDefs.Length; i++)
{
if (FormDefs[i].type == type)
{
_selected.forms[i] = form;
EditorUtility.SetDirty(_selected);
AssetDatabase.SaveAssets();
return;
}
}
}
// ── UI 辅助 ───────────────────────────────────────────────────────────
private static VisualElement MakeSeparator()
{
var sep = new VisualElement();
sep.style.height = 1;
sep.style.backgroundColor = new Color(0.3f, 0.3f, 0.3f, 0.6f);
sep.style.marginTop = 10;
sep.style.marginBottom = 10;
return sep;
}
}
}

View File

@@ -0,0 +1,158 @@
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
namespace BaseGames.Editor
{
/// <summary>
/// 集中管理 ScriptableObject 资产的 CRUD 操作(含 Undo 支持)。
/// </summary>
public static class AssetOperations
{
// ── 创建 ──────────────────────────────────────────────────────────────
/// <summary>
/// 弹出 SaveFilePanel 让用户选择路径,创建并返回新资产。
/// 创建失败或用户取消时返回 null。
/// </summary>
public static T Create<T>(string defaultFolder, string defaultName) where T : ScriptableObject
{
if (!Directory.Exists(defaultFolder))
Directory.CreateDirectory(defaultFolder);
string path = EditorUtility.SaveFilePanelInProject(
"新建 " + typeof(T).Name,
defaultName + ".asset",
"asset",
"选择保存路径",
defaultFolder);
if (string.IsNullOrEmpty(path))
return null;
var asset = ScriptableObject.CreateInstance<T>();
asset.name = Path.GetFileNameWithoutExtension(path);
AssetDatabase.CreateAsset(asset, path);
Undo.RegisterCreatedObjectUndo(asset, "Create " + typeof(T).Name);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
return asset;
}
// ── 重命名 ───────────────────────────────────────────────────────────
/// <summary>
/// 重命名资产(同时更新磁盘文件名和 asset.name
/// 返回 (true, null) 成功;(false, errorMsg) 失败。
/// </summary>
public static (bool ok, string error) Rename(UnityEngine.Object asset, string newName)
{
if (asset == null) return (false, "资产为 null");
if (string.IsNullOrWhiteSpace(newName)) return (false, "名称不能为空");
newName = newName.Trim();
string path = AssetDatabase.GetAssetPath(asset);
if (string.IsNullOrEmpty(path)) return (false, "资产不在 AssetDatabase 中");
// 先更新序列化内部名称
string oldName = asset.name;
Undo.RecordObject(asset, "Rename " + oldName);
asset.name = newName;
EditorUtility.SetDirty(asset);
// 再重命名磁盘文件
string err = AssetDatabase.RenameAsset(path, newName);
if (!string.IsNullOrEmpty(err))
{
asset.name = oldName;
EditorUtility.SetDirty(asset);
return (false, err);
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
return (true, null);
}
// ── 删除 ─────────────────────────────────────────────────────────────
/// <summary>弹出确认对话框,确认后删除资产文件。返回是否已删除。</summary>
public static bool Delete(UnityEngine.Object asset)
{
if (asset == null) return false;
string path = AssetDatabase.GetAssetPath(asset);
if (string.IsNullOrEmpty(path)) return false;
if (!EditorUtility.DisplayDialog(
"确认删除",
$"删除资产:{asset.name}\n路径{path}\n\n此操作不可撤销。",
"删除", "取消"))
return false;
AssetDatabase.DeleteAsset(path);
AssetDatabase.Refresh();
return true;
}
// ── 克隆 ─────────────────────────────────────────────────────────────
/// <summary>复制资产文件并返回克隆的资产;用户取消或失败时返回 null。</summary>
public static T Clone<T>(T source, string defaultFolder) where T : ScriptableObject
{
if (source == null) return null;
string srcPath = AssetDatabase.GetAssetPath(source);
string path = EditorUtility.SaveFilePanelInProject(
"克隆 " + source.name,
source.name + "_Copy.asset",
"asset",
"选择保存路径",
defaultFolder);
if (string.IsNullOrEmpty(path)) return null;
if (!AssetDatabase.CopyAsset(srcPath, path))
{
Debug.LogError($"[AssetOperations] 克隆失败:{srcPath} → {path}");
return null;
}
AssetDatabase.Refresh();
var clone = AssetDatabase.LoadAssetAtPath<T>(path);
if (clone != null)
Undo.RegisterCreatedObjectUndo(clone, "Clone " + source.name);
return clone;
}
// ── 查询 ─────────────────────────────────────────────────────────────
/// <summary>在 AssetDatabase 中查找所有 T 类型资产。</summary>
public static List<T> FindAll<T>() where T : ScriptableObject
{
var result = new List<T>();
string[] guids = AssetDatabase.FindAssets("t:" + typeof(T).Name);
foreach (var guid in guids)
{
string p = AssetDatabase.GUIDToAssetPath(guid);
var asset = AssetDatabase.LoadAssetAtPath<T>(p);
if (asset != null) result.Add(asset);
}
return result;
}
// ── GUID 工具 ─────────────────────────────────────────────────────────
public static string GetGuid(UnityEngine.Object asset)
{
if (asset == null) return string.Empty;
return AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(asset));
}
public static T LoadByGuid<T>(string guid) where T : UnityEngine.Object
{
if (string.IsNullOrEmpty(guid)) return null;
return AssetDatabase.LoadAssetAtPath<T>(AssetDatabase.GUIDToAssetPath(guid));
}
}
}

View File

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

View File

@@ -0,0 +1,123 @@
using System;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace BaseGames.Editor
{
/// <summary>
/// 详情区顶部标题行:显示资产名称,双击或按 ✏ 进入行内重命名。
/// </summary>
public class DetailHeader : VisualElement
{
// ── 事件 ─────────────────────────────────────────────────────────────
/// <summary>用户确认重命名时触发,参数为新名称字符串。</summary>
public event Action<string> RenameRequested;
// ── 私有字段 ──────────────────────────────────────────────────────────
private Label _nameLabel;
private TextField _renameField;
private bool _renaming;
// ── 构造 ─────────────────────────────────────────────────────────────
public DetailHeader()
{
style.flexDirection = FlexDirection.Row;
style.alignItems = Align.Center;
style.paddingLeft = 12;
style.paddingRight = 8;
style.paddingTop = 10;
style.paddingBottom = 10;
style.borderBottomWidth = 1;
style.borderBottomColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.2f));
// 名称 Label
_nameLabel = new Label("(未选中)");
_nameLabel.style.flexGrow = 1;
_nameLabel.style.fontSize = 15;
_nameLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
_nameLabel.RegisterCallback<MouseDownEvent>(e =>
{
if (e.clickCount == 2) BeginRename();
});
Add(_nameLabel);
// 重命名输入框(默认隐藏)
_renameField = new TextField();
_renameField.style.flexGrow = 1;
_renameField.style.fontSize = 15;
_renameField.style.display = DisplayStyle.None;
_renameField.RegisterCallback<KeyDownEvent>(e =>
{
if (e.keyCode == KeyCode.Return || e.keyCode == KeyCode.KeypadEnter)
{
e.StopPropagation();
CommitRename();
}
else if (e.keyCode == KeyCode.Escape)
{
e.StopPropagation();
CancelRename();
}
});
_renameField.RegisterCallback<FocusOutEvent>(_ => CommitRename());
Add(_renameField);
// 编辑按钮
var btnEdit = new Button(BeginRename) { text = "✏", tooltip = "重命名(双击名称也可触发)" };
btnEdit.style.width = 24;
btnEdit.style.height = 24;
btnEdit.style.marginLeft = 6;
btnEdit.style.paddingLeft = 0;
btnEdit.style.paddingRight = 0;
btnEdit.style.fontSize = 13;
Add(btnEdit);
}
// ── 公共 API ──────────────────────────────────────────────────────────
/// <summary>绑定新资产,更新标题显示。</summary>
public void SetAsset(UnityEngine.Object asset)
{
CancelRename();
_nameLabel.text = asset != null ? asset.name : "(未选中)";
}
// ── 内部逻辑 ──────────────────────────────────────────────────────────
private void BeginRename()
{
if (_renaming) return;
_renaming = true;
_renameField.value = _nameLabel.text;
_nameLabel.style.display = DisplayStyle.None;
_renameField.style.display = DisplayStyle.Flex;
schedule.Execute(() =>
{
_renameField.Focus();
_renameField.SelectAll();
});
}
private void CommitRename()
{
if (!_renaming) return;
_renaming = false;
_nameLabel.style.display = DisplayStyle.Flex;
_renameField.style.display = DisplayStyle.None;
var newName = _renameField.value.Trim();
if (!string.IsNullOrEmpty(newName) && newName != _nameLabel.text)
RenameRequested?.Invoke(newName);
}
private void CancelRename()
{
if (!_renaming) return;
_renaming = false;
_nameLabel.style.display = DisplayStyle.Flex;
_renameField.style.display = DisplayStyle.None;
}
}
}

View File

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

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace BaseGames.Editor
{
@@ -11,6 +12,139 @@ namespace BaseGames.Editor
/// </summary>
public static class EditorScaffoldUtils
{
// ── 资产命名前缀 ──────────────────────────────────────────────────────
private static readonly Dictionary<string, (string Prefix, string Convention)> s_PrefixMap = new()
{
{ "WeaponSO", ("WPN_", "WPN_{ID}WPN_SkyBlade") },
{ "FormSkillSO", ("SKL_", "SKL_{Name}SKL_SoulBlade") },
{ "BossSkillSO", ("SKL_", "SKL_{Name}SKL_BossRage") },
{ "SkillSequenceSO", ("SKL_", "SKL_Seq_{Name}SKL_Seq_RageCombo") },
{ "EnemyStatsSO", ("ENM_", "ENM_E{ID}_StatsENM_E001_Stats") },
{ "LootTableSO", ("ENM_", "ENM_E{ID}_LootENM_E001_Loot") },
{ "FormConfigSO", ("PLY_", "PLY_{FormID}PLY_Player01") },
{ "DamageSourceSO", ("CMB_", "CMB_DamageSource_{Name}CMB_DamageSource_Sword") },
{ "AbilityConfigSO", ("ABL_", "ABL_{Name}ABL_DoubleJump") },
{ "CharmConfigSO", ("CHM_", "CHM_{Name}CHM_GhostMantis") },
{ "ShopInventorySO", ("SHP_", "SHP_Inventory_{Name}SHP_Inventory_Forest") },
{ "MapRoomDataSO", ("MAP_", "MAP_RoomData_{Name}MAP_RoomData_Forest_01") },
{ "AudioPlaylistSO", ("AUD_", "AUD_BGM_{Name}AUD_BGM_Forest") },
{ "AudioConfigSO", ("AUD_", "AUD_SFX_{Name}AUD_SFX_Sword") },
{ "GlobalSettingsSO", ("SET_", "SET_{Name}SET_GlobalSettings") },
};
/// <summary>
/// 根据 SO 类型返回命名前缀和规范说明。
/// 事件频道(类名以 EventChannelSO 结尾)统一返回 EVT_ 前缀。
/// </summary>
public static (string Prefix, string Convention) GetAssetPrefixInfo(Type t)
{
if (t == null) return ("", "");
// 事件频道
if (t.Name.EndsWith("EventChannelSO"))
return ("EVT_", "EVT_{Description}EVT_PlayerDied");
// 直接匹配
if (s_PrefixMap.TryGetValue(t.Name, out var info)) return info;
// 向上遍历基类
var baseType = t.BaseType;
while (baseType != null && baseType != typeof(ScriptableObject))
{
if (s_PrefixMap.TryGetValue(baseType.Name, out var baseInfo)) return baseInfo;
baseType = baseType.BaseType;
}
return ("", "");
}
// ── 重命名 UI 组件 ────────────────────────────────────────────────────
/// <summary>
/// 创建可复用的"重命名资产"操作条UIToolkit
/// 重命名成功后以资产 GUID字符串调用 onRenamed调用方可用
/// <see cref="FindIndexByGuid{T}"/> 在刷新后的列表中恢复选中。
/// </summary>
/// <param name="asset">要重命名的资产对象。</param>
/// <param name="prefix">自动前缀(如 WPN_留空则禁用前缀勾选框。</param>
/// <param name="convention">命名规范提示文字,留空则不显示。</param>
/// <param name="onRenamed">重命名成功后的回调,参数为资产 GUID重命名不会改变 GUID。</param>
public static VisualElement MakeRenameBar(
UnityEngine.Object asset,
string prefix,
string convention,
Action<string> onRenamed = null)
{
var bar = new VisualElement();
bar.AddToClassList("rename-bar");
// ── 标题行 ────────────────────────────────────────────────────
var headerRow = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } };
var titleLbl = new Label("重命名") { style = { unityFontStyleAndWeight = FontStyle.Bold, flexGrow = 1 } };
headerRow.Add(titleLbl);
if (!string.IsNullOrEmpty(convention))
{
var hint = new Label($"规范:{convention}");
hint.AddToClassList("rename-hint");
headerRow.Add(hint);
}
bar.Add(headerRow);
// ── 输入行 ────────────────────────────────────────────────────
var inputRow = new VisualElement();
inputRow.AddToClassList("rename-bar-row");
var nameField = new TextField { value = asset.name, style = { flexGrow = 1 } };
inputRow.Add(nameField);
bool hasPrefix = !string.IsNullOrEmpty(prefix);
var toggle = new Toggle("自动前缀") { value = hasPrefix };
toggle.style.marginLeft = 6;
toggle.style.marginRight = 6;
if (!hasPrefix) toggle.SetEnabled(false);
inputRow.Add(toggle);
var btn = new Button(() =>
{
string newName = nameField.value.Trim();
if (string.IsNullOrEmpty(newName)) return;
if (toggle.value && hasPrefix && !newName.StartsWith(prefix, StringComparison.Ordinal))
newName = prefix + newName;
string assetPath = AssetDatabase.GetAssetPath(asset);
if (string.IsNullOrEmpty(assetPath))
{
EditorUtility.DisplayDialog("重命名失败", "资产路径为空,请先保存资产。", "确定");
return;
}
// GUID 在重命名后不变,提前捕获
string guid = AssetDatabase.AssetPathToGUID(assetPath);
// 1. 更新内部序列化名称
asset.name = newName;
EditorUtility.SetDirty(asset);
// 2. 重命名文件(只改文件名,不含扩展名)
string err = AssetDatabase.RenameAsset(assetPath, newName);
if (!string.IsNullOrEmpty(err))
{
// 回滚内存名称
asset.name = System.IO.Path.GetFileNameWithoutExtension(assetPath);
EditorUtility.DisplayDialog("重命名失败", err, "确定");
return;
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
// 同步输入框显示实际名称
nameField.value = asset.name;
onRenamed?.Invoke(guid);
}) { text = "重命名" };
inputRow.Add(btn);
bar.Add(inputRow);
return bar;
}
// ── SO 资产创建 ───────────────────────────────────────────────────────
/// <summary>
@@ -37,6 +171,68 @@ namespace BaseGames.Editor
return asset;
}
/// <summary>
/// 通过 SaveFilePanel 让用户选择名称和路径,交互式创建 SO 资产。
/// 取消时返回 null。
/// </summary>
public static T CreateSOAssetInteractive<T>(string defaultFolder, string defaultName) where T : ScriptableObject
{
string path = EditorUtility.SaveFilePanelInProject(
$"创建 {typeof(T).Name}",
defaultName, "asset",
$"选择 {typeof(T).Name} 的保存路径",
defaultFolder);
if (string.IsNullOrEmpty(path)) return null;
EnsureFolder(System.IO.Path.GetDirectoryName(path)?.Replace('\\', '/') ?? defaultFolder);
var asset = ScriptableObject.CreateInstance<T>();
AssetDatabase.CreateAsset(asset, path);
AssetDatabase.SaveAssets();
PingAndSelect(asset);
return asset;
}
// ── 资产删除 ──────────────────────────────────────────────────────────
/// <summary>
/// 弹出确认对话框后删除 SO 资产。返回 true 表示已成功删除。
/// </summary>
public static bool DeleteSOAsset(ScriptableObject asset)
{
if (asset == null) return false;
string path = AssetDatabase.GetAssetPath(asset);
if (string.IsNullOrEmpty(path)) return false;
bool ok = EditorUtility.DisplayDialog(
"确认删除",
$"确定要删除「{asset.name}」?\n{path}\n\n此操作不可撤销",
"删除", "取消");
if (!ok) return false;
AssetDatabase.DeleteAsset(path);
AssetDatabase.SaveAssets();
return true;
}
// ── 按 GUID 查找 ──────────────────────────────────────────────────────
/// <summary>
/// 在列表中按资产 GUID 查找索引(重命名后 GUID 不变,用于在 Refresh 后恢复选中)。
/// 返回 -1 表示未找到。
/// </summary>
public static int FindIndexByGuid<T>(List<T> list, string guid) where T : UnityEngine.Object
{
if (string.IsNullOrEmpty(guid)) return -1;
for (int i = 0; i < list.Count; i++)
{
if (list[i] == null) continue;
string g = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(list[i]));
if (g == guid) return i;
}
return -1;
}
// ── 目录工具 ──────────────────────────────────────────────────────────
/// <summary>确保 Assets 相对路径目录存在(不存在则递归创建)。</summary>

View File

@@ -0,0 +1,259 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace BaseGames.Editor
{
/// <summary>
/// 通用 ScriptableObject 列表面板VisualElement 子类)。
/// 提供:搜索栏、[新建] 按钮、带类型徽章的 ListView、右键上下文菜单、GUID 选中追踪。
/// </summary>
public class SoListPane<T> : VisualElement where T : ScriptableObject
{
// ── 事件(使用字段委托,允许外部直接赋值替换,避免累积)─────────────
public Action<T> SelectionChanged;
// ── 字段 ─────────────────────────────────────────────────────────────
private readonly string _defaultFolder;
private readonly string _defaultPrefix;
private readonly Func<T, string> _getTypeBadge;
private List<T> _all = new();
private List<T> _filtered = new();
private string _search = "";
private string _savedGuid = "";
private ListView _listView;
private Label _countLabel;
// ── 构造 ─────────────────────────────────────────────────────────────
/// <param name="defaultFolder">新建资产的默认保存目录。</param>
/// <param name="defaultPrefix">新建资产文件名前缀(如 "WPN_")。</param>
/// <param name="getTypeBadge">返回每个资产的类型徽章文本;返回 null/空则不显示徽章。</param>
public SoListPane(
string defaultFolder,
string defaultPrefix = "",
Func<T, string> getTypeBadge = null)
{
_defaultFolder = defaultFolder;
_defaultPrefix = defaultPrefix;
_getTypeBadge = getTypeBadge;
style.flexGrow = 1;
style.flexDirection = FlexDirection.Column;
BuildUI();
}
// ── UI ────────────────────────────────────────────────────────────────
private void BuildUI()
{
// Toolbar
var toolbar = new VisualElement();
toolbar.style.flexDirection = FlexDirection.Row;
toolbar.style.alignItems = Align.Center;
toolbar.style.paddingLeft = 6;
toolbar.style.paddingRight = 6;
toolbar.style.paddingTop = 5;
toolbar.style.paddingBottom = 5;
toolbar.style.borderBottomWidth = 1;
toolbar.style.borderBottomColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.3f));
Add(toolbar);
var searchField = new TextField();
searchField.style.flexGrow = 1;
searchField.style.marginRight = 4;
searchField.RegisterValueChangedCallback(e => { _search = e.newValue; ApplyFilter(); });
toolbar.Add(searchField);
var btnNew = new Button(OnCreateClicked) { text = "+ 新建" };
btnNew.style.height = 20;
btnNew.style.paddingLeft = 8;
btnNew.style.paddingRight = 8;
toolbar.Add(btnNew);
// ListView
_listView = new ListView(_filtered, 24, MakeItem, BindItem);
_listView.style.flexGrow = 1;
_listView.selectionType = SelectionType.Single;
_listView.showAlternatingRowBackgrounds = AlternatingRowBackground.ContentOnly;
_listView.onSelectionChange += objects =>
{
var sel = objects.OfType<T>().FirstOrDefault();
if (sel != null) _savedGuid = AssetOperations.GetGuid(sel);
SelectionChanged?.Invoke(sel);
};
Add(_listView);
// Count footer
_countLabel = new Label();
_countLabel.style.fontSize = 10;
_countLabel.style.opacity = 0.55f;
_countLabel.style.paddingLeft = 6;
_countLabel.style.paddingBottom = 3;
_countLabel.style.paddingTop = 2;
Add(_countLabel);
}
// ── ListView 回调 ─────────────────────────────────────────────────────
private VisualElement MakeItem()
{
var root = new VisualElement();
root.style.flexDirection = FlexDirection.Row;
root.style.alignItems = Align.Center;
root.style.paddingLeft = 6;
root.style.paddingRight = 4;
root.style.height = 24;
// 类型徽章
var badge = new Label { name = "badge" };
badge.style.fontSize = 10;
badge.style.paddingLeft = 4;
badge.style.paddingRight = 4;
badge.style.paddingTop = 1;
badge.style.paddingBottom = 1;
badge.style.borderTopLeftRadius = 3;
badge.style.borderTopRightRadius = 3;
badge.style.borderBottomLeftRadius = 3;
badge.style.borderBottomRightRadius = 3;
badge.style.backgroundColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.3f));
badge.style.marginRight = 5;
badge.style.display = DisplayStyle.None;
root.Add(badge);
// 名称标签
var nameLabel = new Label { name = "name" };
nameLabel.style.flexGrow = 1;
root.Add(nameLabel);
// 右键菜单(通过 userData 获取当前绑定的资产)
root.AddManipulator(new ContextualMenuManipulator(evt =>
{
if (root.userData is not T item) return;
evt.menu.AppendAction("在 Project 中定位", _ =>
{
EditorGUIUtility.PingObject(item);
Selection.activeObject = item;
});
evt.menu.AppendAction("在 Inspector 中打开", _ =>
Selection.activeObject = item);
evt.menu.AppendSeparator();
evt.menu.AppendAction("克隆...", _ =>
{
var clone = AssetOperations.Clone(item, _defaultFolder);
if (clone != null) Refresh(clone);
});
evt.menu.AppendSeparator();
evt.menu.AppendAction("删除", _ =>
{
if (AssetOperations.Delete(item)) Refresh(null);
});
}));
return root;
}
private void BindItem(VisualElement el, int idx)
{
if (idx < 0 || idx >= _filtered.Count) return;
var item = _filtered[idx];
el.userData = item;
el.Q<Label>("name").text = item.name;
var badge = el.Q<Label>("badge");
if (badge != null)
{
if (_getTypeBadge != null)
{
var txt = _getTypeBadge(item);
if (!string.IsNullOrEmpty(txt))
{
badge.text = txt;
badge.style.display = DisplayStyle.Flex;
return;
}
}
badge.style.display = DisplayStyle.None;
}
}
// ── 内部操作 ──────────────────────────────────────────────────────────
private void OnCreateClicked()
{
string name = _defaultPrefix.Length > 0 ? _defaultPrefix + "New" : "New" + typeof(T).Name;
var asset = AssetOperations.Create<T>(_defaultFolder, name);
if (asset != null) Refresh(asset);
}
private void ApplyFilter()
{
_filtered.Clear();
foreach (var item in _all)
{
if (string.IsNullOrEmpty(_search) ||
item.name.IndexOf(_search, StringComparison.OrdinalIgnoreCase) >= 0)
_filtered.Add(item);
}
_listView.RefreshItems();
_countLabel.text = _all.Count == _filtered.Count
? $"{_all.Count} 项"
: $"{_filtered.Count} / {_all.Count} 项";
TryRestoreSelection();
}
private void TryRestoreSelection()
{
if (string.IsNullOrEmpty(_savedGuid)) return;
for (int i = 0; i < _filtered.Count; i++)
{
if (AssetOperations.GetGuid(_filtered[i]) == _savedGuid)
{
_listView.SetSelection(i);
_listView.ScrollToItem(i);
return;
}
}
}
// ── 公共 API ──────────────────────────────────────────────────────────
/// <summary>重新加载所有资产,可选择在刷新后选中指定资产。</summary>
public void Refresh(T selectAfter = null)
{
if (selectAfter != null)
_savedGuid = AssetOperations.GetGuid(selectAfter);
_all = AssetOperations.FindAll<T>();
_all.Sort((a, b) => string.Compare(a.name, b.name, StringComparison.OrdinalIgnoreCase));
ApplyFilter();
}
/// <summary>强制重建列表视觉(如重命名后需刷新显示)。</summary>
public void Invalidate()
{
_listView.RefreshItems();
}
/// <summary>当前选中的资产;无选中时为 null。</summary>
public T Selected =>
_listView.selectedIndex >= 0 && _listView.selectedIndex < _filtered.Count
? _filtered[_listView.selectedIndex]
: null;
/// <summary>清除列表选中,触发 SelectionChanged(null)。</summary>
public void ClearSelection()
{
_listView.ClearSelection();
}
}
}

View File

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

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

@@ -35,6 +35,7 @@ namespace BaseGames.Editor
public string TypeName;
public string AssetPath;
public ScriptableObject Asset;
public string Guid;
}
private readonly List<CategoryEntry> _categories = new();
@@ -50,6 +51,7 @@ namespace BaseGames.Editor
private ListView _assetList;
private TextField _searchField;
private Label _statusLabel;
private VisualElement _renameContainer;
// ── 菜单入口 ──────────────────────────────────────────────────────────
@@ -141,12 +143,16 @@ namespace BaseGames.Editor
var rightPane = new VisualElement();
rightPane.style.flexDirection = FlexDirection.Column;
// 列标题行
// 列标题行paddingRight 额外加 13px滚动条宽度确保列对齐
var colHeader = new VisualElement();
colHeader.AddToClassList("pane-header");
colHeader.style.flexDirection = FlexDirection.Row;
colHeader.style.flexDirection = FlexDirection.Row;
colHeader.style.paddingLeft = 8;
colHeader.style.paddingRight = 21; // 8 + 13 (滚动条占位)
colHeader.style.paddingTop = 4;
colHeader.style.paddingBottom = 4;
colHeader.Add(MakeHeaderLabel("资产名", true, 0));
colHeader.Add(MakeHeaderLabel("类型", false, 170));
colHeader.Add(MakeHeaderLabel("类型", false, 190));
colHeader.Add(MakeHeaderLabel("路径", true, 0));
rightPane.Add(colHeader);
@@ -160,19 +166,30 @@ namespace BaseGames.Editor
_assetList.style.flexGrow = 1;
_assetList.selectionChanged += _ => OnAssetPicked();
_assetList.itemsChosen += _ => FocusProjectWindow();
_assetList.AddManipulator(new ContextualMenuManipulator(BuildAssetContextMenu));
rightPane.Add(_assetList);
// 滚动条常显,确保 header 列与内容列对齐
_assetList.schedule.Execute(() =>
{
var sv = _assetList.Q<ScrollView>();
if (sv != null) sv.verticalScrollerVisibility = ScrollerVisibility.AlwaysVisible;
});
// 状态栏
_statusLabel = new Label("—");
_statusLabel.style.paddingLeft = 8;
_statusLabel.style.paddingTop = 3;
_statusLabel.style.paddingBottom = 3;
_statusLabel.style.borderTopWidth = 1;
_statusLabel.style.borderTopColor = new Color(0.15f, 0.15f, 0.15f);
_statusLabel.style.color = new Color(0.58f, 0.58f, 0.58f);
_statusLabel.style.fontSize = 11;
_statusLabel.style.borderTopColor = new Color(0.0f, 0.0f, 0.0f, 0.20f);
_statusLabel.AddToClassList("so-path-label");
rightPane.Add(_statusLabel);
// 重命名容器(选中资产后显示)
_renameContainer = new VisualElement { style = { display = DisplayStyle.None } };
rightPane.Add(_renameContainer);
split.Add(leftPane);
split.Add(rightPane);
rootVisualElement.Add(split);
@@ -182,7 +199,9 @@ namespace BaseGames.Editor
{
var lbl = new Label(text);
lbl.style.unityFontStyleAndWeight = FontStyle.Bold;
lbl.style.overflow = Overflow.Hidden;
lbl.style.unityTextAlign = TextAnchor.MiddleLeft;
lbl.style.overflow = Overflow.Hidden;
lbl.style.minWidth = 0;
if (grow) lbl.style.flexGrow = 1;
if (fixedWidth > 0) lbl.style.width = fixedWidth;
return lbl;
@@ -219,22 +238,22 @@ namespace BaseGames.Editor
var nameEl = new Label { name = "n" };
nameEl.style.flexGrow = 1;
nameEl.style.minWidth = 0;
nameEl.style.overflow = Overflow.Hidden;
nameEl.style.textOverflow = TextOverflow.Ellipsis;
var typeEl = new Label { name = "t" };
typeEl.style.width = 170;
typeEl.style.overflow = Overflow.Hidden;
typeEl.style.width = 190;
typeEl.style.minWidth = 0;
typeEl.style.overflow = Overflow.Hidden;
typeEl.style.textOverflow = TextOverflow.Ellipsis;
typeEl.style.color = new Color(0.52f, 0.80f, 1.00f);
typeEl.style.fontSize = 11;
typeEl.AddToClassList("so-type-label");
var pathEl = new Label { name = "p" };
pathEl.style.flexGrow = 1;
pathEl.style.overflow = Overflow.Hidden;
pathEl.style.flexGrow = 1;
pathEl.style.overflow = Overflow.Hidden;
pathEl.style.textOverflow = TextOverflow.Ellipsis;
pathEl.style.color = new Color(0.48f, 0.48f, 0.48f);
pathEl.style.fontSize = 10;
pathEl.AddToClassList("so-path-label");
row.Add(nameEl);
row.Add(typeEl);
@@ -262,11 +281,38 @@ namespace BaseGames.Editor
private void OnAssetPicked()
{
int idx = _assetList.selectedIndex;
if (idx < 0 || idx >= _filtered.Count) return;
if (idx < 0 || idx >= _filtered.Count)
{
_renameContainer.style.display = DisplayStyle.None;
return;
}
var asset = _filtered[idx].Asset;
if (asset == null) return;
if (asset == null)
{
_renameContainer.style.display = DisplayStyle.None;
return;
}
EditorGUIUtility.PingObject(asset);
Selection.activeObject = asset;
// 更新重命名栏
_renameContainer.Clear();
var (pfx, conv) = EditorScaffoldUtils.GetAssetPrefixInfo(asset.GetType());
var entry = _filtered[idx];
string savedGuid = entry.Guid;
_renameContainer.Add(EditorScaffoldUtils.MakeRenameBar(asset, pfx, conv, _ =>
{
Refresh();
int ni = _filtered.FindIndex(e => e.Guid == savedGuid);
if (ni >= 0) _assetList.SetSelection(ni);
}));
// 删除按钮
var btnDelete = new Button(() => DeleteCurrentAsset(asset)) { text = "删除…" };
btnDelete.AddToClassList("action-button--danger");
_renameContainer.Add(btnDelete);
_renameContainer.style.display = DisplayStyle.Flex;
}
private static void FocusProjectWindow()
@@ -293,6 +339,7 @@ namespace BaseGames.Editor
TypeName = asset.GetType().Name,
AssetPath = path,
Asset = asset,
Guid = guid,
});
}
@@ -331,6 +378,26 @@ namespace BaseGames.Editor
ApplyFilter();
}
private void DeleteCurrentAsset(ScriptableObject asset)
{
if (!EditorScaffoldUtils.DeleteSOAsset(asset)) return;
_renameContainer.Clear();
_renameContainer.style.display = DisplayStyle.None;
Refresh();
}
private void BuildAssetContextMenu(ContextualMenuPopulateEvent evt)
{
int idx = _assetList.selectedIndex;
if (idx < 0 || idx >= _filtered.Count) return;
var asset = _filtered[idx].Asset;
if (asset == null) return;
evt.menu.AppendAction("在 Project 中定位", _ => EditorScaffoldUtils.PingAndSelect(asset));
evt.menu.AppendAction("在 Inspector 中打开", _ => Selection.activeObject = asset);
evt.menu.AppendSeparator();
evt.menu.AppendAction("删除…", _ => DeleteCurrentAsset(asset));
}
private void ApplyFilter()
{
_filtered.Clear();

View File

@@ -1,13 +1,14 @@
/* BaseGames Editor 统一样式表
路径: Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss */
路径: Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss
原则:不硬编码文字颜色,让 Unity 主题决定前景色 */
/* ── 分区标题 ──────────────────────────────────────────── */
.section-header {
font-size: 11px;
font-size: 12px;
-unity-font-style: bold;
margin-top: 8px;
margin-bottom: 2px;
color: rgb(180, 180, 180);
opacity: 0.85;
}
/* ── 列表项 ─────────────────────────────────────────────── */
@@ -49,16 +50,41 @@
.stats-preview {
flex-direction: row;
flex-wrap: wrap;
align-items: center;
margin-bottom: 6px;
padding: 4px 6px;
background-color: rgba(40, 40, 40, 0.75);
border-radius: 3px;
background-color: rgba(128, 128, 128, 0.08);
border-width: 1px;
border-color: rgba(128, 128, 128, 0.18);
}
/* ── 重命名栏 ──────────────────────────────────────────── */
.rename-bar {
flex-direction: column;
padding: 6px 8px;
margin-bottom: 8px;
border-top-width: 1px;
border-bottom-width: 1px;
border-top-color: rgba(128, 128, 128, 0.20);
border-bottom-color: rgba(128, 128, 128, 0.20);
background-color: rgba(128, 128, 128, 0.06);
}
.rename-bar-row {
flex-direction: row;
align-items: center;
margin-top: 4px;
}
.rename-hint {
font-size: 10px;
opacity: 0.60;
}
/* ── 标签页按钮 ──────────────────────────────────────────── */
.tab-bar {
flex-direction: row;
border-bottom-width: 1px;
border-bottom-color: rgb(60, 60, 60);
border-bottom-color: rgba(128, 128, 128, 0.30);
margin-bottom: 6px;
}
.tab-button {
@@ -66,13 +92,14 @@
border-radius: 0;
border-width: 0;
background-color: rgba(0, 0, 0, 0);
color: rgb(160, 160, 160);
opacity: 0.65;
}
.tab-button:hover {
background-color: rgba(255, 255, 255, 0.08);
background-color: rgba(128, 128, 128, 0.10);
opacity: 1.0;
}
.tab-button--active {
color: rgb(255, 255, 255);
opacity: 1.0;
border-bottom-width: 2px;
border-bottom-color: rgb(100, 160, 255);
}
@@ -88,62 +115,61 @@
border-bottom-width: 2px;
border-bottom-color: rgba(0,0,0,0);
background-color: rgba(0,0,0,0);
color: rgb(170, 170, 170);
opacity: 0.70;
-unity-font-style: bold;
font-size: 13px;
}
.tab-btn:hover {
background-color: rgba(255,255,255,0.06);
background-color: rgba(128,128,128,0.10);
opacity: 1.0;
}
.tab-btn--active {
color: rgb(255, 255, 255);
opacity: 1.0;
border-bottom-width: 2px;
border-bottom-color: rgb(90, 160, 255);
}
/* SO 工厂按钮(绿调) */
.wizard-factory-btn {
background-color: rgba(40, 100, 55, 0.75);
color: rgb(200, 255, 210);
background-color: rgba(40, 120, 60, 0.55);
border-radius: 4px;
padding: 3px 10px;
margin-right: 4px;
margin-bottom: 4px;
border-width: 1px;
border-color: rgba(80, 180, 100, 0.60);
border-color: rgba(80, 180, 100, 0.50);
}
.wizard-factory-btn:hover {
background-color: rgba(55, 130, 70, 0.90);
background-color: rgba(50, 145, 72, 0.70);
}
/* 场景放置按钮(蓝调) */
.wizard-scene-btn {
background-color: rgba(30, 60, 120, 0.80);
color: rgb(190, 220, 255);
background-color: rgba(30, 75, 150, 0.55);
border-radius: 4px;
padding: 3px 10px;
margin-right: 4px;
margin-bottom: 4px;
border-width: 1px;
border-color: rgba(80, 130, 220, 0.55);
border-color: rgba(80, 130, 220, 0.50);
}
.wizard-scene-btn:hover {
background-color: rgba(40, 80, 155, 0.95);
background-color: rgba(40, 95, 180, 0.70);
}
/* 跳转按钮(中性灰调 */
/* 跳转按钮(无背景,仅边框区分 */
.wizard-jump-btn {
background-color: rgba(55, 55, 60, 0.80);
color: rgb(210, 210, 220);
background-color: rgba(0, 0, 0, 0);
border-radius: 4px;
padding: 3px 10px;
margin-right: 4px;
margin-bottom: 4px;
border-width: 1px;
border-color: rgba(120, 120, 130, 0.45);
border-color: rgba(128, 128, 128, 0.45);
}
.wizard-jump-btn:hover {
background-color: rgba(75, 75, 85, 0.95);
background-color: rgba(128, 128, 128, 0.12);
border-color: rgba(128, 128, 128, 0.80);
}
/* 小怪类型选择按钮 */
@@ -155,8 +181,7 @@
/* ── 一键创建全部按钮(蓝调,区别于绿色工厂按钮)────────── */
.wizard-create-all-btn {
background-color: rgba(40, 80, 155, 0.85);
color: rgb(210, 230, 255);
background-color: rgba(40, 80, 155, 0.55);
border-radius: 4px;
padding: 4px 12px;
border-width: 1px;
@@ -166,7 +191,7 @@
height: 28px;
}
.wizard-create-all-btn:hover {
background-color: rgba(55, 100, 180, 0.95);
background-color: rgba(55, 100, 180, 0.75);
}
/* ── SO 状态芯片(圆角徽章,替代 MakeStatusGrid 内联颜色)── */
@@ -178,13 +203,12 @@
padding-right: 6px;
margin-right: 6px;
margin-bottom: 4px;
background-color: rgba(30, 105, 50, 0.90);
color: rgb(175, 245, 185);
background-color: rgba(35, 115, 55, 0.55);
border-width: 1px;
border-color: rgba(65, 165, 80, 0.55);
border-color: rgba(80, 185, 100, 0.55);
}
.status-chip--ok:hover {
background-color: rgba(40, 130, 62, 0.95);
background-color: rgba(50, 145, 70, 0.75);
}
.status-chip--missing {
border-radius: 4px;
@@ -194,8 +218,7 @@
padding-right: 6px;
margin-right: 6px;
margin-bottom: 4px;
background-color: rgba(145, 55, 10, 0.90);
color: rgb(255, 210, 155);
background-color: rgba(145, 55, 10, 0.55);
border-width: 1px;
border-color: rgba(195, 100, 25, 0.55);
}
@@ -217,3 +240,22 @@
background-color: rgba(128, 128, 128, 0.08);
-unity-font-style: bold;
}
.so-type-label {
font-size: 12px;
-unity-font-style: italic;
}
.so-path-label {
font-size: 11px;
opacity: 0.65;
}
/* ── 危险操作按钮(删除)───────────────────────────────── */
.action-button--danger {
border-width: 1px;
border-color: rgba(200, 80, 80, 0.50);
margin-left: 8px;
}
.action-button--danger:hover {
background-color: rgba(200, 80, 80, 0.20);
border-color: rgba(220, 100, 100, 0.80);
}