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_Enabled: 1
|
||||||
m_EditorHideFlags: 0
|
m_EditorHideFlags: 0
|
||||||
m_Script: {fileID: 11500000, guid: d2443d04d1c179d4d8a4f36e7ca7156e, type: 3}
|
m_Script: {fileID: 11500000, guid: d2443d04d1c179d4d8a4f36e7ca7156e, type: 3}
|
||||||
m_Name: Weapon_DiHun
|
m_Name: WPN_New
|
||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
weaponId:
|
weaponId:
|
||||||
displayName:
|
displayName:
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 76de8c0ba36dcce4db8990c5b62e9ed8
|
guid: ce7bc9bad6f58ec42baff08f5353340e
|
||||||
NativeFormatImporter:
|
NativeFormatImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
mainObjectFileID: 11400000
|
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
|
fileFormatVersion: 2
|
||||||
guid: f80486c9cd3d1db459ce6df915b11546
|
guid: e0cf93e053ead744fa1876771ba0d081
|
||||||
NativeFormatImporter:
|
NativeFormatImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
mainObjectFileID: 11400000
|
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
|
fileFormatVersion: 2
|
||||||
guid: d87ae01ed8a2b4f4cb27d159a52d1a14
|
guid: a5d737a5b9641124aafb375d8684e06a
|
||||||
NativeFormatImporter:
|
NativeFormatImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
mainObjectFileID: 11400000
|
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}
|
m_GameObject: {fileID: 123526430}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
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_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
@@ -3885,7 +3885,7 @@ Transform:
|
|||||||
m_GameObject: {fileID: 173935938}
|
m_GameObject: {fileID: 173935938}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
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_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
@@ -5123,7 +5123,7 @@ Transform:
|
|||||||
m_GameObject: {fileID: 225562483}
|
m_GameObject: {fileID: 225562483}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
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_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
@@ -8598,6 +8598,7 @@ MonoBehaviour:
|
|||||||
_onCharmEquipped: {fileID: 0}
|
_onCharmEquipped: {fileID: 0}
|
||||||
_onCharmUnequipped: {fileID: 0}
|
_onCharmUnequipped: {fileID: 0}
|
||||||
_onEquipmentChanged: {fileID: 0}
|
_onEquipmentChanged: {fileID: 0}
|
||||||
|
_onAchievementNotchGranted: {fileID: 0}
|
||||||
--- !u!114 &430284915
|
--- !u!114 &430284915
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -18864,7 +18865,7 @@ Transform:
|
|||||||
m_GameObject: {fileID: 1108462349}
|
m_GameObject: {fileID: 1108462349}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
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_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
@@ -18895,7 +18896,7 @@ Transform:
|
|||||||
m_GameObject: {fileID: 1112393102}
|
m_GameObject: {fileID: 1112393102}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
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_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
@@ -21066,7 +21067,7 @@ MonoBehaviour:
|
|||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
_linkedDoor: {fileID: 1167097821}
|
_linkedDoor: {fileID: 1167097821}
|
||||||
_spawnPoint: {fileID: 1655723310}
|
_spawnPoint: {fileID: 1655723310}
|
||||||
_facingDirectionOnArrive: 1
|
_facingDirectionOnArrive: -1
|
||||||
_autoTrigger: 1
|
_autoTrigger: 1
|
||||||
_transitionOut: {fileID: 0}
|
_transitionOut: {fileID: 0}
|
||||||
_transitionIn: {fileID: 0}
|
_transitionIn: {fileID: 0}
|
||||||
@@ -27623,7 +27624,7 @@ Transform:
|
|||||||
m_GameObject: {fileID: 1655723309}
|
m_GameObject: {fileID: 1655723309}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
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_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
|
|||||||
@@ -182,9 +182,7 @@ namespace BaseGames.Editor
|
|||||||
root.Add(MakeSectionHeader("▶ 专项编辑器"));
|
root.Add(MakeSectionHeader("▶ 专项编辑器"));
|
||||||
|
|
||||||
var jumpGroup = MakeActionGroup();
|
var jumpGroup = MakeActionGroup();
|
||||||
jumpGroup.Add(MakeJumpButton("武器编辑器", () => Combat.WeaponEditorWindow.Open()));
|
jumpGroup.Add(MakeJumpButton("Data Hub(武器/技能/形态)", DataHubWindow.Open));
|
||||||
jumpGroup.Add(MakeJumpButton("技能编辑器", () => Skills.SkillEditorWindow.Open()));
|
|
||||||
jumpGroup.Add(MakeJumpButton("形态编辑器", () => FormEditorWindow.Open()));
|
|
||||||
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
|
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
|
||||||
root.Add(jumpGroup);
|
root.Add(jumpGroup);
|
||||||
|
|
||||||
@@ -248,7 +246,7 @@ namespace BaseGames.Editor
|
|||||||
root.Add(MakeSectionHeader("▶ 专项编辑器"));
|
root.Add(MakeSectionHeader("▶ 专项编辑器"));
|
||||||
|
|
||||||
var jumpGroup = MakeActionGroup();
|
var jumpGroup = MakeActionGroup();
|
||||||
jumpGroup.Add(MakeJumpButton("敌人数据管理", () => Enemies.EnemyDataWindow.Open()));
|
jumpGroup.Add(MakeJumpButton("Data Hub(敌人数据)", DataHubWindow.Open));
|
||||||
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
|
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
|
||||||
root.Add(jumpGroup);
|
root.Add(jumpGroup);
|
||||||
|
|
||||||
@@ -295,7 +293,7 @@ namespace BaseGames.Editor
|
|||||||
|
|
||||||
var jumpGroup = MakeActionGroup();
|
var jumpGroup = MakeActionGroup();
|
||||||
jumpGroup.Add(MakeJumpButton("Boss 技能序列查看器", BossSkillSequenceWindow.OpenWindow));
|
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));
|
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
|
||||||
root.Add(jumpGroup);
|
root.Add(jumpGroup);
|
||||||
|
|
||||||
@@ -763,7 +761,7 @@ namespace BaseGames.Editor
|
|||||||
{
|
{
|
||||||
var sep = new VisualElement();
|
var sep = new VisualElement();
|
||||||
sep.style.height = 1;
|
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.marginTop = 8;
|
||||||
sep.style.marginBottom = 8;
|
sep.style.marginBottom = 8;
|
||||||
return sep;
|
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
|
fileFormatVersion: 2
|
||||||
guid: 3a95bf3e8be76e44881b0efa6a42f753
|
guid: 95a89dac2a3cc7e439be075586617c88
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 2
|
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
|
fileFormatVersion: 2
|
||||||
guid: 15618e4fc32a98346a68e945428fcb47
|
guid: 2cd7579b2889e0943883000232e468dc
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 2
|
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
|
fileFormatVersion: 2
|
||||||
guid: 7cc9d2828e2d3f9458e74befbb0e2b4e
|
guid: f0d0425e529293e469da3762fe3bf8f0
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 2
|
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
|
fileFormatVersion: 2
|
||||||
guid: 22de97a32c867fd429c1814853d61ec6
|
guid: 32772b7d7bbc5824889620b773c352a8
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 2
|
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 System.IO;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using UnityEngine.UIElements;
|
||||||
|
|
||||||
namespace BaseGames.Editor
|
namespace BaseGames.Editor
|
||||||
{
|
{
|
||||||
@@ -11,6 +12,139 @@ namespace BaseGames.Editor
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class EditorScaffoldUtils
|
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 资产创建 ───────────────────────────────────────────────────────
|
// ── SO 资产创建 ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -37,6 +171,68 @@ namespace BaseGames.Editor
|
|||||||
return asset;
|
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>
|
/// <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 TypeName;
|
||||||
public string AssetPath;
|
public string AssetPath;
|
||||||
public ScriptableObject Asset;
|
public ScriptableObject Asset;
|
||||||
|
public string Guid;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly List<CategoryEntry> _categories = new();
|
private readonly List<CategoryEntry> _categories = new();
|
||||||
@@ -50,6 +51,7 @@ namespace BaseGames.Editor
|
|||||||
private ListView _assetList;
|
private ListView _assetList;
|
||||||
private TextField _searchField;
|
private TextField _searchField;
|
||||||
private Label _statusLabel;
|
private Label _statusLabel;
|
||||||
|
private VisualElement _renameContainer;
|
||||||
|
|
||||||
// ── 菜单入口 ──────────────────────────────────────────────────────────
|
// ── 菜单入口 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -141,12 +143,16 @@ namespace BaseGames.Editor
|
|||||||
var rightPane = new VisualElement();
|
var rightPane = new VisualElement();
|
||||||
rightPane.style.flexDirection = FlexDirection.Column;
|
rightPane.style.flexDirection = FlexDirection.Column;
|
||||||
|
|
||||||
// 列标题行
|
// 列标题行:paddingRight 额外加 13px(滚动条宽度)确保列对齐
|
||||||
var colHeader = new VisualElement();
|
var colHeader = new VisualElement();
|
||||||
colHeader.AddToClassList("pane-header");
|
colHeader.AddToClassList("pane-header");
|
||||||
colHeader.style.flexDirection = FlexDirection.Row;
|
colHeader.style.flexDirection = FlexDirection.Row;
|
||||||
|
colHeader.style.paddingLeft = 8;
|
||||||
|
colHeader.style.paddingRight = 21; // 8 + 13 (滚动条占位)
|
||||||
|
colHeader.style.paddingTop = 4;
|
||||||
|
colHeader.style.paddingBottom = 4;
|
||||||
colHeader.Add(MakeHeaderLabel("资产名", true, 0));
|
colHeader.Add(MakeHeaderLabel("资产名", true, 0));
|
||||||
colHeader.Add(MakeHeaderLabel("类型", false, 170));
|
colHeader.Add(MakeHeaderLabel("类型", false, 190));
|
||||||
colHeader.Add(MakeHeaderLabel("路径", true, 0));
|
colHeader.Add(MakeHeaderLabel("路径", true, 0));
|
||||||
rightPane.Add(colHeader);
|
rightPane.Add(colHeader);
|
||||||
|
|
||||||
@@ -160,19 +166,30 @@ namespace BaseGames.Editor
|
|||||||
_assetList.style.flexGrow = 1;
|
_assetList.style.flexGrow = 1;
|
||||||
_assetList.selectionChanged += _ => OnAssetPicked();
|
_assetList.selectionChanged += _ => OnAssetPicked();
|
||||||
_assetList.itemsChosen += _ => FocusProjectWindow();
|
_assetList.itemsChosen += _ => FocusProjectWindow();
|
||||||
|
_assetList.AddManipulator(new ContextualMenuManipulator(BuildAssetContextMenu));
|
||||||
rightPane.Add(_assetList);
|
rightPane.Add(_assetList);
|
||||||
|
|
||||||
|
// 滚动条常显,确保 header 列与内容列对齐
|
||||||
|
_assetList.schedule.Execute(() =>
|
||||||
|
{
|
||||||
|
var sv = _assetList.Q<ScrollView>();
|
||||||
|
if (sv != null) sv.verticalScrollerVisibility = ScrollerVisibility.AlwaysVisible;
|
||||||
|
});
|
||||||
|
|
||||||
// 状态栏
|
// 状态栏
|
||||||
_statusLabel = new Label("—");
|
_statusLabel = new Label("—");
|
||||||
_statusLabel.style.paddingLeft = 8;
|
_statusLabel.style.paddingLeft = 8;
|
||||||
_statusLabel.style.paddingTop = 3;
|
_statusLabel.style.paddingTop = 3;
|
||||||
_statusLabel.style.paddingBottom = 3;
|
_statusLabel.style.paddingBottom = 3;
|
||||||
_statusLabel.style.borderTopWidth = 1;
|
_statusLabel.style.borderTopWidth = 1;
|
||||||
_statusLabel.style.borderTopColor = new Color(0.15f, 0.15f, 0.15f);
|
_statusLabel.style.borderTopColor = new Color(0.0f, 0.0f, 0.0f, 0.20f);
|
||||||
_statusLabel.style.color = new Color(0.58f, 0.58f, 0.58f);
|
_statusLabel.AddToClassList("so-path-label");
|
||||||
_statusLabel.style.fontSize = 11;
|
|
||||||
rightPane.Add(_statusLabel);
|
rightPane.Add(_statusLabel);
|
||||||
|
|
||||||
|
// 重命名容器(选中资产后显示)
|
||||||
|
_renameContainer = new VisualElement { style = { display = DisplayStyle.None } };
|
||||||
|
rightPane.Add(_renameContainer);
|
||||||
|
|
||||||
split.Add(leftPane);
|
split.Add(leftPane);
|
||||||
split.Add(rightPane);
|
split.Add(rightPane);
|
||||||
rootVisualElement.Add(split);
|
rootVisualElement.Add(split);
|
||||||
@@ -182,7 +199,9 @@ namespace BaseGames.Editor
|
|||||||
{
|
{
|
||||||
var lbl = new Label(text);
|
var lbl = new Label(text);
|
||||||
lbl.style.unityFontStyleAndWeight = FontStyle.Bold;
|
lbl.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||||
|
lbl.style.unityTextAlign = TextAnchor.MiddleLeft;
|
||||||
lbl.style.overflow = Overflow.Hidden;
|
lbl.style.overflow = Overflow.Hidden;
|
||||||
|
lbl.style.minWidth = 0;
|
||||||
if (grow) lbl.style.flexGrow = 1;
|
if (grow) lbl.style.flexGrow = 1;
|
||||||
if (fixedWidth > 0) lbl.style.width = fixedWidth;
|
if (fixedWidth > 0) lbl.style.width = fixedWidth;
|
||||||
return lbl;
|
return lbl;
|
||||||
@@ -219,22 +238,22 @@ namespace BaseGames.Editor
|
|||||||
|
|
||||||
var nameEl = new Label { name = "n" };
|
var nameEl = new Label { name = "n" };
|
||||||
nameEl.style.flexGrow = 1;
|
nameEl.style.flexGrow = 1;
|
||||||
|
nameEl.style.minWidth = 0;
|
||||||
nameEl.style.overflow = Overflow.Hidden;
|
nameEl.style.overflow = Overflow.Hidden;
|
||||||
nameEl.style.textOverflow = TextOverflow.Ellipsis;
|
nameEl.style.textOverflow = TextOverflow.Ellipsis;
|
||||||
|
|
||||||
var typeEl = new Label { name = "t" };
|
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.overflow = Overflow.Hidden;
|
||||||
typeEl.style.textOverflow = TextOverflow.Ellipsis;
|
typeEl.style.textOverflow = TextOverflow.Ellipsis;
|
||||||
typeEl.style.color = new Color(0.52f, 0.80f, 1.00f);
|
typeEl.AddToClassList("so-type-label");
|
||||||
typeEl.style.fontSize = 11;
|
|
||||||
|
|
||||||
var pathEl = new Label { name = "p" };
|
var pathEl = new Label { name = "p" };
|
||||||
pathEl.style.flexGrow = 1;
|
pathEl.style.flexGrow = 1;
|
||||||
pathEl.style.overflow = Overflow.Hidden;
|
pathEl.style.overflow = Overflow.Hidden;
|
||||||
pathEl.style.textOverflow = TextOverflow.Ellipsis;
|
pathEl.style.textOverflow = TextOverflow.Ellipsis;
|
||||||
pathEl.style.color = new Color(0.48f, 0.48f, 0.48f);
|
pathEl.AddToClassList("so-path-label");
|
||||||
pathEl.style.fontSize = 10;
|
|
||||||
|
|
||||||
row.Add(nameEl);
|
row.Add(nameEl);
|
||||||
row.Add(typeEl);
|
row.Add(typeEl);
|
||||||
@@ -262,11 +281,38 @@ namespace BaseGames.Editor
|
|||||||
private void OnAssetPicked()
|
private void OnAssetPicked()
|
||||||
{
|
{
|
||||||
int idx = _assetList.selectedIndex;
|
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;
|
var asset = _filtered[idx].Asset;
|
||||||
if (asset == null) return;
|
if (asset == null)
|
||||||
|
{
|
||||||
|
_renameContainer.style.display = DisplayStyle.None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
EditorGUIUtility.PingObject(asset);
|
EditorGUIUtility.PingObject(asset);
|
||||||
Selection.activeObject = 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()
|
private static void FocusProjectWindow()
|
||||||
@@ -293,6 +339,7 @@ namespace BaseGames.Editor
|
|||||||
TypeName = asset.GetType().Name,
|
TypeName = asset.GetType().Name,
|
||||||
AssetPath = path,
|
AssetPath = path,
|
||||||
Asset = asset,
|
Asset = asset,
|
||||||
|
Guid = guid,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,6 +378,26 @@ namespace BaseGames.Editor
|
|||||||
ApplyFilter();
|
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()
|
private void ApplyFilter()
|
||||||
{
|
{
|
||||||
_filtered.Clear();
|
_filtered.Clear();
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
/* BaseGames Editor 统一样式表
|
/* BaseGames Editor 统一样式表
|
||||||
路径: Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss */
|
路径: Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss
|
||||||
|
原则:不硬编码文字颜色,让 Unity 主题决定前景色 */
|
||||||
|
|
||||||
/* ── 分区标题 ──────────────────────────────────────────── */
|
/* ── 分区标题 ──────────────────────────────────────────── */
|
||||||
.section-header {
|
.section-header {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
-unity-font-style: bold;
|
-unity-font-style: bold;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
color: rgb(180, 180, 180);
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 列表项 ─────────────────────────────────────────────── */
|
/* ── 列表项 ─────────────────────────────────────────────── */
|
||||||
@@ -49,16 +50,41 @@
|
|||||||
.stats-preview {
|
.stats-preview {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
padding: 4px 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 {
|
.tab-bar {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
border-bottom-color: rgb(60, 60, 60);
|
border-bottom-color: rgba(128, 128, 128, 0.30);
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
.tab-button {
|
.tab-button {
|
||||||
@@ -66,13 +92,14 @@
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
border-width: 0;
|
border-width: 0;
|
||||||
background-color: rgba(0, 0, 0, 0);
|
background-color: rgba(0, 0, 0, 0);
|
||||||
color: rgb(160, 160, 160);
|
opacity: 0.65;
|
||||||
}
|
}
|
||||||
.tab-button:hover {
|
.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 {
|
.tab-button--active {
|
||||||
color: rgb(255, 255, 255);
|
opacity: 1.0;
|
||||||
border-bottom-width: 2px;
|
border-bottom-width: 2px;
|
||||||
border-bottom-color: rgb(100, 160, 255);
|
border-bottom-color: rgb(100, 160, 255);
|
||||||
}
|
}
|
||||||
@@ -88,62 +115,61 @@
|
|||||||
border-bottom-width: 2px;
|
border-bottom-width: 2px;
|
||||||
border-bottom-color: rgba(0,0,0,0);
|
border-bottom-color: rgba(0,0,0,0);
|
||||||
background-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;
|
-unity-font-style: bold;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
.tab-btn:hover {
|
.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 {
|
.tab-btn--active {
|
||||||
color: rgb(255, 255, 255);
|
opacity: 1.0;
|
||||||
border-bottom-width: 2px;
|
border-bottom-width: 2px;
|
||||||
border-bottom-color: rgb(90, 160, 255);
|
border-bottom-color: rgb(90, 160, 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* SO 工厂按钮(绿调) */
|
/* SO 工厂按钮(绿调) */
|
||||||
.wizard-factory-btn {
|
.wizard-factory-btn {
|
||||||
background-color: rgba(40, 100, 55, 0.75);
|
background-color: rgba(40, 120, 60, 0.55);
|
||||||
color: rgb(200, 255, 210);
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-color: rgba(80, 180, 100, 0.60);
|
border-color: rgba(80, 180, 100, 0.50);
|
||||||
}
|
}
|
||||||
.wizard-factory-btn:hover {
|
.wizard-factory-btn:hover {
|
||||||
background-color: rgba(55, 130, 70, 0.90);
|
background-color: rgba(50, 145, 72, 0.70);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 场景放置按钮(蓝调) */
|
/* 场景放置按钮(蓝调) */
|
||||||
.wizard-scene-btn {
|
.wizard-scene-btn {
|
||||||
background-color: rgba(30, 60, 120, 0.80);
|
background-color: rgba(30, 75, 150, 0.55);
|
||||||
color: rgb(190, 220, 255);
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-color: rgba(80, 130, 220, 0.55);
|
border-color: rgba(80, 130, 220, 0.50);
|
||||||
}
|
}
|
||||||
.wizard-scene-btn:hover {
|
.wizard-scene-btn:hover {
|
||||||
background-color: rgba(40, 80, 155, 0.95);
|
background-color: rgba(40, 95, 180, 0.70);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 跳转按钮(中性灰调) */
|
/* 跳转按钮(无背景,仅边框区分) */
|
||||||
.wizard-jump-btn {
|
.wizard-jump-btn {
|
||||||
background-color: rgba(55, 55, 60, 0.80);
|
background-color: rgba(0, 0, 0, 0);
|
||||||
color: rgb(210, 210, 220);
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-color: rgba(120, 120, 130, 0.45);
|
border-color: rgba(128, 128, 128, 0.45);
|
||||||
}
|
}
|
||||||
.wizard-jump-btn:hover {
|
.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 {
|
.wizard-create-all-btn {
|
||||||
background-color: rgba(40, 80, 155, 0.85);
|
background-color: rgba(40, 80, 155, 0.55);
|
||||||
color: rgb(210, 230, 255);
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
@@ -166,7 +191,7 @@
|
|||||||
height: 28px;
|
height: 28px;
|
||||||
}
|
}
|
||||||
.wizard-create-all-btn:hover {
|
.wizard-create-all-btn:hover {
|
||||||
background-color: rgba(55, 100, 180, 0.95);
|
background-color: rgba(55, 100, 180, 0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── SO 状态芯片(圆角徽章,替代 MakeStatusGrid 内联颜色)── */
|
/* ── SO 状态芯片(圆角徽章,替代 MakeStatusGrid 内联颜色)── */
|
||||||
@@ -178,13 +203,12 @@
|
|||||||
padding-right: 6px;
|
padding-right: 6px;
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
background-color: rgba(30, 105, 50, 0.90);
|
background-color: rgba(35, 115, 55, 0.55);
|
||||||
color: rgb(175, 245, 185);
|
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-color: rgba(65, 165, 80, 0.55);
|
border-color: rgba(80, 185, 100, 0.55);
|
||||||
}
|
}
|
||||||
.status-chip--ok:hover {
|
.status-chip--ok:hover {
|
||||||
background-color: rgba(40, 130, 62, 0.95);
|
background-color: rgba(50, 145, 70, 0.75);
|
||||||
}
|
}
|
||||||
.status-chip--missing {
|
.status-chip--missing {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -194,8 +218,7 @@
|
|||||||
padding-right: 6px;
|
padding-right: 6px;
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
background-color: rgba(145, 55, 10, 0.90);
|
background-color: rgba(145, 55, 10, 0.55);
|
||||||
color: rgb(255, 210, 155);
|
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-color: rgba(195, 100, 25, 0.55);
|
border-color: rgba(195, 100, 25, 0.55);
|
||||||
}
|
}
|
||||||
@@ -217,3 +240,22 @@
|
|||||||
background-color: rgba(128, 128, 128, 0.08);
|
background-color: rgba(128, 128, 128, 0.08);
|
||||||
-unity-font-style: bold;
|
-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