feat: 젬 테스트 경로 및 보스 기절 디버그 추가

- 다중 젬 슬롯용 타입을 별도 스크립트로 분리하고 테스트 젬/로드아웃 자산 생성 경로를 정리

- 젬 테스트 전용 공격 스킬과 분리된 애니메이션 자산을 추가해 베이스 스킬 검증 경로를 마련

- PlayerSkillDebugMenu와 MPP 디버그 메뉴를 보강해 젬 프리셋 적용, 원격 테스트, 보스 기절 디버그 메뉴를 추가

- BossCombatBehaviorContext와 공통 BT 액션이 기절 상태를 존중하도록 수정해 보스 추적과 패턴 실행을 중단

- Unity 리프레시와 외부 빌드 통과를 확인하고 드로그전 및 MPP 기준 젬 프리셋 적용 흐름을 검증
This commit is contained in:
2026-03-25 18:38:12 +09:00
parent 35a5b272cb
commit 24b284ad7e
39 changed files with 4443 additions and 463 deletions

View 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:

View File

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

View File

@@ -0,0 +1,46 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 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}

View File

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

View File

@@ -0,0 +1,46 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 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}

View File

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

View File

@@ -0,0 +1,46 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 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}

View File

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

View File

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

View 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}

View File

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

View 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}

View File

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

View 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}

View File

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

View 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}

View File

@@ -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

View File

@@ -47,6 +47,12 @@ public abstract partial class BossPatternActionBase : Action
if (!IsReady()) if (!IsReady())
return Status.Failure; return Status.Failure;
if (combatBehaviorContext.IsBehaviorSuppressed)
{
StopMovement();
return Status.Failure;
}
if (bossEnemy.IsDead || bossEnemy.IsTransitioning) if (bossEnemy.IsDead || bossEnemy.IsTransitioning)
return Status.Failure; return Status.Failure;
@@ -71,6 +77,12 @@ public abstract partial class BossPatternActionBase : Action
if (!IsReady() || activePattern == null) if (!IsReady() || activePattern == null)
return Status.Failure; return Status.Failure;
if (combatBehaviorContext.IsBehaviorSuppressed)
{
StopMovement();
return Status.Failure;
}
if (bossEnemy.IsDead || bossEnemy.IsTransitioning) if (bossEnemy.IsDead || bossEnemy.IsTransitioning)
return Status.Failure; return Status.Failure;

View File

