feat: 젬 테스트 경로 및 보스 기절 디버그 추가
- 다중 젬 슬롯용 타입을 별도 스크립트로 분리하고 테스트 젬/로드아웃 자산 생성 경로를 정리 - 젬 테스트 전용 공격 스킬과 분리된 애니메이션 자산을 추가해 베이스 스킬 검증 경로를 마련 - PlayerSkillDebugMenu와 MPP 디버그 메뉴를 보강해 젬 프리셋 적용, 원격 테스트, 보스 기절 디버그 메뉴를 추가 - BossCombatBehaviorContext와 공통 BT 액션이 기절 상태를 존중하도록 수정해 보스 추적과 패턴 실행을 중단 - Unity 리프레시와 외부 빌드 통과를 확인하고 드로그전 및 MPP 기준 젬 프리셋 적용 흐름을 검증
This commit is contained in:
BIN
Assets/_Game/Animations/Anim_Common_젬테스트공격.fbx
Normal file
BIN
Assets/_Game/Animations/Anim_Common_젬테스트공격.fbx
Normal file
Binary file not shown.
333
Assets/_Game/Animations/Anim_Common_젬테스트공격.fbx.meta
Normal file
333
Assets/_Game/Animations/Anim_Common_젬테스트공격.fbx.meta
Normal file
@@ -0,0 +1,333 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1a2314cec0db9814f90aaa68fc5ce4bd
|
||||
ModelImporter:
|
||||
serializedVersion: 24200
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
materials:
|
||||
materialImportMode: 2
|
||||
materialName: 0
|
||||
materialSearch: 1
|
||||
materialLocation: 1
|
||||
animations:
|
||||
legacyGenerateAnimations: 4
|
||||
bakeSimulation: 0
|
||||
resampleCurves: 1
|
||||
optimizeGameObjects: 0
|
||||
removeConstantScaleCurves: 0
|
||||
motionNodeName: root
|
||||
animationImportErrors:
|
||||
animationImportWarnings:
|
||||
animationRetargetingWarnings:
|
||||
animationDoRetargetingWarnings: 0
|
||||
importAnimatedCustomProperties: 0
|
||||
importConstraints: 0
|
||||
animationCompression: 0
|
||||
animationRotationError: 0.5
|
||||
animationPositionError: 0.5
|
||||
animationScaleError: 0.5
|
||||
animationWrapMode: 0
|
||||
extraExposedTransformPaths: []
|
||||
extraUserProperties: []
|
||||
clipAnimations:
|
||||
- serializedVersion: 16
|
||||
name: A_MOD_SWD_Attack_GemTest_RM_Neut
|
||||
takeName: A_MOD_SWD_Attack_HeavyStab01_RM_Neut
|
||||
internalID: -8689311932429934276
|
||||
firstFrame: 1
|
||||
lastFrame: 33
|
||||
wrapMode: 0
|
||||
orientationOffsetY: 0
|
||||
level: 0
|
||||
cycleOffset: 0
|
||||
loop: 0
|
||||
hasAdditiveReferencePose: 0
|
||||
loopTime: 0
|
||||
loopBlend: 0
|
||||
loopBlendOrientation: 0
|
||||
loopBlendPositionY: 0
|
||||
loopBlendPositionXZ: 0
|
||||
keepOriginalOrientation: 0
|
||||
keepOriginalPositionY: 1
|
||||
keepOriginalPositionXZ: 0
|
||||
heightFromFeet: 0
|
||||
mirror: 0
|
||||
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
|
||||
curves: []
|
||||
events:
|
||||
- time: 0.6
|
||||
functionName: OnEffect
|
||||
data:
|
||||
objectReferenceParameter: {instanceID: 0}
|
||||
floatParameter: 0
|
||||
intParameter: 0
|
||||
messageOptions: 0
|
||||
- time: 1.0
|
||||
functionName: OnSkillEnd
|
||||
data:
|
||||
objectReferenceParameter: {instanceID: 0}
|
||||
floatParameter: 0
|
||||
intParameter: 0
|
||||
messageOptions: 0
|
||||
transformMask:
|
||||
- path:
|
||||
weight: 1
|
||||
- path: root
|
||||
weight: 1
|
||||
- path: root/ik_foot_root
|
||||
weight: 1
|
||||
- path: root/ik_foot_root/ik_foot_l
|
||||
weight: 1
|
||||
- path: root/ik_foot_root/ik_foot_r
|
||||
weight: 1
|
||||
- path: root/ik_hand_root
|
||||
weight: 1
|
||||
- path: root/ik_hand_root/ik_hand_gun
|
||||
weight: 1
|
||||
- path: root/ik_hand_root/ik_hand_gun/ik_hand_l
|
||||
weight: 1
|
||||
- path: root/ik_hand_root/ik_hand_gun/ik_hand_r
|
||||
weight: 1
|
||||
- path: root/pelvis
|
||||
weight: 1
|
||||
- path: root/pelvis/hipAttach_l
|
||||
weight: 1
|
||||
- path: root/pelvis/hipAttach_r
|
||||
weight: 1
|
||||
- path: root/pelvis/hipAttachBack
|
||||
weight: 1
|
||||
- path: root/pelvis/hipAttachFront
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/backAttach
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_l/shoulderAttach_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_l/upperarm_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_l/upperarm_l/lowerarm_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_l/upperarm_l/lowerarm_l/elbowAttach_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_l/upperarm_l/lowerarm_l/hand_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_l/upperarm_l/lowerarm_l/hand_l/index_01_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_l/upperarm_l/lowerarm_l/hand_l/index_01_l/index_02_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_l/upperarm_l/lowerarm_l/hand_l/index_01_l/index_02_l/index_03_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_l/upperarm_l/lowerarm_l/hand_l/middle_01_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_l/upperarm_l/lowerarm_l/hand_l/middle_01_l/middle_02_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_l/upperarm_l/lowerarm_l/hand_l/middle_01_l/middle_02_l/middle_03_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_l/upperarm_l/lowerarm_l/hand_l/pinky_01_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_l/upperarm_l/lowerarm_l/hand_l/pinky_01_l/pinky_02_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_l/upperarm_l/lowerarm_l/hand_l/pinky_01_l/pinky_02_l/pinky_03_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_l/upperarm_l/lowerarm_l/hand_l/prop_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_l/upperarm_l/lowerarm_l/hand_l/ring_01_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_l/upperarm_l/lowerarm_l/hand_l/ring_01_l/ring_02_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_l/upperarm_l/lowerarm_l/hand_l/ring_01_l/ring_02_l/ring_03_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_l/upperarm_l/lowerarm_l/hand_l/thumb_01_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_l/upperarm_l/lowerarm_l/hand_l/thumb_01_l/thumb_02_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_l/upperarm_l/lowerarm_l/hand_l/thumb_01_l/thumb_02_l/thumb_03_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_l/upperarm_l/lowerarm_l/lowerarm_twist_01_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_l/upperarm_l/upperarm_twist_01_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_r/shoulderAttach_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_r/upperarm_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_r/upperarm_r/lowerarm_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_r/upperarm_r/lowerarm_r/elbowAttach_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_r/upperarm_r/lowerarm_r/hand_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_r/upperarm_r/lowerarm_r/hand_r/index_01_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_r/upperarm_r/lowerarm_r/hand_r/index_01_r/index_02_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_r/upperarm_r/lowerarm_r/hand_r/index_01_r/index_02_r/index_03_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_r/upperarm_r/lowerarm_r/hand_r/middle_01_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_r/upperarm_r/lowerarm_r/hand_r/middle_01_r/middle_02_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_r/upperarm_r/lowerarm_r/hand_r/middle_01_r/middle_02_r/middle_03_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_r/upperarm_r/lowerarm_r/hand_r/pinky_01_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_r/upperarm_r/lowerarm_r/hand_r/pinky_01_r/pinky_02_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_r/upperarm_r/lowerarm_r/hand_r/pinky_01_r/pinky_02_r/pinky_03_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_r/upperarm_r/lowerarm_r/hand_r/prop_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_r/upperarm_r/lowerarm_r/hand_r/ring_01_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_r/upperarm_r/lowerarm_r/hand_r/ring_01_r/ring_02_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_r/upperarm_r/lowerarm_r/hand_r/ring_01_r/ring_02_r/ring_03_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_r/upperarm_r/lowerarm_r/hand_r/thumb_01_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_r/upperarm_r/lowerarm_r/hand_r/thumb_01_r/thumb_02_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_r/upperarm_r/lowerarm_r/hand_r/thumb_01_r/thumb_02_r/thumb_03_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_r/upperarm_r/lowerarm_r/lowerarm_twist_01_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/clavicle_r/upperarm_r/upperarm_twist_01_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/neck_01
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/neck_01/head
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/neck_01/head/eye_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/neck_01/head/eye_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/neck_01/head/eyeLight_l
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/neck_01/head/eyeLight_r
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/neck_01/head/faceAttach
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/neck_01/head/headAttach
|
||||
weight: 1
|
||||
- path: root/pelvis/spine_01/spine_02/spine_03/neck_01/head/jaw
|
||||
weight: 1
|
||||
- path: root/pelvis/thigh_l
|
||||
weight: 1
|
||||
- path: root/pelvis/thigh_l/calf_l
|
||||
weight: 1
|
||||
- path: root/pelvis/thigh_l/calf_l/calf_twist_01_l
|
||||
weight: 1
|
||||
- path: root/pelvis/thigh_l/calf_l/foot_l
|
||||
weight: 1
|
||||
- path: root/pelvis/thigh_l/calf_l/foot_l/ball_l
|
||||
weight: 1
|
||||
- path: root/pelvis/thigh_l/calf_l/kneeAttach_l
|
||||
weight: 1
|
||||
- path: root/pelvis/thigh_l/thigh_twist_01_l
|
||||
weight: 1
|
||||
- path: root/pelvis/thigh_r
|
||||
weight: 1
|
||||
- path: root/pelvis/thigh_r/calf_r
|
||||
weight: 1
|
||||
- path: root/pelvis/thigh_r/calf_r/calf_twist_01_r
|
||||
weight: 1
|
||||
- path: root/pelvis/thigh_r/calf_r/foot_r
|
||||
weight: 1
|
||||
- path: root/pelvis/thigh_r/calf_r/foot_r/ball_r
|
||||
weight: 1
|
||||
- path: root/pelvis/thigh_r/calf_r/kneeAttach_r
|
||||
weight: 1
|
||||
- path: root/pelvis/thigh_r/thigh_twist_01_r
|
||||
weight: 1
|
||||
- path: SK_DMMY_BASE_01_00BODY
|
||||
weight: 1
|
||||
maskType: 1
|
||||
maskSource: {fileID: 31900000, guid: 3daacf102d24acb4aae029057b824d13, type: 2}
|
||||
additiveReferencePoseFrame: 0
|
||||
isReadable: 0
|
||||
meshes:
|
||||
lODScreenPercentages: []
|
||||
globalScale: 1
|
||||
meshCompression: 0
|
||||
addColliders: 0
|
||||
useSRGBMaterialColor: 1
|
||||
sortHierarchyByName: 1
|
||||
importPhysicalCameras: 1
|
||||
importVisibility: 1
|
||||
importBlendShapes: 1
|
||||
importCameras: 1
|
||||
importLights: 1
|
||||
nodeNameCollisionStrategy: 1
|
||||
fileIdsGeneration: 2
|
||||
swapUVChannels: 0
|
||||
generateSecondaryUV: 0
|
||||
useFileUnits: 1
|
||||
keepQuads: 0
|
||||
weldVertices: 1
|
||||
bakeAxisConversion: 0
|
||||
preserveHierarchy: 0
|
||||
skinWeightsMode: 0
|
||||
maxBonesPerVertex: 4
|
||||
minBoneWeight: 0.001
|
||||
optimizeBones: 1
|
||||
generateMeshLods: 0
|
||||
meshLodGenerationFlags: 0
|
||||
maximumMeshLod: -1
|
||||
meshOptimizationFlags: -1
|
||||
indexFormat: 0
|
||||
secondaryUVAngleDistortion: 8
|
||||
secondaryUVAreaDistortion: 15.000001
|
||||
secondaryUVHardAngle: 88
|
||||
secondaryUVMarginMethod: 1
|
||||
secondaryUVMinLightmapResolution: 40
|
||||
secondaryUVMinObjectScale: 1
|
||||
secondaryUVPackMargin: 4
|
||||
useFileScale: 1
|
||||
strictVertexDataChecks: 0
|
||||
tangentSpace:
|
||||
normalSmoothAngle: 60
|
||||
normalImportMode: 0
|
||||
tangentImportMode: 3
|
||||
normalCalculationMode: 4
|
||||
legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0
|
||||
blendShapeNormalImportMode: 1
|
||||
normalSmoothingSource: 0
|
||||
referencedClips: []
|
||||
importAnimation: 1
|
||||
humanDescription:
|
||||
serializedVersion: 3
|
||||
human: []
|
||||
skeleton: []
|
||||
armTwist: 0.5
|
||||
foreArmTwist: 0.5
|
||||
upperLegTwist: 0.5
|
||||
legTwist: 0.5
|
||||
armStretch: 0.05
|
||||
legStretch: 0.05
|
||||
feetSpacing: 0
|
||||
globalScale: 1
|
||||
rootMotionBoneName:
|
||||
hasTranslationDoF: 0
|
||||
hasExtraRoot: 0
|
||||
skeletonHasParents: 1
|
||||
lastHumanDescriptionAvatarSource: {instanceID: 0}
|
||||
autoGenerateAvatarMappingIfUnspecified: 1
|
||||
animationType: 2
|
||||
humanoidOversampling: 1
|
||||
avatarSetup: 0
|
||||
addHumanoidExtraRootOnlyWhenUsingAvatar: 1
|
||||
importBlendShapeDeformPercent: 1
|
||||
remapMaterialsIfMaterialImportModeIsNone: 0
|
||||
additionalBone: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/_Game/Data/Loadouts.meta
Normal file
8
Assets/_Game/Data/Loadouts.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b3aa64bb192a3c43b89e4a0ad054c3e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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: 26d5895a89de4f24aade1ea4b5f7644e, type: 3}
|
||||
m_Name: "Data_LoadoutPreset_Player_\uB51C\uB7EC_\uC82C\uD14C\uC2A4\uD2B8"
|
||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.PlayerLoadoutPreset
|
||||
presetName: "\uB51C\uB7EC \uC82C \uD14C\uC2A4\uD2B8"
|
||||
description: "\uD30C\uC1C4 \uC82C\uC744 \uC0AC\uC6A9\uD558\uB294 \uB51C\uB7EC \uAC80\uC99D
|
||||
\uD504\uB9AC\uC14B"
|
||||
slots:
|
||||
- baseSkill: {fileID: 11400000, guid: b7f09e0e899c8fc4bb2cc9204cc6eb4a, type: 2}
|
||||
socketedGems:
|
||||
- {fileID: 0}
|
||||
- {fileID: 0}
|
||||
- baseSkill: {fileID: 11400000, guid: b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5, type: 2}
|
||||
socketedGems:
|
||||
- {fileID: 0}
|
||||
- {fileID: 0}
|
||||
- baseSkill: {fileID: 11400000, guid: d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1, type: 2}
|
||||
socketedGems:
|
||||
- {fileID: 0}
|
||||
- {fileID: 0}
|
||||
- baseSkill: {fileID: 11400000, guid: f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3, type: 2}
|
||||
socketedGems:
|
||||
- {fileID: 0}
|
||||
- {fileID: 0}
|
||||
- baseSkill: {fileID: 11400000, guid: b8c86399865e91144a3d6fcfddc04fd9, type: 2}
|
||||
socketedGems:
|
||||
- {fileID: 0}
|
||||
- {fileID: 0}
|
||||
- baseSkill: {fileID: 11400000, guid: 549a9978338eb504690c3c490acc0c60, type: 2}
|
||||
socketedGems:
|
||||
- {fileID: 11400000, guid: 2c42bf0e90f5dd9488d534c337a44eed, type: 2}
|
||||
- {fileID: 0}
|
||||
- baseSkill: {fileID: 11400000, guid: 2ed15dca92a165046b6df17b28f64874, type: 2}
|
||||
socketedGems:
|
||||
- {fileID: 0}
|
||||
- {fileID: 0}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0a55c1d1d35c2e1488cc6ebdf35693c8
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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: 26d5895a89de4f24aade1ea4b5f7644e, type: 3}
|
||||
m_Name: "Data_LoadoutPreset_Player_\uC9C0\uC6D0_\uC82C\uD14C\uC2A4\uD2B8"
|
||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.PlayerLoadoutPreset
|
||||
presetName: "\uC9C0\uC6D0 \uC82C \uD14C\uC2A4\uD2B8"
|
||||
description: "\uC218\uD638 \uC82C\uC744 \uC0AC\uC6A9\uD558\uB294 \uC9C0\uC6D0 \uAC80\uC99D
|
||||
\uD504\uB9AC\uC14B"
|
||||
slots:
|
||||
- baseSkill: {fileID: 11400000, guid: b7f09e0e899c8fc4bb2cc9204cc6eb4a, type: 2}
|
||||
socketedGems:
|
||||
- {fileID: 0}
|
||||
- {fileID: 0}
|
||||
- baseSkill: {fileID: 11400000, guid: 21598931a138aa44c86d85d67f6c534a, type: 2}
|
||||
socketedGems:
|
||||
- {fileID: 0}
|
||||
- {fileID: 0}
|
||||
- baseSkill: {fileID: 11400000, guid: 7a245d40a0d21b248b942033d4ec4309, type: 2}
|
||||
socketedGems:
|
||||
- {fileID: 0}
|
||||
- {fileID: 0}
|
||||
- baseSkill: {fileID: 11400000, guid: b78d2eb76cdfbe248b65bafe6e1dc231, type: 2}
|
||||
socketedGems:
|
||||
- {fileID: 0}
|
||||
- {fileID: 0}
|
||||
- baseSkill: {fileID: 11400000, guid: f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3, type: 2}
|
||||
socketedGems:
|
||||
- {fileID: 0}
|
||||
- {fileID: 0}
|
||||
- baseSkill: {fileID: 11400000, guid: 549a9978338eb504690c3c490acc0c60, type: 2}
|
||||
socketedGems:
|
||||
- {fileID: 11400000, guid: de5e48980eba8794c93ea7168d592f8f, type: 2}
|
||||
- {fileID: 0}
|
||||
- baseSkill: {fileID: 11400000, guid: 2ed15dca92a165046b6df17b28f64874, type: 2}
|
||||
socketedGems:
|
||||
- {fileID: 0}
|
||||
- {fileID: 0}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b8d5ad5e2c43e5745838929f1123be8f
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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: 26d5895a89de4f24aade1ea4b5f7644e, type: 3}
|
||||
m_Name: "Data_LoadoutPreset_Player_\uD0F1\uCEE4_\uC82C\uD14C\uC2A4\uD2B8"
|
||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.PlayerLoadoutPreset
|
||||
presetName: "\uD0F1\uCEE4 \uC82C \uD14C\uC2A4\uD2B8"
|
||||
description: "\uB3C4\uC804\uC790 \uC82C\uC744 \uC0AC\uC6A9\uD558\uB294 \uD0F1\uCEE4
|
||||
\uAC80\uC99D \uD504\uB9AC\uC14B"
|
||||
slots:
|
||||
- baseSkill: {fileID: 11400000, guid: b7f09e0e899c8fc4bb2cc9204cc6eb4a, type: 2}
|
||||
socketedGems:
|
||||
- {fileID: 0}
|
||||
- {fileID: 0}
|
||||
- baseSkill: {fileID: 11400000, guid: 1020083ab98b8214f918fa2ab7c1a3a1, type: 2}
|
||||
socketedGems:
|
||||
- {fileID: 0}
|
||||
- {fileID: 0}
|
||||
- baseSkill: {fileID: 11400000, guid: a822c7e8c7cee5546ad594b582208e53, type: 2}
|
||||
socketedGems:
|
||||
- {fileID: 0}
|
||||
- {fileID: 0}
|
||||
- baseSkill: {fileID: 11400000, guid: f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3, type: 2}
|
||||
socketedGems:
|
||||
- {fileID: 0}
|
||||
- {fileID: 0}
|
||||
- baseSkill: {fileID: 11400000, guid: 29e1ce0656471b54f84b18a773032a99, type: 2}
|
||||
socketedGems:
|
||||
- {fileID: 0}
|
||||
- {fileID: 0}
|
||||
- baseSkill: {fileID: 11400000, guid: 549a9978338eb504690c3c490acc0c60, type: 2}
|
||||
socketedGems:
|
||||
- {fileID: 11400000, guid: e86536592f45d2b49b9d25abbad1b184, type: 2}
|
||||
- {fileID: 0}
|
||||
- baseSkill: {fileID: 11400000, guid: 2ed15dca92a165046b6df17b28f64874, type: 2}
|
||||
socketedGems:
|
||||
- {fileID: 0}
|
||||
- {fileID: 0}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a370257e3b9758e43b547a9d4e8ed5d6
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/_Game/Data/SkillGems.meta
Normal file
8
Assets/_Game/Data/SkillGems.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de096830c8246e14490d1d568492c046
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
25
Assets/_Game/Data/SkillGems/Data_SkillGem_Player_도전자.asset
Normal file
25
Assets/_Game/Data/SkillGems/Data_SkillGem_Player_도전자.asset
Normal file
@@ -0,0 +1,25 @@
|
||||
%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: e81a62ae7c7624847ab572ff37789bb8, type: 3}
|
||||
m_Name: "Data_SkillGem_Player_\uB3C4\uC804\uC790"
|
||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.SkillGemData
|
||||
gemName: "\uB3C4\uC804\uC790"
|
||||
description: "\uACE0\uC704\uB825 \uAE30\uC220\uC5D0 \uC704\uD611 \uC120\uC810 \uAE30\uB2A5\uC744
|
||||
\uC5B9\uB294 \uD14C\uC2A4\uD2B8\uC6A9 \uC82C"
|
||||
icon: {fileID: 0}
|
||||
manaCostMultiplier: 1
|
||||
cooldownMultiplier: 1
|
||||
castStartEffects: []
|
||||
triggeredEffects:
|
||||
- triggerIndex: 0
|
||||
effects:
|
||||
- {fileID: 11400000, guid: f0aaa98426be3d44082a386c00ea9aea, type: 2}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e86536592f45d2b49b9d25abbad1b184
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
25
Assets/_Game/Data/SkillGems/Data_SkillGem_Player_수호.asset
Normal file
25
Assets/_Game/Data/SkillGems/Data_SkillGem_Player_수호.asset
Normal file
@@ -0,0 +1,25 @@
|
||||
%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: e81a62ae7c7624847ab572ff37789bb8, type: 3}
|
||||
m_Name: "Data_SkillGem_Player_\uC218\uD638"
|
||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.SkillGemData
|
||||
gemName: "\uC218\uD638"
|
||||
description: "\uACE0\uC704\uB825 \uAE30\uC220\uC5D0 \uBCF4\uD638\uB9C9 \uBCF4\uC870\uB97C
|
||||
\uC5B9\uB294 \uD14C\uC2A4\uD2B8\uC6A9 \uC82C"
|
||||
icon: {fileID: 0}
|
||||
manaCostMultiplier: 1.05
|
||||
cooldownMultiplier: 1.1
|
||||
castStartEffects: []
|
||||
triggeredEffects:
|
||||
- triggerIndex: 0
|
||||
effects:
|
||||
- {fileID: 11400000, guid: 65ed1eabc2fb73d43b86230317222608, type: 2}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de5e48980eba8794c93ea7168d592f8f
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
25
Assets/_Game/Data/SkillGems/Data_SkillGem_Player_파쇄.asset
Normal file
25
Assets/_Game/Data/SkillGems/Data_SkillGem_Player_파쇄.asset
Normal file
@@ -0,0 +1,25 @@
|
||||
%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: e81a62ae7c7624847ab572ff37789bb8, type: 3}
|
||||
m_Name: "Data_SkillGem_Player_\uD30C\uC1C4"
|
||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.SkillGemData
|
||||
gemName: "\uD30C\uC1C4"
|
||||
description: "\uACE0\uC704\uB825 \uAE30\uC220\uC758 \uB2E8\uC77C \uD53C\uD574\uB97C
|
||||
\uAC15\uD654\uD558\uB294 \uD14C\uC2A4\uD2B8\uC6A9 \uC82C"
|
||||
icon: {fileID: 0}
|
||||
manaCostMultiplier: 1.15
|
||||
cooldownMultiplier: 1.1
|
||||
castStartEffects: []
|
||||
triggeredEffects:
|
||||
- triggerIndex: 0
|
||||
effects:
|
||||
- {fileID: 11400000, guid: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4, type: 2}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2c42bf0e90f5dd9488d534c337a44eed
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
25
Assets/_Game/Data/Skills/Data_Skill_Player_젬테스트공격.asset
Normal file
25
Assets/_Game/Data/Skills/Data_Skill_Player_젬테스트공격.asset
Normal file
@@ -0,0 +1,25 @@
|
||||
%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: 94f0a76cebcac2f4fb5daf1b675fd79f, type: 3}
|
||||
m_Name: "Data_Skill_Player_\uC82C\uD14C\uC2A4\uD2B8\uACF5\uACA9"
|
||||
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.SkillData
|
||||
skillName: "\uC82C \uD14C\uC2A4\uD2B8 \uACF5\uACA9"
|
||||
description: "\uB2E4\uC911 \uC82C \uD6A8\uACFC \uAC80\uC99D\uC6A9 \uBE60\uB978 \uB2E8\uC77C \uACF5\uACA9"
|
||||
icon: {fileID: 0}
|
||||
skillClip: {fileID: -8689311932429934276, guid: 1a2314cec0db9814f90aaa68fc5ce4bd, type: 3}
|
||||
endClip: {fileID: 0}
|
||||
useRootMotion: 1
|
||||
ignoreRootMotionY: 1
|
||||
cooldown: 0.5
|
||||
manaCost: 3
|
||||
effects:
|
||||
- {fileID: 11400000, guid: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4, type: 2}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 549a9978338eb504690c3c490acc0c60
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -47,6 +47,12 @@ public abstract partial class BossPatternActionBase : Action
|
||||
if (!IsReady())
|
||||
return Status.Failure;
|
||||
|
||||
if (combatBehaviorContext.IsBehaviorSuppressed)
|
||||
{
|
||||
StopMovement();
|
||||
return Status.Failure;
|
||||
}
|
||||
|
||||
if (bossEnemy.IsDead || bossEnemy.IsTransitioning)
|
||||
return Status.Failure;
|
||||
|
||||
@@ -71,6 +77,12 @@ public abstract partial class BossPatternActionBase : Action
|
||||
if (!IsReady() || activePattern == null)
|
||||
return Status.Failure;
|
||||
|
||||
if (combatBehaviorContext.IsBehaviorSuppressed)
|
||||
{
|
||||
StopMovement();
|
||||
return Status.Failure;
|
||||
}
|
||||
|
||||
if (bossEnemy.IsDead || bossEnemy.IsTransitioning)
|
||||
return Status.Failure;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Colosseum.Enemy;
|
||||
using Unity.Behavior;
|
||||
using UnityEngine;
|
||||
using Action = Unity.Behavior.Action;
|
||||
@@ -22,6 +23,12 @@ public partial class ChaseTargetAction : Action
|
||||
|
||||
protected override Status OnStart()
|
||||
{
|
||||
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
|
||||
if (context != null && context.IsBehaviorSuppressed)
|
||||
{
|
||||
return Status.Failure;
|
||||
}
|
||||
|
||||
if (Target.Value == null)
|
||||
{
|
||||
return Status.Failure;
|
||||
@@ -47,6 +54,15 @@ public partial class ChaseTargetAction : Action
|
||||
|
||||
protected override Status OnUpdate()
|
||||
{
|
||||
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
|
||||
if (context != null && context.IsBehaviorSuppressed)
|
||||
{
|
||||
if (agent != null)
|
||||
agent.isStopped = true;
|
||||
|
||||
return Status.Failure;
|
||||
}
|
||||
|
||||
if (Target.Value == null)
|
||||
{
|
||||
return Status.Failure;
|
||||
|
||||
@@ -25,6 +25,9 @@ public abstract partial class CheckPatternReadyActionBase : Action
|
||||
if (context == null)
|
||||
return Status.Failure;
|
||||
|
||||
if (context.IsBehaviorSuppressed)
|
||||
return Status.Failure;
|
||||
|
||||
BossPatternData pattern = context.GetPattern(PatternRole);
|
||||
return UsePatternAction.IsPatternReady(GameObject, pattern) ? Status.Success : Status.Failure;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ public partial class CheckSignaturePatternReadyAction : Action
|
||||
protected override Status OnStart()
|
||||
{
|
||||
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
|
||||
if (context != null && context.IsBehaviorSuppressed)
|
||||
return Status.Failure;
|
||||
|
||||
return context != null && context.IsSignaturePatternReady()
|
||||
? Status.Success
|
||||
: Status.Failure;
|
||||
|
||||
@@ -24,6 +24,10 @@ public partial class RefreshPrimaryTargetAction : Action
|
||||
|
||||
protected override Status OnStart()
|
||||
{
|
||||
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
|
||||
if (context != null && context.IsBehaviorSuppressed)
|
||||
return Status.Failure;
|
||||
|
||||
EnemyBase enemyBase = GameObject.GetComponent<EnemyBase>();
|
||||
if (enemyBase == null)
|
||||
return Status.Failure;
|
||||
@@ -34,7 +38,6 @@ public partial class RefreshPrimaryTargetAction : Action
|
||||
|
||||
if (resolvedTarget == null)
|
||||
{
|
||||
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
|
||||
resolvedTarget = context != null ? context.FindNearestLivingTarget() : null;
|
||||
}
|
||||
|
||||
|
||||
356
Assets/_Game/Scripts/Editor/MultiplayerPlayModeDebugMenu.cs
Normal file
356
Assets/_Game/Scripts/Editor/MultiplayerPlayModeDebugMenu.cs
Normal file
@@ -0,0 +1,356 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.IO;
|
||||
using Process = System.Diagnostics.Process;
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Colosseum.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Multiplayer Play Mode 관련 상태와 리플렉션 정보를 점검하는 디버그 메뉴입니다.
|
||||
/// </summary>
|
||||
public static class MultiplayerPlayModeDebugMenu
|
||||
{
|
||||
private const string MultiplayerManagerAssetPath = "ProjectSettings/MultiplayerManager.asset";
|
||||
private const string DiagnosticsDirectory = "Temp/MPP";
|
||||
private const string VirtualProjectsRoot = "Library/VP";
|
||||
|
||||
[MenuItem("Tools/Colosseum/Multiplayer/Log Play Mode Module Types")]
|
||||
private static void LogPlayModeModuleTypes()
|
||||
{
|
||||
Assembly playModeAssembly = typeof(UnityEditor.PlayModeStateChange).Assembly;
|
||||
Type[] types = playModeAssembly
|
||||
.GetTypes()
|
||||
.Where(type => type.FullName != null &&
|
||||
(type.FullName.Contains("PlayMode", StringComparison.OrdinalIgnoreCase) ||
|
||||
type.FullName.Contains("Scenario", StringComparison.OrdinalIgnoreCase) ||
|
||||
type.FullName.Contains("Multiplayer", StringComparison.OrdinalIgnoreCase)))
|
||||
.OrderBy(type => type.FullName)
|
||||
.ToArray();
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine("[MPP] PlayModeModule 타입 목록");
|
||||
for (int i = 0; i < types.Length; i++)
|
||||
{
|
||||
builder.Append("- ");
|
||||
builder.AppendLine(types[i].FullName);
|
||||
}
|
||||
|
||||
string diagnosticsPath = EnsureDiagnosticsFilePath("PlayModeModuleTypes.txt");
|
||||
File.WriteAllText(diagnosticsPath, builder.ToString(), Encoding.UTF8);
|
||||
Debug.Log($"[MPP] PlayModeModule 타입 목록을 저장했습니다. {diagnosticsPath}");
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Colosseum/Multiplayer/Log Play Mode User Settings")]
|
||||
private static void LogPlayModeUserSettings()
|
||||
{
|
||||
Type settingsType = Type.GetType("Unity.PlayMode.Editor.PlayModeUserSettings, UnityEditor.PlayModeModule");
|
||||
if (settingsType == null)
|
||||
{
|
||||
Debug.LogWarning("[MPP] Unity.PlayMode.Editor.PlayModeUserSettings 타입을 찾지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
string diagnosticsPath = EnsureDiagnosticsFilePath("PlayModeUserSettings.txt");
|
||||
MethodInfo getOrCreateMethod = settingsType.GetMethod(
|
||||
"GetOrCreateSettings",
|
||||
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
|
||||
object settings = getOrCreateMethod?.Invoke(null, null);
|
||||
if (settings == null)
|
||||
{
|
||||
StringBuilder nullBuilder = new StringBuilder();
|
||||
nullBuilder.AppendLine("[MPP] PlayModeUserSettings 인스턴스를 가져오지 못했습니다.");
|
||||
nullBuilder.AppendLine($"Type: {settingsType.FullName}");
|
||||
nullBuilder.AppendLine("Static Members:");
|
||||
AppendStaticMembers(nullBuilder, settingsType);
|
||||
File.WriteAllText(diagnosticsPath, nullBuilder.ToString(), Encoding.UTF8);
|
||||
Debug.LogWarning($"[MPP] PlayModeUserSettings 인스턴스를 가져오지 못했습니다. 진단 파일: {diagnosticsPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine("[MPP] PlayModeUserSettings");
|
||||
AppendMembers(builder, settingsType, settings);
|
||||
AppendStaticMembers(builder, settingsType);
|
||||
File.WriteAllText(diagnosticsPath, builder.ToString(), Encoding.UTF8);
|
||||
Debug.Log($"[MPP] PlayModeUserSettings 정보를 저장했습니다. {diagnosticsPath}");
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Colosseum/Multiplayer/Enable Local Deployment")]
|
||||
private static void EnableLocalDeployment()
|
||||
{
|
||||
SerializedObject multiplayerManager = GetMultiplayerManagerSerializedObject();
|
||||
if (multiplayerManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SerializedProperty localDeployment = multiplayerManager.FindProperty("m_EnablePlayModeLocalDeployment");
|
||||
if (localDeployment == null)
|
||||
{
|
||||
Debug.LogWarning("[MPP] m_EnablePlayModeLocalDeployment 속성을 찾지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
localDeployment.intValue = 1;
|
||||
multiplayerManager.ApplyModifiedPropertiesWithoutUndo();
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
Debug.Log("[MPP] 로컬 Play Mode 배포를 활성화했습니다.");
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Colosseum/Multiplayer/Disable Local Deployment")]
|
||||
private static void DisableLocalDeployment()
|
||||
{
|
||||
SerializedObject multiplayerManager = GetMultiplayerManagerSerializedObject();
|
||||
if (multiplayerManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SerializedProperty localDeployment = multiplayerManager.FindProperty("m_EnablePlayModeLocalDeployment");
|
||||
if (localDeployment == null)
|
||||
{
|
||||
Debug.LogWarning("[MPP] m_EnablePlayModeLocalDeployment 속성을 찾지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
localDeployment.intValue = 0;
|
||||
multiplayerManager.ApplyModifiedPropertiesWithoutUndo();
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
Debug.Log("[MPP] 로컬 Play Mode 배포를 비활성화했습니다.");
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Colosseum/Multiplayer/Log Multiplayer Manager Settings")]
|
||||
private static void LogMultiplayerManagerSettings()
|
||||
{
|
||||
SerializedObject multiplayerManager = GetMultiplayerManagerSerializedObject();
|
||||
if (multiplayerManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SerializedProperty roles = multiplayerManager.FindProperty("m_EnableMultiplayerRoles");
|
||||
SerializedProperty localDeployment = multiplayerManager.FindProperty("m_EnablePlayModeLocalDeployment");
|
||||
SerializedProperty remoteDeployment = multiplayerManager.FindProperty("m_EnablePlayModeRemoteDeployment");
|
||||
|
||||
Debug.Log(
|
||||
$"[MPP] MultiplayerManager | Roles={roles?.intValue ?? -1} | " +
|
||||
$"LocalDeployment={localDeployment?.intValue ?? -1} | " +
|
||||
$"RemoteDeployment={remoteDeployment?.intValue ?? -1}");
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Colosseum/Multiplayer/Log Virtual Player Clones")]
|
||||
private static void LogVirtualPlayerClones()
|
||||
{
|
||||
string[] cloneDirectories = GetVirtualPlayerCloneDirectories();
|
||||
if (cloneDirectories.Length == 0)
|
||||
{
|
||||
Debug.LogWarning("[MPP] Library/VP 아래에 가상 플레이어 복제본을 찾지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine("[MPP] 가상 플레이어 복제본 목록");
|
||||
for (int i = 0; i < cloneDirectories.Length; i++)
|
||||
{
|
||||
builder.Append("- ");
|
||||
builder.AppendLine(Path.GetFullPath(cloneDirectories[i]));
|
||||
}
|
||||
|
||||
Debug.Log(builder.ToString());
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Colosseum/Multiplayer/Launch First Virtual Player Clone")]
|
||||
private static void LaunchFirstVirtualPlayerClone()
|
||||
{
|
||||
string[] cloneDirectories = GetVirtualPlayerCloneDirectories();
|
||||
if (cloneDirectories.Length == 0)
|
||||
{
|
||||
Debug.LogWarning("[MPP] 실행할 가상 플레이어 복제본이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
string cloneProjectPath = Path.GetFullPath(cloneDirectories[0]);
|
||||
Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = EditorApplication.applicationPath,
|
||||
Arguments = $"-projectPath \"{cloneProjectPath}\"",
|
||||
UseShellExecute = true,
|
||||
});
|
||||
|
||||
Debug.Log($"[MPP] 가상 플레이어 복제본을 실행했습니다. {cloneProjectPath}");
|
||||
}
|
||||
|
||||
private static SerializedObject GetMultiplayerManagerSerializedObject()
|
||||
{
|
||||
UnityEngine.Object[] assets = AssetDatabase.LoadAllAssetsAtPath(MultiplayerManagerAssetPath);
|
||||
if (assets == null || assets.Length == 0 || assets[0] == null)
|
||||
{
|
||||
Debug.LogWarning("[MPP] MultiplayerManager.asset를 찾지 못했습니다.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SerializedObject(assets[0]);
|
||||
}
|
||||
|
||||
private static void AppendMembers(StringBuilder builder, Type settingsType, object settings)
|
||||
{
|
||||
const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
||||
|
||||
List<PropertyInfo> properties = settingsType.GetProperties(flags)
|
||||
.Where(property => property.GetIndexParameters().Length == 0)
|
||||
.OrderBy(property => property.Name)
|
||||
.ToList();
|
||||
|
||||
for (int i = 0; i < properties.Count; i++)
|
||||
{
|
||||
PropertyInfo property = properties[i];
|
||||
object value = null;
|
||||
bool success = true;
|
||||
|
||||
try
|
||||
{
|
||||
value = property.GetValue(settings);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
success = false;
|
||||
value = exception.GetType().Name;
|
||||
}
|
||||
|
||||
builder.Append("- Property ");
|
||||
builder.Append(property.Name);
|
||||
builder.Append(" = ");
|
||||
builder.AppendLine(success ? FormatValue(value) : $"<error: {value}>");
|
||||
}
|
||||
|
||||
List<FieldInfo> fields = settingsType.GetFields(flags)
|
||||
.OrderBy(field => field.Name)
|
||||
.ToList();
|
||||
|
||||
for (int i = 0; i < fields.Count; i++)
|
||||
{
|
||||
FieldInfo field = fields[i];
|
||||
object value = field.GetValue(settings);
|
||||
builder.Append("- Field ");
|
||||
builder.Append(field.Name);
|
||||
builder.Append(" = ");
|
||||
builder.AppendLine(FormatValue(value));
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendStaticMembers(StringBuilder builder, Type settingsType)
|
||||
{
|
||||
const BindingFlags flags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
|
||||
|
||||
List<PropertyInfo> properties = settingsType.GetProperties(flags)
|
||||
.Where(property => property.GetIndexParameters().Length == 0)
|
||||
.OrderBy(property => property.Name)
|
||||
.ToList();
|
||||
|
||||
for (int i = 0; i < properties.Count; i++)
|
||||
{
|
||||
PropertyInfo property = properties[i];
|
||||
object value = null;
|
||||
bool success = true;
|
||||
|
||||
try
|
||||
{
|
||||
value = property.GetValue(null);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
success = false;
|
||||
value = exception.GetType().Name;
|
||||
}
|
||||
|
||||
builder.Append("- Static Property ");
|
||||
builder.Append(property.Name);
|
||||
builder.Append(" = ");
|
||||
builder.AppendLine(success ? FormatValue(value) : $"<error: {value}>");
|
||||
}
|
||||
|
||||
List<FieldInfo> fields = settingsType.GetFields(flags)
|
||||
.OrderBy(field => field.Name)
|
||||
.ToList();
|
||||
|
||||
for (int i = 0; i < fields.Count; i++)
|
||||
{
|
||||
FieldInfo field = fields[i];
|
||||
object value = field.GetValue(null);
|
||||
builder.Append("- Static Field ");
|
||||
builder.Append(field.Name);
|
||||
builder.Append(" = ");
|
||||
builder.AppendLine(FormatValue(value));
|
||||
}
|
||||
|
||||
List<MethodInfo> methods = settingsType.GetMethods(flags)
|
||||
.Where(method => !method.IsSpecialName)
|
||||
.OrderBy(method => method.Name)
|
||||
.ToList();
|
||||
|
||||
for (int i = 0; i < methods.Count; i++)
|
||||
{
|
||||
MethodInfo method = methods[i];
|
||||
string parameterSummary = string.Join(
|
||||
", ",
|
||||
method.GetParameters().Select(parameter => $"{parameter.ParameterType.Name} {parameter.Name}"));
|
||||
|
||||
builder.Append("- Static Method ");
|
||||
builder.Append(method.ReturnType.Name);
|
||||
builder.Append(' ');
|
||||
builder.Append(method.Name);
|
||||
builder.Append('(');
|
||||
builder.Append(parameterSummary);
|
||||
builder.AppendLine(")");
|
||||
}
|
||||
}
|
||||
|
||||
private static string EnsureDiagnosticsFilePath(string fileName)
|
||||
{
|
||||
Directory.CreateDirectory(DiagnosticsDirectory);
|
||||
return Path.Combine(DiagnosticsDirectory, fileName);
|
||||
}
|
||||
|
||||
private static string[] GetVirtualPlayerCloneDirectories()
|
||||
{
|
||||
if (!Directory.Exists(VirtualProjectsRoot))
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
return Directory
|
||||
.GetDirectories(VirtualProjectsRoot, "mppm*")
|
||||
.OrderBy(path => path)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static string FormatValue(object value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return "null";
|
||||
}
|
||||
|
||||
if (value is string stringValue)
|
||||
{
|
||||
return stringValue;
|
||||
}
|
||||
|
||||
if (value is IEnumerable<object> enumerable)
|
||||
{
|
||||
return "[" + string.Join(", ", enumerable.Select(FormatValue)) + "]";
|
||||
}
|
||||
|
||||
return value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eb18c83f7c6efff429f59061e7f0b07b
|
||||
@@ -23,6 +23,7 @@ namespace Colosseum.Editor
|
||||
private const string ShieldSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_보호막.asset";
|
||||
private const string SlashSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_베기.asset";
|
||||
private const string PierceSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_찌르기.asset";
|
||||
private const string GemTestSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_젬테스트공격.asset";
|
||||
private const string SpinSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_회전베기.asset";
|
||||
private const string DashSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_돌진.asset";
|
||||
private const string ProjectileSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_투사체.asset";
|
||||
@@ -33,6 +34,14 @@ namespace Colosseum.Editor
|
||||
private const string StunAbnormalityPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Player_Stun.asset";
|
||||
private const string SilenceAbnormalityPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Player_Silence.asset";
|
||||
private const string MarkAbnormalityPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Player_집행자의낙인.asset";
|
||||
private const string SkillGemFolderPath = "Assets/_Game/Data/SkillGems";
|
||||
private const string LoadoutPresetFolderPath = "Assets/_Game/Data/Loadouts";
|
||||
private const string CrushGemPath = SkillGemFolderPath + "/Data_SkillGem_Player_파쇄.asset";
|
||||
private const string ChallengerGemPath = SkillGemFolderPath + "/Data_SkillGem_Player_도전자.asset";
|
||||
private const string GuardianGemPath = SkillGemFolderPath + "/Data_SkillGem_Player_수호.asset";
|
||||
private const string TankGemPresetPath = LoadoutPresetFolderPath + "/Data_LoadoutPreset_Player_탱커_젬테스트.asset";
|
||||
private const string SupportGemPresetPath = LoadoutPresetFolderPath + "/Data_LoadoutPreset_Player_지원_젬테스트.asset";
|
||||
private const string DpsGemPresetPath = LoadoutPresetFolderPath + "/Data_LoadoutPreset_Player_딜러_젬테스트.asset";
|
||||
|
||||
[MenuItem("Tools/Colosseum/Debug/Cast Local Skill 3")]
|
||||
private static void CastLocalSkill3()
|
||||
@@ -40,6 +49,12 @@ namespace Colosseum.Editor
|
||||
CastLocalSkill(2);
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Colosseum/Debug/Cast Local Skill R")]
|
||||
private static void CastLocalSkillR()
|
||||
{
|
||||
CastLocalSkill(1);
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Colosseum/Debug/Cast Local Skill 4")]
|
||||
private static void CastLocalSkill4()
|
||||
{
|
||||
@@ -52,6 +67,42 @@ namespace Colosseum.Editor
|
||||
CastLocalSkill(4);
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Colosseum/Debug/Cast Local Skill 6")]
|
||||
private static void CastLocalSkill6()
|
||||
{
|
||||
CastLocalSkill(5);
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Colosseum/Debug/Cast Client1 Skill R")]
|
||||
private static void CastClient1SkillR()
|
||||
{
|
||||
CastOwnedPlayerSkillAsServer(1, 1);
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Colosseum/Debug/Cast Client1 Skill 1")]
|
||||
private static void CastClient1Skill1()
|
||||
{
|
||||
CastOwnedPlayerSkillAsServer(1, 2);
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Colosseum/Debug/Cast Client1 Skill 2")]
|
||||
private static void CastClient1Skill2()
|
||||
{
|
||||
CastOwnedPlayerSkillAsServer(1, 3);
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Colosseum/Debug/Cast Client1 Skill 3")]
|
||||
private static void CastClient1Skill3()
|
||||
{
|
||||
CastOwnedPlayerSkillAsServer(1, 4);
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Colosseum/Debug/Cast Client1 Skill 4")]
|
||||
private static void CastClient1Skill4()
|
||||
{
|
||||
CastOwnedPlayerSkillAsServer(1, 5);
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Colosseum/Debug/Cast Local Heal")]
|
||||
private static void CastLocalHeal()
|
||||
{
|
||||
@@ -134,14 +185,14 @@ namespace Colosseum.Editor
|
||||
continue;
|
||||
|
||||
if (builder.Length > 0)
|
||||
builder.AppendLine().AppendLine();
|
||||
builder.Append(" || ");
|
||||
|
||||
builder.Append(enemy.name);
|
||||
builder.Append(" : ");
|
||||
builder.Append(enemy.GetThreatDebugSummary().Replace("\r\n", " | ").Replace("\n", " | "));
|
||||
}
|
||||
|
||||
Debug.Log($"[Debug] 보스 위협 요약\n{builder}");
|
||||
Debug.Log($"[Debug] 보스 위협 요약 | {builder}");
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Colosseum/Debug/Apply Local Stun")]
|
||||
@@ -162,6 +213,33 @@ namespace Colosseum.Editor
|
||||
ApplyLocalAbnormality(MarkAbnormalityPath);
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Colosseum/Debug/Apply Boss Stun")]
|
||||
private static void ApplyBossStun()
|
||||
{
|
||||
if (!EditorApplication.isPlaying)
|
||||
{
|
||||
Debug.LogWarning("[Debug] 플레이 모드에서만 사용할 수 있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
AbnormalityManager abnormalityManager = FindBossAbnormalityManager();
|
||||
if (abnormalityManager == null)
|
||||
{
|
||||
Debug.LogWarning("[Debug] 보스 AbnormalityManager를 찾지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
AbnormalityData abnormality = AssetDatabase.LoadAssetAtPath<AbnormalityData>(StunAbnormalityPath);
|
||||
if (abnormality == null)
|
||||
{
|
||||
Debug.LogWarning($"[Debug] 기절 이상상태 에셋을 찾지 못했습니다: {StunAbnormalityPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
abnormalityManager.ApplyAbnormality(abnormality, abnormalityManager.gameObject);
|
||||
Debug.Log($"[Debug] 보스에게 기절 적용 | Target={abnormalityManager.gameObject.name} | Abnormality={abnormality.abnormalityName}");
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Colosseum/Debug/Log HUD Abnormality Summary")]
|
||||
private static void LogHudAbnormalitySummary()
|
||||
{
|
||||
@@ -223,6 +301,131 @@ namespace Colosseum.Editor
|
||||
EvadeSkillPath);
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Colosseum/Debug/Apply Tank Gem Loadout")]
|
||||
private static void ApplyTankGemLoadout()
|
||||
{
|
||||
ApplyLoadoutPreset(TankGemPresetPath, "탱커 젬");
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Colosseum/Debug/Apply Support Gem Loadout")]
|
||||
private static void ApplySupportGemLoadout()
|
||||
{
|
||||
ApplyLoadoutPreset(SupportGemPresetPath, "지원 젬");
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Colosseum/Debug/Apply DPS Gem Loadout")]
|
||||
private static void ApplyDpsGemLoadout()
|
||||
{
|
||||
ApplyLoadoutPreset(DpsGemPresetPath, "딜러 젬");
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Colosseum/Setup/Create or Update Test Skill Gems")]
|
||||
public static void CreateOrUpdateTestSkillGems()
|
||||
{
|
||||
EnsureFolder("Assets/_Game/Data", "SkillGems");
|
||||
|
||||
SkillEffect damageEffect = AssetDatabase.LoadAssetAtPath<SkillEffect>("Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Player_찌르기_0_데미지.asset");
|
||||
SkillEffect tauntEffect = AssetDatabase.LoadAssetAtPath<SkillEffect>("Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Player_도발_0_도발.asset");
|
||||
SkillEffect shieldEffect = AssetDatabase.LoadAssetAtPath<SkillEffect>("Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Player_보호막_0_보호막.asset");
|
||||
|
||||
CreateOrUpdateGemAsset(
|
||||
CrushGemPath,
|
||||
"파쇄",
|
||||
"고위력 기술의 단일 피해를 강화하는 테스트용 젬",
|
||||
1.15f,
|
||||
1.1f,
|
||||
damageEffect);
|
||||
|
||||
CreateOrUpdateGemAsset(
|
||||
ChallengerGemPath,
|
||||
"도전자",
|
||||
"고위력 기술에 위협 선점 기능을 얹는 테스트용 젬",
|
||||
1f,
|
||||
1f,
|
||||
tauntEffect);
|
||||
|
||||
CreateOrUpdateGemAsset(
|
||||
GuardianGemPath,
|
||||
"수호",
|
||||
"고위력 기술에 보호막 보조를 얹는 테스트용 젬",
|
||||
1.05f,
|
||||
1.1f,
|
||||
shieldEffect);
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
Debug.Log("[Debug] 테스트용 젬 자산 생성/갱신 완료");
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Colosseum/Setup/Create or Update Test Loadout Presets")]
|
||||
public static void CreateOrUpdateTestLoadoutPresets()
|
||||
{
|
||||
EnsureFolder("Assets/_Game/Data", "Loadouts");
|
||||
|
||||
CreateOrUpdateTestSkillGems();
|
||||
|
||||
SkillData slashSkill = AssetDatabase.LoadAssetAtPath<SkillData>(SlashSkillPath);
|
||||
SkillData tauntSkill = AssetDatabase.LoadAssetAtPath<SkillData>(TauntSkillPath);
|
||||
SkillData guardSkill = AssetDatabase.LoadAssetAtPath<SkillData>(GuardSkillPath);
|
||||
SkillData dashSkill = AssetDatabase.LoadAssetAtPath<SkillData>(DashSkillPath);
|
||||
SkillData ironWallSkill = AssetDatabase.LoadAssetAtPath<SkillData>(IronWallSkillPath);
|
||||
SkillData pierceSkill = AssetDatabase.LoadAssetAtPath<SkillData>(PierceSkillPath);
|
||||
SkillData gemTestSkill = AssetDatabase.LoadAssetAtPath<SkillData>(GemTestSkillPath);
|
||||
SkillData healSkill = AssetDatabase.LoadAssetAtPath<SkillData>(HealSkillPath);
|
||||
SkillData areaHealSkill = AssetDatabase.LoadAssetAtPath<SkillData>(AreaHealSkillPath);
|
||||
SkillData shieldSkill = AssetDatabase.LoadAssetAtPath<SkillData>(ShieldSkillPath);
|
||||
SkillData projectileSkill = AssetDatabase.LoadAssetAtPath<SkillData>(ProjectileSkillPath);
|
||||
SkillData spinSkill = AssetDatabase.LoadAssetAtPath<SkillData>(SpinSkillPath);
|
||||
SkillData evadeSkill = AssetDatabase.LoadAssetAtPath<SkillData>(EvadeSkillPath);
|
||||
|
||||
SkillGemData crushGem = AssetDatabase.LoadAssetAtPath<SkillGemData>(CrushGemPath);
|
||||
SkillGemData challengerGem = AssetDatabase.LoadAssetAtPath<SkillGemData>(ChallengerGemPath);
|
||||
SkillGemData guardianGem = AssetDatabase.LoadAssetAtPath<SkillGemData>(GuardianGemPath);
|
||||
|
||||
CreateOrUpdatePresetAsset(
|
||||
TankGemPresetPath,
|
||||
"탱커 젬 테스트",
|
||||
"도전자 젬을 사용하는 탱커 검증 프리셋",
|
||||
CreateLoadoutEntries(
|
||||
CreateEntry(slashSkill),
|
||||
CreateEntry(tauntSkill),
|
||||
CreateEntry(guardSkill),
|
||||
CreateEntry(dashSkill),
|
||||
CreateEntry(ironWallSkill),
|
||||
CreateEntry(gemTestSkill, challengerGem),
|
||||
CreateEntry(evadeSkill)));
|
||||
|
||||
CreateOrUpdatePresetAsset(
|
||||
SupportGemPresetPath,
|
||||
"지원 젬 테스트",
|
||||
"수호 젬을 사용하는 지원 검증 프리셋",
|
||||
CreateLoadoutEntries(
|
||||
CreateEntry(slashSkill),
|
||||
CreateEntry(healSkill),
|
||||
CreateEntry(areaHealSkill),
|
||||
CreateEntry(shieldSkill),
|
||||
CreateEntry(dashSkill),
|
||||
CreateEntry(gemTestSkill, guardianGem),
|
||||
CreateEntry(evadeSkill)));
|
||||
|
||||
CreateOrUpdatePresetAsset(
|
||||
DpsGemPresetPath,
|
||||
"딜러 젬 테스트",
|
||||
"파쇄 젬을 사용하는 딜러 검증 프리셋",
|
||||
CreateLoadoutEntries(
|
||||
CreateEntry(slashSkill),
|
||||
CreateEntry(pierceSkill),
|
||||
CreateEntry(spinSkill),
|
||||
CreateEntry(dashSkill),
|
||||
CreateEntry(projectileSkill),
|
||||
CreateEntry(gemTestSkill, crushGem),
|
||||
CreateEntry(evadeSkill)));
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
Debug.Log("[Debug] 테스트용 젬 프리셋 생성/갱신 완료");
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Colosseum/Debug/Log Local Skill Loadout")]
|
||||
private static void LogLocalSkillLoadout()
|
||||
{
|
||||
@@ -245,9 +448,11 @@ namespace Colosseum.Editor
|
||||
for (int i = 0; i < slotOrder.Length; i++)
|
||||
{
|
||||
SkillData skill = localSkillInput.GetSkill(slotOrder[i]);
|
||||
SkillLoadoutEntry loadoutEntry = localSkillInput.GetSkillLoadout(slotOrder[i]);
|
||||
builder.Append(slotNames[i]);
|
||||
builder.Append(": ");
|
||||
builder.Append(skill != null ? skill.SkillName : "(비어 있음)");
|
||||
AppendGemSummary(builder, loadoutEntry);
|
||||
|
||||
if (i < slotOrder.Length - 1)
|
||||
builder.Append(" | ");
|
||||
@@ -291,6 +496,20 @@ namespace Colosseum.Editor
|
||||
return localNetworkController.GetComponent<AbnormalityManager>();
|
||||
}
|
||||
|
||||
private static AbnormalityManager FindBossAbnormalityManager()
|
||||
{
|
||||
BossEnemy activeBoss = BossEnemy.ActiveBoss;
|
||||
if (activeBoss != null)
|
||||
{
|
||||
AbnormalityManager activeManager = activeBoss.GetComponent<AbnormalityManager>();
|
||||
if (activeManager != null)
|
||||
return activeManager;
|
||||
}
|
||||
|
||||
BossEnemy bossEnemy = Object.FindFirstObjectByType<BossEnemy>();
|
||||
return bossEnemy != null ? bossEnemy.GetComponent<AbnormalityManager>() : null;
|
||||
}
|
||||
|
||||
private static void CastLocalSkill(int slotIndex)
|
||||
{
|
||||
if (!EditorApplication.isPlaying)
|
||||
@@ -391,5 +610,197 @@ namespace Colosseum.Editor
|
||||
localSkillInput.SetSkills(skills);
|
||||
Debug.Log($"[Debug] {loadoutName} 프리셋을 적용했습니다.");
|
||||
}
|
||||
|
||||
private static void ApplyLoadoutPreset(string presetPath, string presetLabel)
|
||||
{
|
||||
if (!EditorApplication.isPlaying)
|
||||
{
|
||||
Debug.LogWarning("[Debug] 플레이 모드에서만 사용할 수 있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerSkillInput localSkillInput = FindLocalSkillInput();
|
||||
if (localSkillInput == null)
|
||||
{
|
||||
Debug.LogWarning("[Debug] 로컬 PlayerSkillInput을 찾지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerLoadoutPreset preset = AssetDatabase.LoadAssetAtPath<PlayerLoadoutPreset>(presetPath);
|
||||
if (preset == null)
|
||||
{
|
||||
Debug.LogWarning($"[Debug] 프리셋 에셋을 찾지 못했습니다: {presetPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
localSkillInput.ApplyLoadoutPreset(preset);
|
||||
Debug.Log($"[Debug] {presetLabel} 프리셋을 적용했습니다.");
|
||||
}
|
||||
|
||||
private static void EnsureFolder(string parentFolder, string childFolderName)
|
||||
{
|
||||
string combined = $"{parentFolder}/{childFolderName}";
|
||||
if (AssetDatabase.IsValidFolder(combined))
|
||||
return;
|
||||
|
||||
AssetDatabase.CreateFolder(parentFolder, childFolderName);
|
||||
}
|
||||
|
||||
private static SkillLoadoutEntry[] CreateLoadoutEntries(params SkillLoadoutEntry[] entries)
|
||||
{
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static SkillLoadoutEntry CreateEntry(SkillData skill, params SkillGemData[] gems)
|
||||
{
|
||||
SkillLoadoutEntry entry = SkillLoadoutEntry.CreateTemporary(skill);
|
||||
if (gems == null)
|
||||
return entry;
|
||||
|
||||
for (int i = 0; i < gems.Length; i++)
|
||||
{
|
||||
entry.SetGem(i, gems[i]);
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
private static void CreateOrUpdateGemAsset(string assetPath, string gemName, string description, float manaCostMultiplier, float cooldownMultiplier, SkillEffect triggeredEffect)
|
||||
{
|
||||
SkillGemData gem = AssetDatabase.LoadAssetAtPath<SkillGemData>(assetPath);
|
||||
if (gem == null)
|
||||
{
|
||||
if (AssetDatabase.LoadMainAssetAtPath(assetPath) != null)
|
||||
{
|
||||
AssetDatabase.DeleteAsset(assetPath);
|
||||
}
|
||||
|
||||
gem = ScriptableObject.CreateInstance<SkillGemData>();
|
||||
AssetDatabase.CreateAsset(gem, assetPath);
|
||||
}
|
||||
|
||||
SerializedObject serializedGem = new SerializedObject(gem);
|
||||
serializedGem.FindProperty("gemName").stringValue = gemName;
|
||||
serializedGem.FindProperty("description").stringValue = description;
|
||||
serializedGem.FindProperty("manaCostMultiplier").floatValue = manaCostMultiplier;
|
||||
serializedGem.FindProperty("cooldownMultiplier").floatValue = cooldownMultiplier;
|
||||
|
||||
SerializedProperty castStartEffectsProperty = serializedGem.FindProperty("castStartEffects");
|
||||
castStartEffectsProperty.arraySize = 0;
|
||||
|
||||
SerializedProperty triggeredEffectsProperty = serializedGem.FindProperty("triggeredEffects");
|
||||
triggeredEffectsProperty.arraySize = triggeredEffect != null ? 1 : 0;
|
||||
if (triggeredEffect != null)
|
||||
{
|
||||
SerializedProperty triggeredEntry = triggeredEffectsProperty.GetArrayElementAtIndex(0);
|
||||
triggeredEntry.FindPropertyRelative("triggerIndex").intValue = 0;
|
||||
|
||||
SerializedProperty effectArray = triggeredEntry.FindPropertyRelative("effects");
|
||||
effectArray.arraySize = 1;
|
||||
effectArray.GetArrayElementAtIndex(0).objectReferenceValue = triggeredEffect;
|
||||
}
|
||||
|
||||
serializedGem.ApplyModifiedPropertiesWithoutUndo();
|
||||
EditorUtility.SetDirty(gem);
|
||||
}
|
||||
|
||||
private static void CreateOrUpdatePresetAsset(string assetPath, string presetName, string description, IReadOnlyList<SkillLoadoutEntry> entries)
|
||||
{
|
||||
PlayerLoadoutPreset preset = AssetDatabase.LoadAssetAtPath<PlayerLoadoutPreset>(assetPath);
|
||||
if (preset == null)
|
||||
{
|
||||
if (AssetDatabase.LoadMainAssetAtPath(assetPath) != null)
|
||||
{
|
||||
AssetDatabase.DeleteAsset(assetPath);
|
||||
}
|
||||
|
||||
preset = ScriptableObject.CreateInstance<PlayerLoadoutPreset>();
|
||||
AssetDatabase.CreateAsset(preset, assetPath);
|
||||
}
|
||||
|
||||
SerializedObject serializedPreset = new SerializedObject(preset);
|
||||
serializedPreset.FindProperty("presetName").stringValue = presetName;
|
||||
serializedPreset.FindProperty("description").stringValue = description;
|
||||
|
||||
SerializedProperty slotsProperty = serializedPreset.FindProperty("slots");
|
||||
slotsProperty.arraySize = entries != null ? entries.Count : 0;
|
||||
|
||||
for (int i = 0; i < slotsProperty.arraySize; i++)
|
||||
{
|
||||
SkillLoadoutEntry entry = entries[i] != null ? entries[i].CreateCopy() : new SkillLoadoutEntry();
|
||||
SerializedProperty slotProperty = slotsProperty.GetArrayElementAtIndex(i);
|
||||
slotProperty.FindPropertyRelative("baseSkill").objectReferenceValue = entry.BaseSkill;
|
||||
|
||||
SerializedProperty gemsProperty = slotProperty.FindPropertyRelative("socketedGems");
|
||||
gemsProperty.arraySize = entry.SocketedGems.Count;
|
||||
for (int j = 0; j < gemsProperty.arraySize; j++)
|
||||
{
|
||||
gemsProperty.GetArrayElementAtIndex(j).objectReferenceValue = entry.GetGem(j);
|
||||
}
|
||||
}
|
||||
|
||||
serializedPreset.ApplyModifiedPropertiesWithoutUndo();
|
||||
EditorUtility.SetDirty(preset);
|
||||
}
|
||||
|
||||
private static void AppendGemSummary(StringBuilder builder, SkillLoadoutEntry loadoutEntry)
|
||||
{
|
||||
if (builder == null || loadoutEntry == null || loadoutEntry.SocketedGems == null)
|
||||
return;
|
||||
|
||||
bool hasGem = false;
|
||||
StringBuilder gemBuilder = new StringBuilder();
|
||||
for (int i = 0; i < loadoutEntry.SocketedGems.Count; i++)
|
||||
{
|
||||
SkillGemData gem = loadoutEntry.SocketedGems[i];
|
||||
if (gem == null)
|
||||
continue;
|
||||
|
||||
if (hasGem)
|
||||
gemBuilder.Append(", ");
|
||||
|
||||
gemBuilder.Append(gem.GemName);
|
||||
hasGem = true;
|
||||
}
|
||||
|
||||
if (!hasGem)
|
||||
return;
|
||||
|
||||
builder.Append(" [");
|
||||
builder.Append(gemBuilder);
|
||||
builder.Append("]");
|
||||
}
|
||||
|
||||
private static void CastOwnedPlayerSkillAsServer(ulong ownerClientId, int slotIndex)
|
||||
{
|
||||
if (!EditorApplication.isPlaying)
|
||||
{
|
||||
Debug.LogWarning("[Debug] 플레이 모드에서만 사용할 수 있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerSkillInput playerSkillInput = FindPlayerSkillInputByOwner(ownerClientId);
|
||||
if (playerSkillInput == null)
|
||||
{
|
||||
Debug.LogWarning($"[Debug] OwnerClientId={ownerClientId} 인 PlayerSkillInput을 찾지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
bool executed = playerSkillInput.DebugExecuteSkillAsServer(slotIndex);
|
||||
Debug.Log($"[Debug] 원격 스킬 실행 요청 | OwnerClientId={ownerClientId} | Slot={slotIndex} | Success={executed}");
|
||||
}
|
||||
|
||||
private static PlayerSkillInput FindPlayerSkillInputByOwner(ulong ownerClientId)
|
||||
{
|
||||
PlayerSkillInput[] skillInputs = Object.FindObjectsByType<PlayerSkillInput>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
||||
for (int i = 0; i < skillInputs.Length; i++)
|
||||
{
|
||||
PlayerSkillInput skillInput = skillInputs[i];
|
||||
if (skillInput != null && skillInput.OwnerClientId == ownerClientId)
|
||||
return skillInput;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace Colosseum.Enemy
|
||||
[SerializeField] protected BossEnemy bossEnemy;
|
||||
[SerializeField] protected EnemyBase enemyBase;
|
||||
[SerializeField] protected SkillController skillController;
|
||||
[SerializeField] protected AbnormalityManager abnormalityManager;
|
||||
[SerializeField] protected UnityEngine.AI.NavMeshAgent navMeshAgent;
|
||||
[SerializeField] protected BehaviorGraphAgent behaviorGraphAgent;
|
||||
|
||||
@@ -192,6 +193,11 @@ namespace Colosseum.Enemy
|
||||
/// </summary>
|
||||
public bool DebugModeEnabled => debugMode;
|
||||
|
||||
/// <summary>
|
||||
/// 기절 등으로 인해 보스 전투 로직을 진행할 수 없는 상태인지 여부
|
||||
/// </summary>
|
||||
public bool IsBehaviorSuppressed => abnormalityManager != null && abnormalityManager.IsStunned;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 보스 패턴 페이즈
|
||||
/// </summary>
|
||||
@@ -238,6 +244,12 @@ namespace Colosseum.Enemy
|
||||
if (bossEnemy.IsDead || bossEnemy.IsTransitioning)
|
||||
return;
|
||||
|
||||
if (IsBehaviorSuppressed)
|
||||
{
|
||||
StopMovement();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!disableBehaviorGraph)
|
||||
return;
|
||||
|
||||
@@ -468,6 +480,9 @@ namespace Colosseum.Enemy
|
||||
if (!IsServer || bossEnemy == null || skillController == null)
|
||||
return false;
|
||||
|
||||
if (IsBehaviorSuppressed)
|
||||
return false;
|
||||
|
||||
if (CurrentPatternPhase < signatureMinPhase)
|
||||
return false;
|
||||
|
||||
@@ -699,6 +714,9 @@ namespace Colosseum.Enemy
|
||||
if (skillController == null)
|
||||
skillController = GetComponent<SkillController>();
|
||||
|
||||
if (abnormalityManager == null)
|
||||
abnormalityManager = GetComponent<AbnormalityManager>();
|
||||
|
||||
if (navMeshAgent == null)
|
||||
navMeshAgent = GetComponent<UnityEngine.AI.NavMeshAgent>();
|
||||
|
||||
|
||||
@@ -7,6 +7,13 @@ using System.Collections.Generic;
|
||||
using Colosseum.Skills;
|
||||
using Colosseum.Weapons;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace Colosseum.Player
|
||||
{
|
||||
/// <summary>
|
||||
@@ -18,9 +25,46 @@ namespace Colosseum.Player
|
||||
{
|
||||
private const int ExpectedSkillSlotCount = 7;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private static readonly string[] TankLoadoutPaths =
|
||||
{
|
||||
"Assets/_Game/Data/Skills/Data_Skill_Player_베기.asset",
|
||||
"Assets/_Game/Data/Skills/Data_Skill_Player_도발.asset",
|
||||
"Assets/_Game/Data/Skills/Data_Skill_Player_방어태세.asset",
|
||||
"Assets/_Game/Data/Skills/Data_Skill_Player_돌진.asset",
|
||||
"Assets/_Game/Data/Skills/Data_Skill_Player_철벽.asset",
|
||||
"Assets/_Game/Data/Skills/Data_Skill_Player_찌르기.asset",
|
||||
"Assets/_Game/Data/Skills/Data_Skill_Player_구르기.asset",
|
||||
};
|
||||
|
||||
private static readonly string[] SupportLoadoutPaths =
|
||||
{
|
||||
"Assets/_Game/Data/Skills/Data_Skill_Player_베기.asset",
|
||||
"Assets/_Game/Data/Skills/Data_Skill_Player_치유.asset",
|
||||
"Assets/_Game/Data/Skills/Data_Skill_Player_광역치유.asset",
|
||||
"Assets/_Game/Data/Skills/Data_Skill_Player_보호막.asset",
|
||||
"Assets/_Game/Data/Skills/Data_Skill_Player_돌진.asset",
|
||||
"Assets/_Game/Data/Skills/Data_Skill_Player_투사체.asset",
|
||||
"Assets/_Game/Data/Skills/Data_Skill_Player_구르기.asset",
|
||||
};
|
||||
|
||||
private static readonly string[] DpsLoadoutPaths =
|
||||
{
|
||||
"Assets/_Game/Data/Skills/Data_Skill_Player_베기.asset",
|
||||
"Assets/_Game/Data/Skills/Data_Skill_Player_찌르기.asset",
|
||||
"Assets/_Game/Data/Skills/Data_Skill_Player_회전베기.asset",
|
||||
"Assets/_Game/Data/Skills/Data_Skill_Player_돌진.asset",
|
||||
"Assets/_Game/Data/Skills/Data_Skill_Player_투사체.asset",
|
||||
"Assets/_Game/Data/Skills/Data_Skill_Player_보호막.asset",
|
||||
"Assets/_Game/Data/Skills/Data_Skill_Player_구르기.asset",
|
||||
};
|
||||
#endif
|
||||
|
||||
[Header("Skill Slots")]
|
||||
[Tooltip("각 슬롯에 등록할 스킬 데이터 (6개 + 추가 슬롯)")]
|
||||
[SerializeField] private SkillData[] skillSlots = new SkillData[ExpectedSkillSlotCount];
|
||||
[Tooltip("각 슬롯의 베이스 스킬 + 젬 조합")]
|
||||
[SerializeField] private SkillLoadoutEntry[] skillLoadoutEntries = new SkillLoadoutEntry[ExpectedSkillSlotCount];
|
||||
|
||||
[Header("References")]
|
||||
[Tooltip("SkillController (없으면 자동 검색)")]
|
||||
@@ -35,6 +79,7 @@ namespace Colosseum.Player
|
||||
private InputSystem_Actions inputActions;
|
||||
|
||||
public SkillData[] SkillSlots => skillSlots;
|
||||
public SkillLoadoutEntry[] SkillLoadoutEntries => skillLoadoutEntries;
|
||||
|
||||
/// <summary>
|
||||
/// 스킬 슬롯 구성이 변경되었을 때 호출됩니다.
|
||||
@@ -44,6 +89,10 @@ namespace Colosseum.Player
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
EnsureSkillSlotCapacity();
|
||||
EnsureSkillLoadoutCapacity();
|
||||
SyncLegacySkillsToLoadoutEntries();
|
||||
EnsureRuntimeReferences();
|
||||
ApplyEditorMultiplayerLoadoutIfNeeded();
|
||||
|
||||
if (!IsOwner)
|
||||
{
|
||||
@@ -51,35 +100,6 @@ namespace Colosseum.Player
|
||||
return;
|
||||
}
|
||||
|
||||
// SkillController 참조 확인
|
||||
if (skillController == null)
|
||||
{
|
||||
skillController = GetComponent<SkillController>();
|
||||
if (skillController == null)
|
||||
{
|
||||
Debug.LogError("PlayerSkillInput: SkillController not found!");
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// PlayerNetworkController 참조 확인
|
||||
if (networkController == null)
|
||||
{
|
||||
networkController = GetComponent<PlayerNetworkController>();
|
||||
}
|
||||
|
||||
// WeaponEquipment 참조 확인
|
||||
if (weaponEquipment == null)
|
||||
{
|
||||
weaponEquipment = GetComponent<WeaponEquipment>();
|
||||
}
|
||||
|
||||
if (actionState == null)
|
||||
{
|
||||
actionState = GetOrCreateActionState();
|
||||
}
|
||||
|
||||
InitializeInputActions();
|
||||
}
|
||||
|
||||
@@ -113,11 +133,15 @@ namespace Colosseum.Player
|
||||
private void Awake()
|
||||
{
|
||||
EnsureSkillSlotCapacity();
|
||||
EnsureSkillLoadoutCapacity();
|
||||
SyncLegacySkillsToLoadoutEntries();
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
EnsureSkillSlotCapacity();
|
||||
EnsureSkillLoadoutCapacity();
|
||||
SyncLegacySkillsToLoadoutEntries();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
@@ -150,6 +174,63 @@ namespace Colosseum.Player
|
||||
skillSlots = resizedSlots;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 슬롯별 로드아웃 엔트리 배열을 보정합니다.
|
||||
/// </summary>
|
||||
private void EnsureSkillLoadoutCapacity()
|
||||
{
|
||||
if (skillLoadoutEntries == null || skillLoadoutEntries.Length != ExpectedSkillSlotCount)
|
||||
{
|
||||
SkillLoadoutEntry[] resizedEntries = new SkillLoadoutEntry[ExpectedSkillSlotCount];
|
||||
if (skillLoadoutEntries != null)
|
||||
{
|
||||
int copyCount = Mathf.Min(skillLoadoutEntries.Length, resizedEntries.Length);
|
||||
for (int i = 0; i < copyCount; i++)
|
||||
{
|
||||
resizedEntries[i] = skillLoadoutEntries[i];
|
||||
}
|
||||
}
|
||||
|
||||
skillLoadoutEntries = resizedEntries;
|
||||
}
|
||||
|
||||
for (int i = 0; i < skillLoadoutEntries.Length; i++)
|
||||
{
|
||||
if (skillLoadoutEntries[i] == null)
|
||||
skillLoadoutEntries[i] = new SkillLoadoutEntry();
|
||||
|
||||
skillLoadoutEntries[i].EnsureGemSlotCapacity();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기존 SkillData 직렬화와 새 로드아웃 엔트리 구조를 동기화합니다.
|
||||
/// </summary>
|
||||
private void SyncLegacySkillsToLoadoutEntries()
|
||||
{
|
||||
EnsureSkillSlotCapacity();
|
||||
EnsureSkillLoadoutCapacity();
|
||||
|
||||
for (int i = 0; i < skillSlots.Length; i++)
|
||||
{
|
||||
SkillLoadoutEntry entry = skillLoadoutEntries[i];
|
||||
SkillData legacySkill = skillSlots[i];
|
||||
|
||||
if (entry.BaseSkill == null && legacySkill != null)
|
||||
{
|
||||
entry.SetBaseSkill(legacySkill);
|
||||
}
|
||||
else if (legacySkill == null && entry.BaseSkill != null)
|
||||
{
|
||||
skillSlots[i] = entry.BaseSkill;
|
||||
}
|
||||
else if (entry.BaseSkill != legacySkill)
|
||||
{
|
||||
skillSlots[i] = entry.BaseSkill;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanupInputActions()
|
||||
{
|
||||
if (inputActions != null)
|
||||
@@ -166,7 +247,8 @@ namespace Colosseum.Player
|
||||
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
|
||||
return;
|
||||
|
||||
SkillData skill = skillSlots[slotIndex];
|
||||
SkillLoadoutEntry loadoutEntry = GetSkillLoadout(slotIndex);
|
||||
SkillData skill = loadoutEntry != null ? loadoutEntry.BaseSkill : null;
|
||||
if (skill == null)
|
||||
{
|
||||
Debug.Log($"Skill slot {slotIndex + 1} is empty");
|
||||
@@ -191,7 +273,7 @@ namespace Colosseum.Player
|
||||
}
|
||||
|
||||
// 마나 비용 체크 (무기 배율 적용)
|
||||
float actualManaCost = GetActualManaCost(skill);
|
||||
float actualManaCost = GetActualManaCost(loadoutEntry);
|
||||
if (networkController != null && networkController.Mana < actualManaCost)
|
||||
{
|
||||
Debug.Log($"Not enough mana for skill: {skill.SkillName}");
|
||||
@@ -211,7 +293,8 @@ namespace Colosseum.Player
|
||||
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
|
||||
return;
|
||||
|
||||
SkillData skill = skillSlots[slotIndex];
|
||||
SkillLoadoutEntry loadoutEntry = GetSkillLoadout(slotIndex);
|
||||
SkillData skill = loadoutEntry != null ? loadoutEntry.BaseSkill : null;
|
||||
if (skill == null) return;
|
||||
|
||||
// 서버에서 다시 검증
|
||||
@@ -222,7 +305,7 @@ namespace Colosseum.Player
|
||||
return;
|
||||
|
||||
// 마나 비용 체크 (무기 배율 적용)
|
||||
float actualManaCost = GetActualManaCost(skill);
|
||||
float actualManaCost = GetActualManaCost(loadoutEntry);
|
||||
if (networkController != null && networkController.Mana < actualManaCost)
|
||||
return;
|
||||
|
||||
@@ -245,21 +328,22 @@ namespace Colosseum.Player
|
||||
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
|
||||
return;
|
||||
|
||||
SkillData skill = skillSlots[slotIndex];
|
||||
SkillLoadoutEntry loadoutEntry = GetSkillLoadout(slotIndex);
|
||||
SkillData skill = loadoutEntry != null ? loadoutEntry.BaseSkill : null;
|
||||
if (skill == null) return;
|
||||
|
||||
// 모든 클라이언트에서 스킬 실행 (애니메이션 포함)
|
||||
skillController.ExecuteSkill(skill);
|
||||
skillController.ExecuteSkill(loadoutEntry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 무기 마나 배율이 적용된 실제 마나 비용 계산
|
||||
/// </summary>
|
||||
private float GetActualManaCost(SkillData skill)
|
||||
private float GetActualManaCost(SkillLoadoutEntry loadoutEntry)
|
||||
{
|
||||
if (skill == null) return 0f;
|
||||
if (loadoutEntry == null || loadoutEntry.BaseSkill == null) return 0f;
|
||||
|
||||
float baseCost = skill.ManaCost;
|
||||
float baseCost = loadoutEntry.GetResolvedManaCost();
|
||||
float multiplier = weaponEquipment != null ? weaponEquipment.ManaCostMultiplier : 1f;
|
||||
|
||||
return baseCost * multiplier;
|
||||
@@ -271,10 +355,27 @@ namespace Colosseum.Player
|
||||
public SkillData GetSkill(int slotIndex)
|
||||
{
|
||||
EnsureSkillSlotCapacity();
|
||||
EnsureSkillLoadoutCapacity();
|
||||
SyncLegacySkillsToLoadoutEntries();
|
||||
|
||||
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
|
||||
return null;
|
||||
return skillSlots[slotIndex];
|
||||
return skillLoadoutEntries[slotIndex] != null ? skillLoadoutEntries[slotIndex].BaseSkill : skillSlots[slotIndex];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 슬롯 엔트리 접근자
|
||||
/// </summary>
|
||||
public SkillLoadoutEntry GetSkillLoadout(int slotIndex)
|
||||
{
|
||||
EnsureSkillSlotCapacity();
|
||||
EnsureSkillLoadoutCapacity();
|
||||
SyncLegacySkillsToLoadoutEntries();
|
||||
|
||||
if (slotIndex < 0 || slotIndex >= skillLoadoutEntries.Length)
|
||||
return null;
|
||||
|
||||
return skillLoadoutEntries[slotIndex];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -288,6 +389,7 @@ namespace Colosseum.Player
|
||||
return;
|
||||
|
||||
skillSlots[slotIndex] = skill;
|
||||
skillLoadoutEntries[slotIndex].SetBaseSkill(skill);
|
||||
OnSkillSlotsChanged?.Invoke();
|
||||
}
|
||||
|
||||
@@ -305,16 +407,92 @@ namespace Colosseum.Player
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
skillSlots[i] = skills[i];
|
||||
skillLoadoutEntries[i].SetBaseSkill(skills[i]);
|
||||
}
|
||||
|
||||
for (int i = count; i < skillSlots.Length; i++)
|
||||
{
|
||||
skillSlots[i] = null;
|
||||
skillLoadoutEntries[i].SetBaseSkill(null);
|
||||
}
|
||||
|
||||
OnSkillSlotsChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 슬롯 엔트리를 직접 설정합니다.
|
||||
/// </summary>
|
||||
public void SetSkillLoadout(int slotIndex, SkillLoadoutEntry loadoutEntry)
|
||||
{
|
||||
EnsureSkillSlotCapacity();
|
||||
EnsureSkillLoadoutCapacity();
|
||||
|
||||
if (slotIndex < 0 || slotIndex >= skillLoadoutEntries.Length)
|
||||
return;
|
||||
|
||||
skillLoadoutEntries[slotIndex] = loadoutEntry != null ? loadoutEntry.CreateCopy() : new SkillLoadoutEntry();
|
||||
skillLoadoutEntries[slotIndex].EnsureGemSlotCapacity();
|
||||
skillSlots[slotIndex] = skillLoadoutEntries[slotIndex].BaseSkill;
|
||||
OnSkillSlotsChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 슬롯의 특정 젬 슬롯을 갱신합니다.
|
||||
/// </summary>
|
||||
public void SetSkillGem(int slotIndex, int gemSlotIndex, SkillGemData gem)
|
||||
{
|
||||
EnsureSkillSlotCapacity();
|
||||
EnsureSkillLoadoutCapacity();
|
||||
|
||||
if (slotIndex < 0 || slotIndex >= skillLoadoutEntries.Length)
|
||||
return;
|
||||
|
||||
skillLoadoutEntries[slotIndex].SetGem(gemSlotIndex, gem);
|
||||
skillSlots[slotIndex] = skillLoadoutEntries[slotIndex].BaseSkill;
|
||||
OnSkillSlotsChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 슬롯 엔트리 전체를 한 번에 갱신합니다.
|
||||
/// </summary>
|
||||
public void SetSkillLoadouts(IReadOnlyList<SkillLoadoutEntry> loadouts)
|
||||
{
|
||||
EnsureSkillSlotCapacity();
|
||||
EnsureSkillLoadoutCapacity();
|
||||
|
||||
if (loadouts == null)
|
||||
return;
|
||||
|
||||
int count = Mathf.Min(skillLoadoutEntries.Length, loadouts.Count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
skillLoadoutEntries[i] = loadouts[i] != null ? loadouts[i].CreateCopy() : new SkillLoadoutEntry();
|
||||
skillLoadoutEntries[i].EnsureGemSlotCapacity();
|
||||
skillSlots[i] = skillLoadoutEntries[i].BaseSkill;
|
||||
}
|
||||
|
||||
for (int i = count; i < skillLoadoutEntries.Length; i++)
|
||||
{
|
||||
skillLoadoutEntries[i] = new SkillLoadoutEntry();
|
||||
skillLoadoutEntries[i].EnsureGemSlotCapacity();
|
||||
skillSlots[i] = null;
|
||||
}
|
||||
|
||||
OnSkillSlotsChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 프리셋 기반으로 슬롯 엔트리를 일괄 적용합니다.
|
||||
/// </summary>
|
||||
public void ApplyLoadoutPreset(PlayerLoadoutPreset preset)
|
||||
{
|
||||
if (preset == null)
|
||||
return;
|
||||
|
||||
preset.EnsureSlotCapacity();
|
||||
SetSkillLoadouts(preset.Slots);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 남은 쿨타임 조회
|
||||
/// </summary>
|
||||
@@ -331,6 +509,8 @@ namespace Colosseum.Player
|
||||
/// </summary>
|
||||
public bool CanUseSkill(int slotIndex)
|
||||
{
|
||||
EnsureRuntimeReferences();
|
||||
|
||||
SkillData skill = GetSkill(slotIndex);
|
||||
if (skill == null) return false;
|
||||
|
||||
@@ -352,6 +532,44 @@ namespace Colosseum.Player
|
||||
OnSkillInput(slotIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 서버 권한에서 특정 슬롯 스킬을 강제로 실행합니다.
|
||||
/// 멀티플레이 테스트 시 원격 플레이어 스킬을 호스트에서 검증할 때 사용합니다.
|
||||
/// </summary>
|
||||
public bool DebugExecuteSkillAsServer(int slotIndex)
|
||||
{
|
||||
if (!IsServer)
|
||||
return false;
|
||||
|
||||
EnsureRuntimeReferences();
|
||||
|
||||
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
|
||||
return false;
|
||||
|
||||
SkillLoadoutEntry loadoutEntry = GetSkillLoadout(slotIndex);
|
||||
SkillData skill = loadoutEntry != null ? loadoutEntry.BaseSkill : null;
|
||||
if (skill == null)
|
||||
return false;
|
||||
|
||||
if (actionState != null && !actionState.CanStartSkill(skill))
|
||||
return false;
|
||||
|
||||
if (skillController == null || skillController.IsExecutingSkill || skillController.IsOnCooldown(skill))
|
||||
return false;
|
||||
|
||||
float actualManaCost = GetActualManaCost(loadoutEntry);
|
||||
if (networkController != null && networkController.Mana < actualManaCost)
|
||||
return false;
|
||||
|
||||
if (networkController != null && actualManaCost > 0f)
|
||||
{
|
||||
networkController.UseManaRpc(actualManaCost);
|
||||
}
|
||||
|
||||
BroadcastSkillExecutionRpc(slotIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnSkill1Performed(InputAction.CallbackContext context) => OnSkillInput(0);
|
||||
|
||||
private void OnSkill2Performed(InputAction.CallbackContext context) => OnSkillInput(1);
|
||||
@@ -374,5 +592,123 @@ namespace Colosseum.Player
|
||||
|
||||
return gameObject.AddComponent<PlayerActionState>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 로컬/원격 여부와 관계없이 런타임 참조를 보정합니다.
|
||||
/// 서버에서 원격 플레이어 스킬을 디버그 실행할 때도 동일한 검증 경로를 쓰기 위해 필요합니다.
|
||||
/// </summary>
|
||||
private void EnsureRuntimeReferences()
|
||||
{
|
||||
if (skillController == null)
|
||||
{
|
||||
skillController = GetComponent<SkillController>();
|
||||
}
|
||||
|
||||
if (networkController == null)
|
||||
{
|
||||
networkController = GetComponent<PlayerNetworkController>();
|
||||
}
|
||||
|
||||
if (weaponEquipment == null)
|
||||
{
|
||||
weaponEquipment = GetComponent<WeaponEquipment>();
|
||||
}
|
||||
|
||||
if (actionState == null)
|
||||
{
|
||||
actionState = GetOrCreateActionState();
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// MPP 환경에서는 메인 에디터에 탱커, 가상 플레이어 복제본에 지원 프리셋을 자동 적용합니다.
|
||||
/// </summary>
|
||||
private void ApplyEditorMultiplayerLoadoutIfNeeded()
|
||||
{
|
||||
if (!ShouldApplyMppmLoadout())
|
||||
return;
|
||||
|
||||
string[] loadoutPaths = GetMppmLoadoutPathsForOwner();
|
||||
List<SkillData> loadout = LoadSkillAssets(loadoutPaths);
|
||||
if (loadout == null || loadout.Count == 0)
|
||||
return;
|
||||
|
||||
SetSkills(loadout);
|
||||
Debug.Log($"[MPP] 자동 프리셋 적용: {GetMppmLoadoutLabel()} (OwnerClientId={OwnerClientId})");
|
||||
}
|
||||
|
||||
private static bool ShouldApplyMppmLoadout()
|
||||
{
|
||||
string systemDataPath = GetMppmSystemDataPath();
|
||||
if (string.IsNullOrEmpty(systemDataPath) || !File.Exists(systemDataPath))
|
||||
return false;
|
||||
|
||||
string json = File.ReadAllText(systemDataPath);
|
||||
if (!json.Contains("\"IsMppmActive\": true", StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
return Regex.Matches(json, "\"Active\"\\s*:\\s*true").Count > 1;
|
||||
}
|
||||
|
||||
private static string GetMppmSystemDataPath()
|
||||
{
|
||||
string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
|
||||
if (IsVirtualProjectCloneInEditor())
|
||||
return Path.GetFullPath(Path.Combine(projectRoot, "..", "SystemData.json"));
|
||||
|
||||
return Path.Combine(projectRoot, "Library", "VP", "SystemData.json");
|
||||
}
|
||||
|
||||
private static bool IsVirtualProjectCloneInEditor()
|
||||
{
|
||||
string[] arguments = Environment.GetCommandLineArgs();
|
||||
for (int i = 0; i < arguments.Length; i++)
|
||||
{
|
||||
if (string.Equals(arguments[i], "--virtual-project-clone", StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<SkillData> LoadSkillAssets(IReadOnlyList<string> assetPaths)
|
||||
{
|
||||
List<SkillData> skills = new List<SkillData>(assetPaths.Count);
|
||||
for (int i = 0; i < assetPaths.Count; i++)
|
||||
{
|
||||
SkillData skill = AssetDatabase.LoadAssetAtPath<SkillData>(assetPaths[i]);
|
||||
if (skill == null)
|
||||
{
|
||||
Debug.LogWarning($"[MPP] 스킬 에셋을 찾지 못했습니다: {assetPaths[i]}");
|
||||
return null;
|
||||
}
|
||||
|
||||
skills.Add(skill);
|
||||
}
|
||||
|
||||
return skills;
|
||||
}
|
||||
|
||||
private string[] GetMppmLoadoutPathsForOwner()
|
||||
{
|
||||
return OwnerClientId switch
|
||||
{
|
||||
0 => TankLoadoutPaths,
|
||||
1 => SupportLoadoutPaths,
|
||||
_ => DpsLoadoutPaths,
|
||||
};
|
||||
}
|
||||
|
||||
private string GetMppmLoadoutLabel()
|
||||
{
|
||||
return OwnerClientId switch
|
||||
{
|
||||
0 => "탱커",
|
||||
1 => "지원",
|
||||
_ => "딜러",
|
||||
};
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
69
Assets/_Game/Scripts/Skills/PlayerLoadoutPreset.cs
Normal file
69
Assets/_Game/Scripts/Skills/PlayerLoadoutPreset.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace Colosseum.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// 플레이어가 슬롯별로 사용할 스킬/젬 조합 프리셋입니다.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "NewPlayerLoadoutPreset", menuName = "Colosseum/Player Loadout Preset")]
|
||||
public class PlayerLoadoutPreset : ScriptableObject
|
||||
{
|
||||
private const int DefaultSlotCount = 7;
|
||||
|
||||
[Header("기본 정보")]
|
||||
[SerializeField] private string presetName;
|
||||
[TextArea(2, 4)]
|
||||
[SerializeField] private string description;
|
||||
|
||||
[Header("슬롯 구성")]
|
||||
[SerializeField] private SkillLoadoutEntry[] slots = new SkillLoadoutEntry[DefaultSlotCount];
|
||||
|
||||
public string PresetName => presetName;
|
||||
public string Description => description;
|
||||
public IReadOnlyList<SkillLoadoutEntry> Slots => slots;
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
EnsureSlotCapacity();
|
||||
}
|
||||
|
||||
public void EnsureSlotCapacity(int slotCount = DefaultSlotCount)
|
||||
{
|
||||
slotCount = Mathf.Max(0, slotCount);
|
||||
if (slots != null && slots.Length == slotCount)
|
||||
{
|
||||
EnsureGemSlots();
|
||||
return;
|
||||
}
|
||||
|
||||
SkillLoadoutEntry[] resized = new SkillLoadoutEntry[slotCount];
|
||||
if (slots != null)
|
||||
{
|
||||
int copyCount = Mathf.Min(slots.Length, resized.Length);
|
||||
for (int i = 0; i < copyCount; i++)
|
||||
{
|
||||
resized[i] = slots[i];
|
||||
}
|
||||
}
|
||||
|
||||
slots = resized;
|
||||
EnsureGemSlots();
|
||||
}
|
||||
|
||||
private void EnsureGemSlots()
|
||||
{
|
||||
if (slots == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < slots.Length; i++)
|
||||
{
|
||||
if (slots[i] == null)
|
||||
slots[i] = new SkillLoadoutEntry();
|
||||
|
||||
slots[i].EnsureGemSlotCapacity();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/Skills/PlayerLoadoutPreset.cs.meta
Normal file
2
Assets/_Game/Scripts/Skills/PlayerLoadoutPreset.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 26d5895a89de4f24aade1ea4b5f7644e
|
||||
@@ -53,6 +53,9 @@ namespace Colosseum.Skills
|
||||
|
||||
// 현재 실행 중인 스킬
|
||||
private SkillData currentSkill;
|
||||
private SkillLoadoutEntry currentLoadoutEntry;
|
||||
private readonly List<SkillEffect> currentCastStartEffects = new();
|
||||
private readonly Dictionary<int, List<SkillEffect>> currentTriggeredEffects = new();
|
||||
private bool skillEndRequested; // OnSkillEnd 이벤트 호출 여부
|
||||
private bool waitingForEndAnimation; // EndAnimation 종료 대기 중
|
||||
|
||||
@@ -66,6 +69,7 @@ namespace Colosseum.Skills
|
||||
public bool UsesRootMotion => currentSkill != null && currentSkill.UseRootMotion;
|
||||
public bool IgnoreRootMotionY => currentSkill != null && currentSkill.IgnoreRootMotionY;
|
||||
public SkillData CurrentSkill => currentSkill;
|
||||
public SkillLoadoutEntry CurrentLoadoutEntry => currentLoadoutEntry;
|
||||
public Animator Animator => animator;
|
||||
public SkillCancelReason LastCancelReason => lastCancelReason;
|
||||
public string LastCancelledSkillName => lastCancelledSkillName;
|
||||
@@ -131,6 +135,15 @@ namespace Colosseum.Skills
|
||||
/// </summary>
|
||||
public bool ExecuteSkill(SkillData skill)
|
||||
{
|
||||
return ExecuteSkill(SkillLoadoutEntry.CreateTemporary(skill));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 슬롯 엔트리 기준으로 스킬 시전
|
||||
/// </summary>
|
||||
public bool ExecuteSkill(SkillLoadoutEntry loadoutEntry)
|
||||
{
|
||||
SkillData skill = loadoutEntry != null ? loadoutEntry.BaseSkill : null;
|
||||
if (skill == null)
|
||||
{
|
||||
Debug.LogWarning("Skill is null!");
|
||||
@@ -157,17 +170,19 @@ namespace Colosseum.Skills
|
||||
return false;
|
||||
}
|
||||
|
||||
currentLoadoutEntry = loadoutEntry != null ? loadoutEntry.CreateCopy() : SkillLoadoutEntry.CreateTemporary(skill);
|
||||
currentSkill = skill;
|
||||
skillEndRequested = false;
|
||||
waitingForEndAnimation = false;
|
||||
lastCancelReason = SkillCancelReason.None;
|
||||
BuildResolvedEffects(currentLoadoutEntry);
|
||||
|
||||
if (debugMode) Debug.Log($"[Skill] Cast: {skill.SkillName}");
|
||||
|
||||
// 쿨타임 시작
|
||||
StartCooldown(skill);
|
||||
StartCooldown(skill, currentLoadoutEntry.GetResolvedCooldown());
|
||||
|
||||
TriggerCastStartEffects(skill);
|
||||
TriggerCastStartEffects();
|
||||
|
||||
// 스킬 애니메이션 재생
|
||||
if (skill.SkillClip != null && animator != null)
|
||||
@@ -176,7 +191,7 @@ namespace Colosseum.Skills
|
||||
PlaySkillClip(skill.SkillClip);
|
||||
}
|
||||
|
||||
TriggerImmediateSelfEffectsIfNeeded(skill);
|
||||
TriggerImmediateSelfEffectsIfNeeded();
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -185,17 +200,17 @@ namespace Colosseum.Skills
|
||||
/// 시전 시작 즉시 발동하는 효과를 실행합니다.
|
||||
/// 서버 권한으로만 처리해 실제 게임플레이 효과가 한 번만 적용되게 합니다.
|
||||
/// </summary>
|
||||
private void TriggerCastStartEffects(SkillData skill)
|
||||
private void TriggerCastStartEffects()
|
||||
{
|
||||
if (skill == null || skill.CastStartEffects == null || skill.CastStartEffects.Count == 0)
|
||||
if (currentSkill == null || currentCastStartEffects.Count == 0)
|
||||
return;
|
||||
|
||||
if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < skill.CastStartEffects.Count; i++)
|
||||
for (int i = 0; i < currentCastStartEffects.Count; i++)
|
||||
{
|
||||
SkillEffect effect = skill.CastStartEffects[i];
|
||||
SkillEffect effect = currentCastStartEffects[i];
|
||||
if (effect == null)
|
||||
continue;
|
||||
|
||||
@@ -208,20 +223,23 @@ namespace Colosseum.Skills
|
||||
/// 애니메이션 이벤트가 없는 자기 자신 대상 효과는 시전 즉시 발동합니다.
|
||||
/// 버프/무적 같은 자기 강화 스킬이 이벤트 누락으로 동작하지 않는 상황을 막기 위한 보정입니다.
|
||||
/// </summary>
|
||||
private void TriggerImmediateSelfEffectsIfNeeded(SkillData skill)
|
||||
private void TriggerImmediateSelfEffectsIfNeeded()
|
||||
{
|
||||
if (skill == null || skill.Effects == null || skill.Effects.Count == 0)
|
||||
if (currentSkill == null || currentTriggeredEffects.Count == 0)
|
||||
return;
|
||||
|
||||
if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer)
|
||||
return;
|
||||
|
||||
if (skill.SkillClip != null && skill.SkillClip.events != null && skill.SkillClip.events.Length > 0)
|
||||
if (currentSkill.SkillClip != null && currentSkill.SkillClip.events != null && currentSkill.SkillClip.events.Length > 0)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < skill.Effects.Count; i++)
|
||||
if (!currentTriggeredEffects.TryGetValue(0, out List<SkillEffect> effectsAtZero))
|
||||
return;
|
||||
|
||||
for (int i = 0; i < effectsAtZero.Count; i++)
|
||||
{
|
||||
SkillEffect effect = skill.Effects[i];
|
||||
SkillEffect effect = effectsAtZero[i];
|
||||
if (effect == null || effect.TargetType != TargetType.Self)
|
||||
continue;
|
||||
|
||||
@@ -230,6 +248,21 @@ namespace Colosseum.Skills
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 슬롯 엔트리 기준으로 시전 시작/트리거 효과를 합성합니다.
|
||||
/// </summary>
|
||||
private void BuildResolvedEffects(SkillLoadoutEntry loadoutEntry)
|
||||
{
|
||||
currentCastStartEffects.Clear();
|
||||
currentTriggeredEffects.Clear();
|
||||
|
||||
if (loadoutEntry == null)
|
||||
return;
|
||||
|
||||
loadoutEntry.CollectCastStartEffects(currentCastStartEffects);
|
||||
loadoutEntry.CollectTriggeredEffects(currentTriggeredEffects);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 스킬 클립으로 Override Controller 생성 후 재생
|
||||
/// </summary>
|
||||
@@ -354,23 +387,28 @@ namespace Colosseum.Skills
|
||||
return;
|
||||
}
|
||||
|
||||
var effects = currentSkill.Effects;
|
||||
if (index < 0 || index >= effects.Count)
|
||||
if (!currentTriggeredEffects.TryGetValue(index, out List<SkillEffect> effects) || effects == null || effects.Count == 0)
|
||||
{
|
||||
if (debugMode) Debug.LogWarning($"[Effect] Invalid index: {index}");
|
||||
return;
|
||||
}
|
||||
|
||||
var effect = effects[index];
|
||||
if (debugMode) Debug.Log($"[Effect] {effect.name} (index {index})");
|
||||
|
||||
// 공격 범위 시각화
|
||||
if (showAreaDebug)
|
||||
for (int i = 0; i < effects.Count; i++)
|
||||
{
|
||||
effect.DrawDebugRange(gameObject, debugDrawDuration);
|
||||
}
|
||||
SkillEffect effect = effects[i];
|
||||
if (effect == null)
|
||||
continue;
|
||||
|
||||
effect.ExecuteOnCast(gameObject);
|
||||
if (debugMode) Debug.Log($"[Effect] {effect.name} (index {index})");
|
||||
|
||||
// 공격 범위 시각화
|
||||
if (showAreaDebug)
|
||||
{
|
||||
effect.DrawDebugRange(gameObject, debugDrawDuration);
|
||||
}
|
||||
|
||||
effect.ExecuteOnCast(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -408,6 +446,9 @@ namespace Colosseum.Skills
|
||||
|
||||
RestoreBaseController();
|
||||
currentSkill = null;
|
||||
currentLoadoutEntry = null;
|
||||
currentCastStartEffects.Clear();
|
||||
currentTriggeredEffects.Clear();
|
||||
skillEndRequested = false;
|
||||
waitingForEndAnimation = false;
|
||||
return true;
|
||||
@@ -430,9 +471,9 @@ namespace Colosseum.Skills
|
||||
return Mathf.Max(0f, remaining);
|
||||
}
|
||||
|
||||
private void StartCooldown(SkillData skill)
|
||||
private void StartCooldown(SkillData skill, float cooldownDuration)
|
||||
{
|
||||
cooldownTracker[skill] = Time.time + skill.Cooldown;
|
||||
cooldownTracker[skill] = Time.time + cooldownDuration;
|
||||
}
|
||||
|
||||
public void ResetCooldown(SkillData skill)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using UnityEngine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace Colosseum.Skills
|
||||
{
|
||||
/// <summary>
|
||||
@@ -43,6 +45,10 @@ namespace Colosseum.Skills
|
||||
[Min(0f)] [SerializeField] private float cooldown = 1f;
|
||||
[Min(0f)] [SerializeField] private float manaCost = 0f;
|
||||
|
||||
[Header("젬 슬롯")]
|
||||
[Tooltip("이 스킬에 장착 가능한 젬 슬롯 수")]
|
||||
[Min(0)] [SerializeField] private int maxGemSlotCount = 2;
|
||||
|
||||
[Header("효과 목록")]
|
||||
[Tooltip("시전 시작 즉시 발동하는 효과 목록. 시전 보호 버프 등에 사용됩니다.")]
|
||||
[SerializeField] private List<SkillEffect> castStartEffects = new List<SkillEffect>();
|
||||
@@ -60,6 +66,7 @@ namespace Colosseum.Skills
|
||||
public float AnimationSpeed => animationSpeed;
|
||||
public float Cooldown => cooldown;
|
||||
public float ManaCost => manaCost;
|
||||
public int MaxGemSlotCount => maxGemSlotCount;
|
||||
public bool UseRootMotion => useRootMotion;
|
||||
public bool IgnoreRootMotionY => ignoreRootMotionY;
|
||||
public bool JumpToTarget => jumpToTarget;
|
||||
|
||||
55
Assets/_Game/Scripts/Skills/SkillGemData.cs
Normal file
55
Assets/_Game/Scripts/Skills/SkillGemData.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace Colosseum.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// 젬 효과가 발동될 애니메이션 이벤트 인덱스와 효과 목록입니다.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class SkillGemTriggeredEffectEntry
|
||||
{
|
||||
[Tooltip("OnEffect(index)와 매칭되는 애니메이션 이벤트 인덱스")]
|
||||
[Min(0)] [SerializeField] private int triggerIndex = 0;
|
||||
[Tooltip("해당 인덱스에서 함께 실행할 추가 효과")]
|
||||
[SerializeField] private List<SkillEffect> effects = new();
|
||||
|
||||
public int TriggerIndex => triggerIndex;
|
||||
public IReadOnlyList<SkillEffect> Effects => effects;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 스킬의 기반 효과 위에 추가 동작을 덧붙이는 젬 데이터입니다.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "NewSkillGem", menuName = "Colosseum/Skill Gem")]
|
||||
public class SkillGemData : ScriptableObject
|
||||
{
|
||||
[Header("기본 정보")]
|
||||
[SerializeField] private string gemName;
|
||||
[TextArea(2, 4)]
|
||||
[SerializeField] private string description;
|
||||
[SerializeField] private Sprite icon;
|
||||
|
||||
[Header("기본 수치 보정")]
|
||||
[Tooltip("장착 시 마나 비용 배율")]
|
||||
[Min(0f)] [SerializeField] private float manaCostMultiplier = 1f;
|
||||
[Tooltip("장착 시 쿨타임 배율")]
|
||||
[Min(0f)] [SerializeField] private float cooldownMultiplier = 1f;
|
||||
|
||||
[Header("추가 효과")]
|
||||
[Tooltip("시전 시작 시 즉시 발동하는 추가 효과")]
|
||||
[SerializeField] private List<SkillEffect> castStartEffects = new();
|
||||
[Tooltip("애니메이션 이벤트 인덱스별로 발동하는 추가 효과")]
|
||||
[SerializeField] private List<SkillGemTriggeredEffectEntry> triggeredEffects = new();
|
||||
|
||||
public string GemName => gemName;
|
||||
public string Description => description;
|
||||
public Sprite Icon => icon;
|
||||
public float ManaCostMultiplier => manaCostMultiplier;
|
||||
public float CooldownMultiplier => cooldownMultiplier;
|
||||
public IReadOnlyList<SkillEffect> CastStartEffects => castStartEffects;
|
||||
public IReadOnlyList<SkillGemTriggeredEffectEntry> TriggeredEffects => triggeredEffects;
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/Skills/SkillGemData.cs.meta
Normal file
2
Assets/_Game/Scripts/Skills/SkillGemData.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e81a62ae7c7624847ab572ff37789bb8
|
||||
226
Assets/_Game/Scripts/Skills/SkillLoadoutEntry.cs
Normal file
226
Assets/_Game/Scripts/Skills/SkillLoadoutEntry.cs
Normal file
@@ -0,0 +1,226 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace Colosseum.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// 단일 슬롯에서 사용할 스킬과 장착된 젬 조합입니다.
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class SkillLoadoutEntry
|
||||
{
|
||||
private const int DefaultGemSlotCount = 2;
|
||||
|
||||
[Tooltip("이 슬롯의 기반 스킬")]
|
||||
[SerializeField] private SkillData baseSkill;
|
||||
[Tooltip("기반 스킬에 장착된 젬")]
|
||||
[SerializeField] private SkillGemData[] socketedGems = new SkillGemData[DefaultGemSlotCount];
|
||||
|
||||
public SkillData BaseSkill => baseSkill;
|
||||
public IReadOnlyList<SkillGemData> SocketedGems => socketedGems;
|
||||
|
||||
public static SkillLoadoutEntry CreateTemporary(SkillData skill)
|
||||
{
|
||||
SkillLoadoutEntry entry = new SkillLoadoutEntry();
|
||||
entry.SetBaseSkill(skill);
|
||||
entry.EnsureGemSlotCapacity();
|
||||
return entry;
|
||||
}
|
||||
|
||||
public SkillLoadoutEntry CreateCopy()
|
||||
{
|
||||
SkillLoadoutEntry copy = new SkillLoadoutEntry();
|
||||
copy.baseSkill = baseSkill;
|
||||
copy.socketedGems = new SkillGemData[socketedGems != null ? socketedGems.Length : DefaultGemSlotCount];
|
||||
|
||||
if (socketedGems != null)
|
||||
{
|
||||
for (int i = 0; i < socketedGems.Length; i++)
|
||||
{
|
||||
copy.socketedGems[i] = socketedGems[i];
|
||||
}
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
public void EnsureGemSlotCapacity(int slotCount = -1)
|
||||
{
|
||||
if (slotCount < 0)
|
||||
{
|
||||
slotCount = baseSkill != null ? baseSkill.MaxGemSlotCount : DefaultGemSlotCount;
|
||||
}
|
||||
|
||||
slotCount = Mathf.Max(0, slotCount);
|
||||
if (socketedGems != null && socketedGems.Length == slotCount)
|
||||
return;
|
||||
|
||||
SkillGemData[] resized = new SkillGemData[slotCount];
|
||||
if (socketedGems != null)
|
||||
{
|
||||
int copyCount = Mathf.Min(socketedGems.Length, resized.Length);
|
||||
for (int i = 0; i < copyCount; i++)
|
||||
{
|
||||
resized[i] = socketedGems[i];
|
||||
}
|
||||
}
|
||||
|
||||
socketedGems = resized;
|
||||
}
|
||||
|
||||
public void SetBaseSkill(SkillData skill)
|
||||
{
|
||||
baseSkill = skill;
|
||||
EnsureGemSlotCapacity();
|
||||
}
|
||||
|
||||
public void SetGem(int slotIndex, SkillGemData gem)
|
||||
{
|
||||
EnsureGemSlotCapacity();
|
||||
if (slotIndex < 0 || slotIndex >= socketedGems.Length)
|
||||
return;
|
||||
|
||||
socketedGems[slotIndex] = gem;
|
||||
}
|
||||
|
||||
public SkillGemData GetGem(int slotIndex)
|
||||
{
|
||||
EnsureGemSlotCapacity();
|
||||
if (slotIndex < 0 || slotIndex >= socketedGems.Length)
|
||||
return null;
|
||||
|
||||
return socketedGems[slotIndex];
|
||||
}
|
||||
|
||||
public float GetResolvedManaCost()
|
||||
{
|
||||
if (baseSkill == null)
|
||||
return 0f;
|
||||
|
||||
float resolved = baseSkill.ManaCost;
|
||||
if (socketedGems == null)
|
||||
return resolved;
|
||||
|
||||
for (int i = 0; i < socketedGems.Length; i++)
|
||||
{
|
||||
SkillGemData gem = socketedGems[i];
|
||||
if (gem == null)
|
||||
continue;
|
||||
|
||||
resolved *= gem.ManaCostMultiplier;
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
public float GetResolvedCooldown()
|
||||
{
|
||||
if (baseSkill == null)
|
||||
return 0f;
|
||||
|
||||
float resolved = baseSkill.Cooldown;
|
||||
if (socketedGems == null)
|
||||
return resolved;
|
||||
|
||||
for (int i = 0; i < socketedGems.Length; i++)
|
||||
{
|
||||
SkillGemData gem = socketedGems[i];
|
||||
if (gem == null)
|
||||
continue;
|
||||
|
||||
resolved *= gem.CooldownMultiplier;
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
public void CollectCastStartEffects(List<SkillEffect> destination)
|
||||
{
|
||||
if (destination == null)
|
||||
return;
|
||||
|
||||
if (baseSkill != null && baseSkill.CastStartEffects != null)
|
||||
{
|
||||
for (int i = 0; i < baseSkill.CastStartEffects.Count; i++)
|
||||
{
|
||||
SkillEffect effect = baseSkill.CastStartEffects[i];
|
||||
if (effect != null)
|
||||
destination.Add(effect);
|
||||
}
|
||||
}
|
||||
|
||||
if (socketedGems == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < socketedGems.Length; i++)
|
||||
{
|
||||
SkillGemData gem = socketedGems[i];
|
||||
if (gem == null || gem.CastStartEffects == null)
|
||||
continue;
|
||||
|
||||
for (int j = 0; j < gem.CastStartEffects.Count; j++)
|
||||
{
|
||||
SkillEffect effect = gem.CastStartEffects[j];
|
||||
if (effect != null)
|
||||
destination.Add(effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CollectTriggeredEffects(Dictionary<int, List<SkillEffect>> destination)
|
||||
{
|
||||
if (destination == null)
|
||||
return;
|
||||
|
||||
if (baseSkill != null && baseSkill.Effects != null)
|
||||
{
|
||||
for (int i = 0; i < baseSkill.Effects.Count; i++)
|
||||
{
|
||||
SkillEffect effect = baseSkill.Effects[i];
|
||||
if (effect == null)
|
||||
continue;
|
||||
|
||||
AddTriggeredEffect(destination, i, effect);
|
||||
}
|
||||
}
|
||||
|
||||
if (socketedGems == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < socketedGems.Length; i++)
|
||||
{
|
||||
SkillGemData gem = socketedGems[i];
|
||||
if (gem == null || gem.TriggeredEffects == null)
|
||||
continue;
|
||||
|
||||
for (int j = 0; j < gem.TriggeredEffects.Count; j++)
|
||||
{
|
||||
SkillGemTriggeredEffectEntry entry = gem.TriggeredEffects[j];
|
||||
if (entry == null || entry.Effects == null)
|
||||
continue;
|
||||
|
||||
for (int k = 0; k < entry.Effects.Count; k++)
|
||||
{
|
||||
SkillEffect effect = entry.Effects[k];
|
||||
if (effect == null)
|
||||
continue;
|
||||
|
||||
AddTriggeredEffect(destination, entry.TriggerIndex, effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddTriggeredEffect(Dictionary<int, List<SkillEffect>> destination, int triggerIndex, SkillEffect effect)
|
||||
{
|
||||
if (!destination.TryGetValue(triggerIndex, out List<SkillEffect> effectList))
|
||||
{
|
||||
effectList = new List<SkillEffect>();
|
||||
destination.Add(triggerIndex, effectList);
|
||||
}
|
||||
|
||||
effectList.Add(effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/Skills/SkillLoadoutEntry.cs.meta
Normal file
2
Assets/_Game/Scripts/Skills/SkillLoadoutEntry.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d06556806f8c370429a54ca7af7e2a34
|
||||
@@ -1,3 +1,5 @@
|
||||
using System;
|
||||
|
||||
using UnityEngine;
|
||||
using Unity.Netcode;
|
||||
using Unity.Netcode.Transports.UTP;
|
||||
@@ -34,7 +36,10 @@ namespace Colosseum.UI
|
||||
#if UNITY_EDITOR
|
||||
if (autoStartHostInEditor && NetworkManager.Singleton != null && !NetworkManager.Singleton.IsListening)
|
||||
{
|
||||
StartHost();
|
||||
if (IsVirtualProjectClone())
|
||||
StartClient();
|
||||
else
|
||||
StartHost();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -107,5 +112,20 @@ namespace Colosseum.UI
|
||||
UpdateTransportSettings();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MPP 가상 플레이어 복제본 에디터인지 확인합니다.
|
||||
/// </summary>
|
||||
private static bool IsVirtualProjectClone()
|
||||
{
|
||||
string[] arguments = Environment.GetCommandLineArgs();
|
||||
for (int i = 0; i < arguments.Length; i++)
|
||||
{
|
||||
if (string.Equals(arguments[i], "--virtual-project-clone", StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user