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:
@@ -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:
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 76de8c0ba36dcce4db8990c5b62e9ed8
|
||||
guid: ce7bc9bad6f58ec42baff08f5353340e
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
28
Assets/_Game/Data/Enemies/ENM_New_Stats.asset
Normal file
28
Assets/_Game/Data/Enemies/ENM_New_Stats.asset
Normal 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
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f80486c9cd3d1db459ce6df915b11546
|
||||
guid: e0cf93e053ead744fa1876771ba0d081
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
49
Assets/_Game/Data/Enemies/SKL_Boss_New.asset
Normal file
49
Assets/_Game/Data/Enemies/SKL_Boss_New.asset
Normal 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: []
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d87ae01ed8a2b4f4cb27d159a52d1a14
|
||||
guid: a5d737a5b9641124aafb375d8684e06a
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
@@ -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: []
|
||||
@@ -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: []
|
||||
46
Assets/_Game/Data/Progression/Skills/SKL_New.asset
Normal file
46
Assets/_Game/Data/Progression/Skills/SKL_New.asset
Normal 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: []
|
||||
8
Assets/_Game/Data/Progression/Skills/SKL_New.asset.meta
Normal file
8
Assets/_Game/Data/Progression/Skills/SKL_New.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d00e0d6104f281345b8978d3a72eed13
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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: []
|
||||
|
||||
@@ -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 Hub(Boss技能)", 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;
|
||||
|
||||
@@ -1,334 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Animancer;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Player;
|
||||
|
||||
namespace BaseGames.Editor.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// 武器数据管理窗口(W-02)。
|
||||
/// 技术:UI Toolkit TwoPaneSplitView。
|
||||
/// 菜单:BaseGames / Data / Weapon Editor
|
||||
///
|
||||
/// 左栏:可搜索的 WeaponSO 列表 + [新建] 按钮。
|
||||
/// 右栏:选中武器的完整属性编辑 + HitBox Prefab 结构校验 + 快速操作。
|
||||
/// </summary>
|
||||
public class WeaponEditorWindow : EditorWindow
|
||||
{
|
||||
private static readonly StyleSheet _sharedUSS;
|
||||
|
||||
static WeaponEditorWindow()
|
||||
{
|
||||
_sharedUSS = AssetDatabase.LoadAssetAtPath<StyleSheet>(
|
||||
"Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss");
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Data/Weapon Editor", priority = 100)]
|
||||
public static void Open()
|
||||
{
|
||||
var wnd = GetWindow<WeaponEditorWindow>();
|
||||
wnd.titleContent = new GUIContent("Weapon Editor");
|
||||
wnd.minSize = new Vector2(680, 400);
|
||||
}
|
||||
|
||||
// ── 状态 ─────────────────────────────────────────────────────────────
|
||||
private List<WeaponSO> _weapons = new();
|
||||
private List<WeaponSO> _filtered = new();
|
||||
private ListView _listView;
|
||||
private VisualElement _detailRoot;
|
||||
private string _searchText = "";
|
||||
private InspectorElement _currentInspector;
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||||
|
||||
public void CreateGUI()
|
||||
{
|
||||
if (_sharedUSS != null)
|
||||
rootVisualElement.styleSheets.Add(_sharedUSS);
|
||||
|
||||
// Toolbar
|
||||
var toolbar = new Toolbar();
|
||||
var searchField = new ToolbarSearchField { style = { flexGrow = 1 } };
|
||||
searchField.RegisterValueChangedCallback(e =>
|
||||
{
|
||||
_searchText = e.newValue;
|
||||
RefreshFilter();
|
||||
});
|
||||
toolbar.Add(searchField);
|
||||
|
||||
var btnCreate = new ToolbarButton(CreateNewWeapon) { text = "+ 新建武器" };
|
||||
toolbar.Add(btnCreate);
|
||||
|
||||
var btnRefresh = new ToolbarButton(RefreshAll) { text = "↺" };
|
||||
btnRefresh.tooltip = "重新扫描 Project 中的 WeaponSO 资产";
|
||||
toolbar.Add(btnRefresh);
|
||||
|
||||
rootVisualElement.Add(toolbar);
|
||||
|
||||
// Split view
|
||||
var split = new TwoPaneSplitView(0, 220, TwoPaneSplitViewOrientation.Horizontal);
|
||||
|
||||
// ── 左栏 ──────────────────────────────────────────────────────
|
||||
var leftPane = new VisualElement { style = { minWidth = 140 } };
|
||||
|
||||
_listView = new ListView
|
||||
{
|
||||
selectionType = SelectionType.Single,
|
||||
fixedItemHeight = 22,
|
||||
makeItem = MakeListItem,
|
||||
bindItem = BindListItem,
|
||||
style = { flexGrow = 1 },
|
||||
};
|
||||
_listView.selectionChanged += OnSelectionChanged;
|
||||
leftPane.Add(_listView);
|
||||
split.Add(leftPane);
|
||||
|
||||
// ── 右栏 ──────────────────────────────────────────────────────
|
||||
_detailRoot = new ScrollView { style = { flexGrow = 1 } };
|
||||
_detailRoot.AddToClassList("detail-panel");
|
||||
split.Add(_detailRoot);
|
||||
|
||||
rootVisualElement.Add(split);
|
||||
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
private void OnFocus() => RefreshAll();
|
||||
|
||||
// ── 列表构建 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void RefreshAll()
|
||||
{
|
||||
_weapons = EditorScaffoldUtils.FindAllAssetsOfType<WeaponSO>();
|
||||
_weapons.Sort((a, b) => string.Compare(
|
||||
a.weaponId, b.weaponId, StringComparison.OrdinalIgnoreCase));
|
||||
RefreshFilter();
|
||||
}
|
||||
|
||||
private void RefreshFilter()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_searchText))
|
||||
{
|
||||
_filtered = new List<WeaponSO>(_weapons);
|
||||
}
|
||||
else
|
||||
{
|
||||
string s = _searchText;
|
||||
_filtered = _weapons.Where(w => w != null &&
|
||||
(w.weaponId?.Contains(s, StringComparison.OrdinalIgnoreCase) == true ||
|
||||
w.displayName?.Contains(s, StringComparison.OrdinalIgnoreCase) == true)).ToList();
|
||||
}
|
||||
|
||||
_listView.itemsSource = _filtered;
|
||||
_listView.Rebuild();
|
||||
}
|
||||
|
||||
private static VisualElement MakeListItem()
|
||||
{
|
||||
var label = new Label();
|
||||
label.AddToClassList("list-item");
|
||||
return label;
|
||||
}
|
||||
|
||||
private void BindListItem(VisualElement element, int index)
|
||||
{
|
||||
var label = (Label)element;
|
||||
var weapon = _filtered.Count > index ? _filtered[index] : null;
|
||||
if (weapon == null) { label.text = "(null)"; return; }
|
||||
|
||||
label.text = string.IsNullOrEmpty(weapon.displayName)
|
||||
? weapon.weaponId
|
||||
: $"{weapon.weaponId} <color=#888>({weapon.displayName})</color>";
|
||||
}
|
||||
|
||||
// ── 详情面板 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void OnSelectionChanged(IEnumerable<object> items)
|
||||
{
|
||||
_detailRoot.Clear();
|
||||
_currentInspector = null;
|
||||
|
||||
var weapon = items.FirstOrDefault() as WeaponSO;
|
||||
if (weapon == null) return;
|
||||
|
||||
// 标题
|
||||
var title = new Label(
|
||||
string.IsNullOrEmpty(weapon.displayName) ? weapon.weaponId : $"{weapon.weaponId} · {weapon.displayName}")
|
||||
{
|
||||
style =
|
||||
{
|
||||
fontSize = 14,
|
||||
unityFontStyleAndWeight = FontStyle.Bold,
|
||||
marginBottom = 6,
|
||||
}
|
||||
};
|
||||
_detailRoot.Add(title);
|
||||
|
||||
// HitBox Prefab 状态
|
||||
BuildHitBoxStatus(weapon);
|
||||
|
||||
// 连击链预览
|
||||
BuildComboPreview(weapon);
|
||||
|
||||
// Inspector 完整属性编辑
|
||||
_currentInspector = new InspectorElement(weapon);
|
||||
_detailRoot.Add(_currentInspector);
|
||||
|
||||
// 操作按钮
|
||||
var btnRow = new VisualElement();
|
||||
btnRow.AddToClassList("action-buttons");
|
||||
|
||||
var btnSelect = new Button(() => EditorScaffoldUtils.PingAndSelect(weapon))
|
||||
{ text = "在 Project 中定位" };
|
||||
var btnInspector = new Button(() => Selection.activeObject = weapon)
|
||||
{ text = "在 Inspector 中打开" };
|
||||
var btnWizard = new Button(WeaponHitBoxWizard.Open)
|
||||
{ text = "HitBox Prefab 向导…" };
|
||||
|
||||
btnRow.Add(btnSelect);
|
||||
btnRow.Add(btnInspector);
|
||||
btnRow.Add(btnWizard);
|
||||
_detailRoot.Add(btnRow);
|
||||
}
|
||||
|
||||
/// <summary>连击序列数值横排预览。</summary>
|
||||
private void BuildComboPreview(WeaponSO weapon)
|
||||
{
|
||||
if (weapon.groundComboSteps == null || weapon.groundComboSteps.Length == 0)
|
||||
return;
|
||||
|
||||
var section = new Label("连击链预览") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginBottom = 4 } };
|
||||
_detailRoot.Add(section);
|
||||
|
||||
var chain = new VisualElement();
|
||||
chain.AddToClassList("stats-preview");
|
||||
|
||||
for (int i = 0; i < weapon.groundComboSteps.Length; i++)
|
||||
{
|
||||
var step = weapon.groundComboSteps[i];
|
||||
bool addArrow = i < weapon.groundComboSteps.Length - 1;
|
||||
|
||||
var cell = new VisualElement
|
||||
{
|
||||
style =
|
||||
{
|
||||
alignItems = Align.Center,
|
||||
marginRight = 4,
|
||||
paddingLeft = 6,
|
||||
paddingRight = 6,
|
||||
paddingTop = 3,
|
||||
paddingBottom = 3,
|
||||
backgroundColor = new Color(0.25f, 0.25f, 0.28f, 1f),
|
||||
borderTopLeftRadius = 3,
|
||||
borderTopRightRadius = 3,
|
||||
borderBottomLeftRadius = 3,
|
||||
borderBottomRightRadius = 3,
|
||||
}
|
||||
};
|
||||
|
||||
cell.Add(new Label($"攻击{i + 1}")
|
||||
{
|
||||
style = { fontSize = 10, color = new Color(0.65f, 0.65f, 0.65f) }
|
||||
});
|
||||
|
||||
string clipName = step.clip?.Clip != null ? step.clip.Clip.name : "<无动画>";
|
||||
cell.Add(new Label(clipName)
|
||||
{
|
||||
style = { fontSize = 11, unityFontStyleAndWeight = FontStyle.Bold }
|
||||
});
|
||||
|
||||
if (step.damageSource != null)
|
||||
{
|
||||
int dmg = Mathf.RoundToInt(step.damageSource.BaseDamage * step.damageSource.DamageMultiplier);
|
||||
cell.Add(new Label($"伤害 {dmg} [{step.damageSource.BreakLevel}]")
|
||||
{
|
||||
style = { fontSize = 10, color = new Color(1f, 0.7f, 0.3f) }
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.Add(new Label("(无 DamageSource)")
|
||||
{
|
||||
style = { fontSize = 10, color = new Color(0.8f, 0.3f, 0.3f) }
|
||||
});
|
||||
}
|
||||
|
||||
chain.Add(cell);
|
||||
|
||||
if (addArrow)
|
||||
chain.Add(new Label("→") { style = { alignSelf = Align.Center, marginLeft = 2, marginRight = 2 } });
|
||||
}
|
||||
|
||||
_detailRoot.Add(chain);
|
||||
|
||||
// 追加空中/上/下攻击的简要行
|
||||
var extraRow = new VisualElement
|
||||
{
|
||||
style = { flexDirection = FlexDirection.Row, flexWrap = Wrap.Wrap, marginBottom = 6, paddingLeft = 6 }
|
||||
};
|
||||
|
||||
void ExtraStat(string label, DamageSourceSO src)
|
||||
{
|
||||
if (src == null) return;
|
||||
int dmg = Mathf.RoundToInt(src.BaseDamage * src.DamageMultiplier);
|
||||
extraRow.Add(new Label($"{label}:{dmg} [{src.BreakLevel}]")
|
||||
{
|
||||
style = { marginRight = 14, fontSize = 11, color = new Color(0.7f, 0.7f, 0.7f) }
|
||||
});
|
||||
}
|
||||
|
||||
ExtraStat("空中", weapon.airComboSteps?[0].damageSource);
|
||||
ExtraStat("上劈", weapon.upStep.damageSource);
|
||||
ExtraStat("下劈", weapon.downStep.damageSource);
|
||||
|
||||
if (extraRow.childCount > 0)
|
||||
_detailRoot.Add(extraRow);
|
||||
}
|
||||
|
||||
private void BuildHitBoxStatus(WeaponSO weapon)
|
||||
{
|
||||
HelpBoxMessageType msgType;
|
||||
string msg;
|
||||
|
||||
if (weapon.hitBoxPrefab == null)
|
||||
{
|
||||
msgType = HelpBoxMessageType.Warning;
|
||||
msg = "hitBoxPrefab 未赋值!请创建并关联武器 HitBox Prefab。";
|
||||
}
|
||||
else if (weapon.hitBoxPrefab.GetComponent<WeaponHitBoxInstance>() == null)
|
||||
{
|
||||
msgType = HelpBoxMessageType.Error;
|
||||
msg = $"hitBoxPrefab「{weapon.hitBoxPrefab.name}」缺少 WeaponHitBoxInstance 组件!";
|
||||
}
|
||||
else
|
||||
{
|
||||
msgType = HelpBoxMessageType.Info;
|
||||
msg = $"HitBox Prefab 结构正常:{weapon.hitBoxPrefab.name}";
|
||||
}
|
||||
|
||||
_detailRoot.Add(new HelpBox(msg, msgType) { style = { marginBottom = 6 } });
|
||||
}
|
||||
|
||||
// ── 新建武器 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void CreateNewWeapon()
|
||||
{
|
||||
var asset = EditorScaffoldUtils.CreateSOAsset<WeaponSO>(
|
||||
"Assets/_Game/Data/Combat/Weapons", "WPN_New");
|
||||
|
||||
if (asset != null)
|
||||
{
|
||||
RefreshAll();
|
||||
int idx = _filtered.IndexOf(asset);
|
||||
if (idx >= 0)
|
||||
_listView.SetSelection(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
8
Assets/_Game/Scripts/Editor/Hub.meta
Normal file
8
Assets/_Game/Scripts/Editor/Hub.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 54a83daf1b31e4c4e98beff7506eecb2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
262
Assets/_Game/Scripts/Editor/Hub/DataHubWindow.cs
Normal file
262
Assets/_Game/Scripts/Editor/Hub/DataHubWindow.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a95bf3e8be76e44881b0efa6a42f753
|
||||
guid: 95a89dac2a3cc7e439be075586617c88
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
28
Assets/_Game/Scripts/Editor/Hub/IDataModule.cs
Normal file
28
Assets/_Game/Scripts/Editor/Hub/IDataModule.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 15618e4fc32a98346a68e945428fcb47
|
||||
guid: 2cd7579b2889e0943883000232e468dc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
8
Assets/_Game/Scripts/Editor/Modules.meta
Normal file
8
Assets/_Game/Scripts/Editor/Modules.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 231b80ce7e59248449a7431b00a05b59
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
212
Assets/_Game/Scripts/Editor/Modules/BossSkillModule.cs
Normal file
212
Assets/_Game/Scripts/Editor/Modules/BossSkillModule.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7cc9d2828e2d3f9458e74befbb0e2b4e
|
||||
guid: f0d0425e529293e469da3762fe3bf8f0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
207
Assets/_Game/Scripts/Editor/Modules/EnemyModule.cs
Normal file
207
Assets/_Game/Scripts/Editor/Modules/EnemyModule.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 22de97a32c867fd429c1814853d61ec6
|
||||
guid: 32772b7d7bbc5824889620b773c352a8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
209
Assets/_Game/Scripts/Editor/Modules/FormModule.cs
Normal file
209
Assets/_Game/Scripts/Editor/Modules/FormModule.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/Modules/FormModule.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/Modules/FormModule.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa270e8dd563fbc429bdb342988b9a54
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
187
Assets/_Game/Scripts/Editor/Modules/SkillModule.cs
Normal file
187
Assets/_Game/Scripts/Editor/Modules/SkillModule.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/Modules/SkillModule.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/Modules/SkillModule.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7f77603c5dde1584eade768456618cef
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
184
Assets/_Game/Scripts/Editor/Modules/WeaponModule.cs
Normal file
184
Assets/_Game/Scripts/Editor/Modules/WeaponModule.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/Modules/WeaponModule.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/Modules/WeaponModule.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7b4131880a180b34d9f619b70813edb8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
158
Assets/_Game/Scripts/Editor/Shared/AssetOperations.cs
Normal file
158
Assets/_Game/Scripts/Editor/Shared/AssetOperations.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/Shared/AssetOperations.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/Shared/AssetOperations.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2212f3dc47d61dd42b245a2470d2a90a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
123
Assets/_Game/Scripts/Editor/Shared/DetailHeader.cs
Normal file
123
Assets/_Game/Scripts/Editor/Shared/DetailHeader.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/Shared/DetailHeader.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/Shared/DetailHeader.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b4a51b9cef4da264fb261dac2e74700e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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}_Stats,例:ENM_E001_Stats") },
|
||||
{ "LootTableSO", ("ENM_", "ENM_E{ID}_Loot,例:ENM_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>
|
||||
|
||||
259
Assets/_Game/Scripts/Editor/Shared/SoListPane.cs
Normal file
259
Assets/_Game/Scripts/Editor/Shared/SoListPane.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/Shared/SoListPane.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/Shared/SoListPane.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 011ed46e4350a784f88ae3687ce76197
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.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.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.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.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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user