@@ -1,4 +1,5 @@
using System; using System;
using Colosseum.Enemy;
using Unity.Behavior; using Unity.Behavior;
using UnityEngine; using UnityEngine;
using Action = Unity.Behavior.Action; using Action = Unity.Behavior.Action;
@@ -22,6 +23,12 @@ public partial class ChaseTargetAction : Action
protected override Status OnStart() protected override Status OnStart()
{ {
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
if (context != null && context.IsBehaviorSuppressed)
{
return Status.Failure;
}
if (Target.Value == null) if (Target.Value == null)
{ {
return Status.Failure; return Status.Failure;
@@ -47,6 +54,15 @@ public partial class ChaseTargetAction : Action
protected override Status OnUpdate() 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) if (Target.Value == null)
{ {
return Status.Failure; return Status.Failure;

View File

@@ -25,6 +25,9 @@ public abstract partial class CheckPatternReadyActionBase : Action
if (context == null) if (context == null)
return Status.Failure; return Status.Failure;
if (context.IsBehaviorSuppressed)
return Status.Failure;
BossPatternData pattern = context.GetPattern(PatternRole); BossPatternData pattern = context.GetPattern(PatternRole);
return UsePatternAction.IsPatternReady(GameObject, pattern) ? Status.Success : Status.Failure; return UsePatternAction.IsPatternReady(GameObject, pattern) ? Status.Success : Status.Failure;
} }

View File

@@ -21,6 +21,9 @@ public partial class CheckSignaturePatternReadyAction : Action
protected override Status OnStart() protected override Status OnStart()
{ {
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>(); BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
if (context != null && context.IsBehaviorSuppressed)
return Status.Failure;
return context != null && context.IsSignaturePatternReady() return context != null && context.IsSignaturePatternReady()
? Status.Success ? Status.Success
: Status.Failure; : Status.Failure;

View File

@@ -24,6 +24,10 @@ public partial class RefreshPrimaryTargetAction : Action
protected override Status OnStart() protected override Status OnStart()
{ {
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
if (context != null && context.IsBehaviorSuppressed)
return Status.Failure;
EnemyBase enemyBase = GameObject.GetComponent<EnemyBase>(); EnemyBase enemyBase = GameObject.GetComponent<EnemyBase>();
if (enemyBase == null) if (enemyBase == null)
return Status.Failure; return Status.Failure;
@@ -34,7 +38,6 @@ public partial class RefreshPrimaryTargetAction : Action
if (resolvedTarget == null) if (resolvedTarget == null)
{ {
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
resolvedTarget = context != null ? context.FindNearestLivingTarget() : null; resolvedTarget = context != null ? context.FindNearestLivingTarget() : null;
} }

View 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();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: eb18c83f7c6efff429f59061e7f0b07b

View File

@@ -23,6 +23,7 @@ namespace Colosseum.Editor
private const string ShieldSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_보호막.asset"; 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 SlashSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_베기.asset";
private const string PierceSkillPath = "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 SpinSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_회전베기.asset";
private const string DashSkillPath = "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"; 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 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 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 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")] [MenuItem("Tools/Colosseum/Debug/Cast Local Skill 3")]
private static void CastLocalSkill3() private static void CastLocalSkill3()
@@ -40,6 +49,12 @@ namespace Colosseum.Editor
CastLocalSkill(2); CastLocalSkill(2);
} }
[MenuItem("Tools/Colosseum/Debug/Cast Local Skill R")]
private static void CastLocalSkillR()
{
CastLocalSkill(1);
}
[MenuItem("Tools/Colosseum/Debug/Cast Local Skill 4")] [MenuItem("Tools/Colosseum/Debug/Cast Local Skill 4")]
private static void CastLocalSkill4() private static void CastLocalSkill4()
{ {
@@ -52,6 +67,42 @@ namespace Colosseum.Editor
CastLocalSkill(4); 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")] [MenuItem("Tools/Colosseum/Debug/Cast Local Heal")]
private static void CastLocalHeal() private static void CastLocalHeal()
{ {
@@ -134,14 +185,14 @@ namespace Colosseum.Editor
continue; continue;
if (builder.Length > 0) if (builder.Length > 0)
builder.AppendLine().AppendLine(); builder.Append(" || ");
builder.Append(enemy.name); builder.Append(enemy.name);
builder.Append(" : "); builder.Append(" : ");
builder.Append(enemy.GetThreatDebugSummary().Replace("\r\n", " | ").Replace("\n", " | ")); 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")] [MenuItem("Tools/Colosseum/Debug/Apply Local Stun")]
@@ -162,6 +213,33 @@ namespace Colosseum.Editor
ApplyLocalAbnormality(MarkAbnormalityPath); 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")] [MenuItem("Tools/Colosseum/Debug/Log HUD Abnormality Summary")]
private static void LogHudAbnormalitySummary() private static void LogHudAbnormalitySummary()
{ {
@@ -223,6 +301,131 @@ namespace Colosseum.Editor
EvadeSkillPath); 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")] [MenuItem("Tools/Colosseum/Debug/Log Local Skill Loadout")]
private static void LogLocalSkillLoadout() private static void LogLocalSkillLoadout()
{ {
@@ -245,9 +448,11 @@ namespace Colosseum.Editor
for (int i = 0; i < slotOrder.Length; i++) for (int i = 0; i < slotOrder.Length; i++)
{ {
SkillData skill = localSkillInput.GetSkill(slotOrder[i]); SkillData skill = localSkillInput.GetSkill(slotOrder[i]);
SkillLoadoutEntry loadoutEntry = localSkillInput.GetSkillLoadout(slotOrder[i]);
builder.Append(slotNames[i]); builder.Append(slotNames[i]);
builder.Append(": "); builder.Append(": ");
builder.Append(skill != null ? skill.SkillName : "(비어 있음)"); builder.Append(skill != null ? skill.SkillName : "(비어 있음)");
AppendGemSummary(builder, loadoutEntry);
if (i < slotOrder.Length - 1) if (i < slotOrder.Length - 1)
builder.Append(" | "); builder.Append(" | ");
@@ -291,6 +496,20 @@ namespace Colosseum.Editor
return localNetworkController.GetComponent<AbnormalityManager>(); 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) private static void CastLocalSkill(int slotIndex)
{ {
if (!EditorApplication.isPlaying) if (!EditorApplication.isPlaying)
@@ -391,5 +610,197 @@ namespace Colosseum.Editor
localSkillInput.SetSkills(skills); localSkillInput.SetSkills(skills);
Debug.Log($"[Debug] {loadoutName} 프리셋을 적용했습니다."); 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;
}
} }
} }

View File

@@ -27,6 +27,7 @@ namespace Colosseum.Enemy
[SerializeField] protected BossEnemy bossEnemy; [SerializeField] protected BossEnemy bossEnemy;
[SerializeField] protected EnemyBase enemyBase; [SerializeField] protected EnemyBase enemyBase;
[SerializeField] protected SkillController skillController; [SerializeField] protected SkillController skillController;
[SerializeField] protected AbnormalityManager abnormalityManager;
[SerializeField] protected UnityEngine.AI.NavMeshAgent navMeshAgent; [SerializeField] protected UnityEngine.AI.NavMeshAgent navMeshAgent;
[SerializeField] protected BehaviorGraphAgent behaviorGraphAgent; [SerializeField] protected BehaviorGraphAgent behaviorGraphAgent;
@@ -192,6 +193,11 @@ namespace Colosseum.Enemy
/// </summary> /// </summary>
public bool DebugModeEnabled => debugMode; public bool DebugModeEnabled => debugMode;
/// <summary>
/// 기절 등으로 인해 보스 전투 로직을 진행할 수 없는 상태인지 여부
/// </summary>
public bool IsBehaviorSuppressed => abnormalityManager != null && abnormalityManager.IsStunned;
/// <summary> /// <summary>
/// 현재 보스 패턴 페이즈 /// 현재 보스 패턴 페이즈
/// </summary> /// </summary>
@@ -238,6 +244,12 @@ namespace Colosseum.Enemy
if (bossEnemy.IsDead || bossEnemy.IsTransitioning) if (bossEnemy.IsDead || bossEnemy.IsTransitioning)
return; return;
if (IsBehaviorSuppressed)
{
StopMovement();
return;
}
if (!disableBehaviorGraph) if (!disableBehaviorGraph)
return; return;
@@ -468,6 +480,9 @@ namespace Colosseum.Enemy
if (!IsServer || bossEnemy == null || skillController == null) if (!IsServer || bossEnemy == null || skillController == null)
return false; return false;
if (IsBehaviorSuppressed)
return false;
if (CurrentPatternPhase < signatureMinPhase) if (CurrentPatternPhase < signatureMinPhase)
return false; return false;
@@ -699,6 +714,9 @@ namespace Colosseum.Enemy
if (skillController == null) if (skillController == null)
skillController = GetComponent<SkillController>(); skillController = GetComponent<SkillController>();
if (abnormalityManager == null)
abnormalityManager = GetComponent<AbnormalityManager>();
if (navMeshAgent == null) if (navMeshAgent == null)
navMeshAgent = GetComponent<UnityEngine.AI.NavMeshAgent>(); navMeshAgent = GetComponent<UnityEngine.AI.NavMeshAgent>();

View File

@@ -7,6 +7,13 @@ using System.Collections.Generic;
using Colosseum.Skills; using Colosseum.Skills;
using Colosseum.Weapons; using Colosseum.Weapons;
#if UNITY_EDITOR
using System.IO;
using System.Text.RegularExpressions;
using UnityEditor;
#endif
namespace Colosseum.Player namespace Colosseum.Player
{ {
/// <summary> /// <summary>
@@ -18,9 +25,46 @@ namespace Colosseum.Player
{ {
private const int ExpectedSkillSlotCount = 7; 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")] [Header("Skill Slots")]
[Tooltip("각 슬롯에 등록할 스킬 데이터 (6개 + 추가 슬롯)")] [Tooltip("각 슬롯에 등록할 스킬 데이터 (6개 + 추가 슬롯)")]
[SerializeField] private SkillData[] skillSlots = new SkillData[ExpectedSkillSlotCount]; [SerializeField] private SkillData[] skillSlots = new SkillData[ExpectedSkillSlotCount];
[Tooltip("각 슬롯의 베이스 스킬 + 젬 조합")]
[SerializeField] private SkillLoadoutEntry[] skillLoadoutEntries = new SkillLoadoutEntry[ExpectedSkillSlotCount];
[Header("References")] [Header("References")]
[Tooltip("SkillController (없으면 자동 검색)")] [Tooltip("SkillController (없으면 자동 검색)")]
@@ -35,6 +79,7 @@ namespace Colosseum.Player
private InputSystem_Actions inputActions; private InputSystem_Actions inputActions;
public SkillData[] SkillSlots => skillSlots; public SkillData[] SkillSlots => skillSlots;
public SkillLoadoutEntry[] SkillLoadoutEntries => skillLoadoutEntries;
/// <summary> /// <summary>
/// 스킬 슬롯 구성이 변경되었을 때 호출됩니다. /// 스킬 슬롯 구성이 변경되었을 때 호출됩니다.
@@ -44,6 +89,10 @@ namespace Colosseum.Player
public override void OnNetworkSpawn() public override void OnNetworkSpawn()
{ {
EnsureSkillSlotCapacity(); EnsureSkillSlotCapacity();
EnsureSkillLoadoutCapacity();
SyncLegacySkillsToLoadoutEntries();
EnsureRuntimeReferences();
ApplyEditorMultiplayerLoadoutIfNeeded();
if (!IsOwner) if (!IsOwner)
{ {
@@ -51,35 +100,6 @@ namespace Colosseum.Player
return; 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(); InitializeInputActions();
} }
@@ -113,11 +133,15 @@ namespace Colosseum.Player
private void Awake() private void Awake()
{ {
EnsureSkillSlotCapacity(); EnsureSkillSlotCapacity();
EnsureSkillLoadoutCapacity();
SyncLegacySkillsToLoadoutEntries();
} }
private void OnValidate() private void OnValidate()
{ {
EnsureSkillSlotCapacity(); EnsureSkillSlotCapacity();
EnsureSkillLoadoutCapacity();
SyncLegacySkillsToLoadoutEntries();
} }
private void OnEnable() private void OnEnable()
@@ -150,6 +174,63 @@ namespace Colosseum.Player
skillSlots = resizedSlots; 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() private void CleanupInputActions()
{ {
if (inputActions != null) if (inputActions != null)
@@ -166,7 +247,8 @@ namespace Colosseum.Player
if (slotIndex < 0 || slotIndex >= skillSlots.Length) if (slotIndex < 0 || slotIndex >= skillSlots.Length)
return; return;
SkillData skill = skillSlots[slotIndex]; SkillLoadoutEntry loadoutEntry = GetSkillLoadout(slotIndex);
SkillData skill = loadoutEntry != null ? loadoutEntry.BaseSkill : null;
if (skill == null) if (skill == null)
{ {
Debug.Log($"Skill slot {slotIndex + 1} is empty"); 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) if (networkController != null && networkController.Mana < actualManaCost)
{ {
Debug.Log($"Not enough mana for skill: {skill.SkillName}"); Debug.Log($"Not enough mana for skill: {skill.SkillName}");
@@ -211,7 +293,8 @@ namespace Colosseum.Player
if (slotIndex < 0 || slotIndex >= skillSlots.Length) if (slotIndex < 0 || slotIndex >= skillSlots.Length)
return; return;
SkillData skill = skillSlots[slotIndex]; SkillLoadoutEntry loadoutEntry = GetSkillLoadout(slotIndex);
SkillData skill = loadoutEntry != null ? loadoutEntry.BaseSkill : null;
if (skill == null) return; if (skill == null) return;
// 서버에서 다시 검증 // 서버에서 다시 검증
@@ -222,7 +305,7 @@ namespace Colosseum.Player
return; return;
// 마나 비용 체크 (무기 배율 적용) // 마나 비용 체크 (무기 배율 적용)
float actualManaCost = GetActualManaCost(skill); float actualManaCost = GetActualManaCost(loadoutEntry);
if (networkController != null && networkController.Mana < actualManaCost) if (networkController != null && networkController.Mana < actualManaCost)
return; return;
@@ -245,21 +328,22 @@ namespace Colosseum.Player
if (slotIndex < 0 || slotIndex >= skillSlots.Length) if (slotIndex < 0 || slotIndex >= skillSlots.Length)
return; return;
SkillData skill = skillSlots[slotIndex]; SkillLoadoutEntry loadoutEntry = GetSkillLoadout(slotIndex);
SkillData skill = loadoutEntry != null ? loadoutEntry.BaseSkill : null;
if (skill == null) return; if (skill == null) return;
// 모든 클라이언트에서 스킬 실행 (애니메이션 포함) // 모든 클라이언트에서 스킬 실행 (애니메이션 포함)
skillController.ExecuteSkill(skill); skillController.ExecuteSkill(loadoutEntry);
} }
/// <summary> /// <summary>
/// 무기 마나 배율이 적용된 실제 마나 비용 계산 /// 무기 마나 배율이 적용된 실제 마나 비용 계산
/// </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; float multiplier = weaponEquipment != null ? weaponEquipment.ManaCostMultiplier : 1f;
return baseCost * multiplier; return baseCost * multiplier;
@@ -271,10 +355,27 @@ namespace Colosseum.Player
public SkillData GetSkill(int slotIndex) public SkillData GetSkill(int slotIndex)
{ {
EnsureSkillSlotCapacity(); EnsureSkillSlotCapacity();
EnsureSkillLoadoutCapacity();
SyncLegacySkillsToLoadoutEntries();
if (slotIndex < 0 || slotIndex >= skillSlots.Length) if (slotIndex < 0 || slotIndex >= skillSlots.Length)
return null; 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> /// <summary>
@@ -288,6 +389,7 @@ namespace Colosseum.Player
return; return;
skillSlots[slotIndex] = skill; skillSlots[slotIndex] = skill;
skillLoadoutEntries[slotIndex].SetBaseSkill(skill);
OnSkillSlotsChanged?.Invoke(); OnSkillSlotsChanged?.Invoke();
} }
@@ -305,16 +407,92 @@ namespace Colosseum.Player
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
skillSlots[i] = skills[i]; skillSlots[i] = skills[i];
skillLoadoutEntries[i].SetBaseSkill(skills[i]);
} }
for (int i = count; i < skillSlots.Length; i++) for (int i = count; i < skillSlots.Length; i++)
{ {
skillSlots[i] = null; skillSlots[i] = null;
skillLoadoutEntries[i].SetBaseSkill(null);
} }
OnSkillSlotsChanged?.Invoke(); 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>
/// 남은 쿨타임 조회 /// 남은 쿨타임 조회
/// </summary> /// </summary>
@@ -331,6 +509,8 @@ namespace Colosseum.Player
/// </summary> /// </summary>
public bool CanUseSkill(int slotIndex) public bool CanUseSkill(int slotIndex)
{ {
EnsureRuntimeReferences();
SkillData skill = GetSkill(slotIndex); SkillData skill = GetSkill(slotIndex);
if (skill == null) return false; if (skill == null) return false;
@@ -352,6 +532,44 @@ namespace Colosseum.Player
OnSkillInput(slotIndex); 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 OnSkill1Performed(InputAction.CallbackContext context) => OnSkillInput(0);
private void OnSkill2Performed(InputAction.CallbackContext context) => OnSkillInput(1); private void OnSkill2Performed(InputAction.CallbackContext context) => OnSkillInput(1);
@@ -374,5 +592,123 @@ namespace Colosseum.Player
return gameObject.AddComponent<PlayerActionState>(); 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
} }
} }

View 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();
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 26d5895a89de4f24aade1ea4b5f7644e

View File

@@ -53,6 +53,9 @@ namespace Colosseum.Skills
// 현재 실행 중인 스킬 // 현재 실행 중인 스킬
private SkillData currentSkill; 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 skillEndRequested; // OnSkillEnd 이벤트 호출 여부
private bool waitingForEndAnimation; // EndAnimation 종료 대기 중 private bool waitingForEndAnimation; // EndAnimation 종료 대기 중
@@ -66,6 +69,7 @@ namespace Colosseum.Skills
public bool UsesRootMotion => currentSkill != null && currentSkill.UseRootMotion; public bool UsesRootMotion => currentSkill != null && currentSkill.UseRootMotion;
public bool IgnoreRootMotionY => currentSkill != null && currentSkill.IgnoreRootMotionY; public bool IgnoreRootMotionY => currentSkill != null && currentSkill.IgnoreRootMotionY;
public SkillData CurrentSkill => currentSkill; public SkillData CurrentSkill => currentSkill;
public SkillLoadoutEntry CurrentLoadoutEntry => currentLoadoutEntry;
public Animator Animator => animator; public Animator Animator => animator;
public SkillCancelReason LastCancelReason => lastCancelReason; public SkillCancelReason LastCancelReason => lastCancelReason;
public string LastCancelledSkillName => lastCancelledSkillName; public string LastCancelledSkillName => lastCancelledSkillName;
@@ -131,6 +135,15 @@ namespace Colosseum.Skills
/// </summary> /// </summary>
public bool ExecuteSkill(SkillData skill) 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) if (skill == null)
{ {
Debug.LogWarning("Skill is null!"); Debug.LogWarning("Skill is null!");
@@ -157,17 +170,19 @@ namespace Colosseum.Skills
return false; return false;
} }
currentLoadoutEntry = loadoutEntry != null ? loadoutEntry.CreateCopy() : SkillLoadoutEntry.CreateTemporary(skill);
currentSkill = skill; currentSkill = skill;
skillEndRequested = false; skillEndRequested = false;
waitingForEndAnimation = false; waitingForEndAnimation = false;
lastCancelReason = SkillCancelReason.None; lastCancelReason = SkillCancelReason.None;
BuildResolvedEffects(currentLoadoutEntry);
if (debugMode) Debug.Log($"[Skill] Cast: {skill.SkillName}"); if (debugMode) Debug.Log($"[Skill] Cast: {skill.SkillName}");
// 쿨타임 시작 // 쿨타임 시작
StartCooldown(skill); StartCooldown(skill, currentLoadoutEntry.GetResolvedCooldown());
TriggerCastStartEffects(skill); TriggerCastStartEffects();
// 스킬 애니메이션 재생 // 스킬 애니메이션 재생
if (skill.SkillClip != null && animator != null) if (skill.SkillClip != null && animator != null)
@@ -176,7 +191,7 @@ namespace Colosseum.Skills
PlaySkillClip(skill.SkillClip); PlaySkillClip(skill.SkillClip);
} }
TriggerImmediateSelfEffectsIfNeeded(skill); TriggerImmediateSelfEffectsIfNeeded();
return true; return true;
} }
@@ -185,17 +200,17 @@ namespace Colosseum.Skills
/// 시전 시작 즉시 발동하는 효과를 실행합니다. /// 시전 시작 즉시 발동하는 효과를 실행합니다.
/// 서버 권한으로만 처리해 실제 게임플레이 효과가 한 번만 적용되게 합니다. /// 서버 권한으로만 처리해 실제 게임플레이 효과가 한 번만 적용되게 합니다.
/// </summary> /// </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; return;
if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer) if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer)
return; 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) if (effect == null)
continue; continue;
@@ -208,20 +223,23 @@ namespace Colosseum.Skills
/// 애니메이션 이벤트가 없는 자기 자신 대상 효과는 시전 즉시 발동합니다. /// 애니메이션 이벤트가 없는 자기 자신 대상 효과는 시전 즉시 발동합니다.
/// 버프/무적 같은 자기 강화 스킬이 이벤트 누락으로 동작하지 않는 상황을 막기 위한 보정입니다. /// 버프/무적 같은 자기 강화 스킬이 이벤트 누락으로 동작하지 않는 상황을 막기 위한 보정입니다.
/// </summary> /// </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; return;
if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer) if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer)
return; 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; 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) if (effect == null || effect.TargetType != TargetType.Self)
continue; 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> /// <summary>
/// 스킬 클립으로 Override Controller 생성 후 재생 /// 스킬 클립으로 Override Controller 생성 후 재생
/// </summary> /// </summary>
@@ -354,23 +387,28 @@ namespace Colosseum.Skills
return; return;
} }
var effects = currentSkill.Effects; if (!currentTriggeredEffects.TryGetValue(index, out List<SkillEffect> effects) || effects == null || effects.Count == 0)
if (index < 0 || index >= effects.Count)
{ {
if (debugMode) Debug.LogWarning($"[Effect] Invalid index: {index}"); if (debugMode) Debug.LogWarning($"[Effect] Invalid index: {index}");
return; return;
} }
var effect = effects[index]; for (int i = 0; i < effects.Count; i++)
if (debugMode) Debug.Log($"[Effect] {effect.name} (index {index})");
// 공격 범위 시각화
if (showAreaDebug)
{ {
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> /// <summary>
@@ -408,6 +446,9 @@ namespace Colosseum.Skills
RestoreBaseController(); RestoreBaseController();
currentSkill = null; currentSkill = null;
currentLoadoutEntry = null;
currentCastStartEffects.Clear();
currentTriggeredEffects.Clear();
skillEndRequested = false; skillEndRequested = false;
waitingForEndAnimation = false; waitingForEndAnimation = false;
return true; return true;
@@ -430,9 +471,9 @@ namespace Colosseum.Skills
return Mathf.Max(0f, remaining); 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) public void ResetCooldown(SkillData skill)

View File

@@ -1,6 +1,8 @@
using UnityEngine; using System;
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine;
namespace Colosseum.Skills namespace Colosseum.Skills
{ {
/// <summary> /// <summary>
@@ -43,6 +45,10 @@ namespace Colosseum.Skills
[Min(0f)] [SerializeField] private float cooldown = 1f; [Min(0f)] [SerializeField] private float cooldown = 1f;
[Min(0f)] [SerializeField] private float manaCost = 0f; [Min(0f)] [SerializeField] private float manaCost = 0f;
[Header("젬 슬롯")]
[Tooltip("이 스킬에 장착 가능한 젬 슬롯 수")]
[Min(0)] [SerializeField] private int maxGemSlotCount = 2;
[Header("효과 목록")] [Header("효과 목록")]
[Tooltip("시전 시작 즉시 발동하는 효과 목록. 시전 보호 버프 등에 사용됩니다.")] [Tooltip("시전 시작 즉시 발동하는 효과 목록. 시전 보호 버프 등에 사용됩니다.")]
[SerializeField] private List<SkillEffect> castStartEffects = new List<SkillEffect>(); [SerializeField] private List<SkillEffect> castStartEffects = new List<SkillEffect>();
@@ -60,6 +66,7 @@ namespace Colosseum.Skills
public float AnimationSpeed => animationSpeed; public float AnimationSpeed => animationSpeed;
public float Cooldown => cooldown; public float Cooldown => cooldown;
public float ManaCost => manaCost; public float ManaCost => manaCost;
public int MaxGemSlotCount => maxGemSlotCount;
public bool UseRootMotion => useRootMotion; public bool UseRootMotion => useRootMotion;
public bool IgnoreRootMotionY => ignoreRootMotionY; public bool IgnoreRootMotionY => ignoreRootMotionY;
public bool JumpToTarget => jumpToTarget; public bool JumpToTarget => jumpToTarget;

View 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;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e81a62ae7c7624847ab572ff37789bb8

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d06556806f8c370429a54ca7af7e2a34

View File

@@ -1,3 +1,5 @@
using System;
using UnityEngine; using UnityEngine;
using Unity.Netcode; using Unity.Netcode;
using Unity.Netcode.Transports.UTP; using Unity.Netcode.Transports.UTP;
@@ -34,7 +36,10 @@ namespace Colosseum.UI
#if UNITY_EDITOR #if UNITY_EDITOR
if (autoStartHostInEditor && NetworkManager.Singleton != null && !NetworkManager.Singleton.IsListening) if (autoStartHostInEditor && NetworkManager.Singleton != null && !NetworkManager.Singleton.IsListening)
{ {
StartHost(); if (IsVirtualProjectClone())
StartClient();
else
StartHost();
} }
#endif #endif
} }
@@ -107,5 +112,20 @@ namespace Colosseum.UI
UpdateTransportSettings(); 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;
}
} }
} }