Compare commits

...

6 Commits

99 changed files with 8535 additions and 125 deletions

View File

@@ -2491,15 +2491,15 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 170188453522781500, guid: 54087b4bd46db9e4fb7da13cf7a6cc69, type: 3}
propertyPath: m_AnchorMax.x
value: 0
value: 0.2
objectReference: {fileID: 0}
- target: {fileID: 170188453522781500, guid: 54087b4bd46db9e4fb7da13cf7a6cc69, type: 3}
propertyPath: m_AnchorMax.y
value: 0
value: 1
objectReference: {fileID: 0}
- target: {fileID: 170188453522781500, guid: 54087b4bd46db9e4fb7da13cf7a6cc69, type: 3}
propertyPath: m_AnchorMin.x
value: 0
value: 0.2
objectReference: {fileID: 0}
- target: {fileID: 222507439395443271, guid: 54087b4bd46db9e4fb7da13cf7a6cc69, type: 3}
propertyPath: m_Enabled
@@ -2579,15 +2579,15 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 2668790415109567114, guid: 54087b4bd46db9e4fb7da13cf7a6cc69, type: 3}
propertyPath: m_AnchorMax.x
value: 0
value: 0.2
objectReference: {fileID: 0}
- target: {fileID: 2668790415109567114, guid: 54087b4bd46db9e4fb7da13cf7a6cc69, type: 3}
propertyPath: m_AnchorMax.y
value: 0
value: 1
objectReference: {fileID: 0}
- target: {fileID: 2668790415109567114, guid: 54087b4bd46db9e4fb7da13cf7a6cc69, type: 3}
propertyPath: m_AnchorMin.x
value: 0
value: 0.2
objectReference: {fileID: 0}
- target: {fileID: 3299919758736932218, guid: 54087b4bd46db9e4fb7da13cf7a6cc69, type: 3}
propertyPath: m_SizeDelta.x
@@ -4738,8 +4738,8 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: Colosseum.Game::Colosseum.UI.ConnectionUI
ipAddress: 127.0.0.1
port: 7788
autoStartHostInEditor: 0
port: 7777
autoStartHostInEditor: 1
connectionStatus: Disconnected
--- !u!4 &854739757 stripped
Transform:
@@ -6796,6 +6796,19 @@ Transform:
m_CorrespondingSourceObject: {fileID: 7132605379903659868, guid: 5b4ac53b97612ae4392b84786de0d50d, type: 3}
m_PrefabInstance: {fileID: 1329389111}
m_PrefabAsset: {fileID: 0}
--- !u!134 &1236586838
PhysicsMaterial:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: ' (Instance)'
serializedVersion: 2
m_DynamicFriction: 0.6
m_StaticFriction: 0.6
m_Bounciness: 0
m_FrictionCombine: 0
m_BounceCombine: 0
--- !u!1001 &1236816272
PrefabInstance:
m_ObjectHideFlags: 0
@@ -7252,6 +7265,18 @@ PrefabInstance:
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7035590099291789281, guid: 2122b1e1b36684a40978673f272f200e, type: 3}
propertyPath: m_AnchorMax.x
value: 1
objectReference: {fileID: 0}
- target: {fileID: 7035590099291789281, guid: 2122b1e1b36684a40978673f272f200e, type: 3}
propertyPath: m_AnchorMax.y
value: 1
objectReference: {fileID: 0}
- target: {fileID: 7035590099291789281, guid: 2122b1e1b36684a40978673f272f200e, type: 3}
propertyPath: m_AnchorMin.x
value: 1
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
@@ -11385,19 +11410,6 @@ Transform:
m_CorrespondingSourceObject: {fileID: 1824081129582740912, guid: bf62c127a7934334d9b955503391f574, type: 3}
m_PrefabInstance: {fileID: 649018401}
m_PrefabAsset: {fileID: 0}
--- !u!134 &1976569495
PhysicsMaterial:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
serializedVersion: 2
m_DynamicFriction: 0.6
m_StaticFriction: 0.6
m_Bounciness: 0
m_FrictionCombine: 0
m_BounceCombine: 0
--- !u!1001 &1986213921
PrefabInstance:
m_ObjectHideFlags: 0
@@ -11470,18 +11482,18 @@ PrefabInstance:
- target: {fileID: -4476104823835307179, guid: 56986b707b0dc09439cb35ff2f87dcc9, type: 3}
propertyPath: m_Material
value:
objectReference: {fileID: 1976569495}
objectReference: {fileID: 1236586838}
- target: {fileID: 3122493901255878198, guid: 56986b707b0dc09439cb35ff2f87dcc9, type: 3}
propertyPath: m_LocalPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3122493901255878198, guid: 56986b707b0dc09439cb35ff2f87dcc9, type: 3}
propertyPath: m_LocalPosition.y
value: -0
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3122493901255878198, guid: 56986b707b0dc09439cb35ff2f87dcc9, type: 3}
propertyPath: m_LocalPosition.z
value: 15
value: 8
objectReference: {fileID: 0}
- target: {fileID: 3122493901255878198, guid: 56986b707b0dc09439cb35ff2f87dcc9, type: 3}
propertyPath: m_LocalRotation.w
@@ -11511,13 +11523,9 @@ PrefabInstance:
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5153439431748782209, guid: 56986b707b0dc09439cb35ff2f87dcc9, type: 3}
propertyPath: m_BaseOffset
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5581648761285601425, guid: 56986b707b0dc09439cb35ff2f87dcc9, type: 3}
propertyPath: m_Name
value: TestBoss
value: Drog
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []

View File

@@ -1,4 +1,4 @@
%YAML 1.1
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-6758363554468061369
MonoBehaviour:
@@ -10,7 +10,7 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5b27bb6d9a2c8d540a10dff10acc543e, type: 3}
m_Name: BT_TestBoss Debug Info
m_Name: BT_Drog Debug Info
m_EditorClassIdentifier: Unity.Behavior::Unity.Behavior.BehaviorGraphDebugInfo
m_CodeBreakPointsList: []
--- !u!114 &-6591390698989283165
@@ -23,7 +23,7 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 2dd922ae02c94c87a66e46a10a7319b9, type: 3}
m_Name: BT_TestBoss Blackboard
m_Name: BT_Drog Blackboard
m_EditorClassIdentifier: Unity.Behavior.Authoring::Unity.Behavior.BehaviorBlackboardAuthoringAsset
AssetID:
m_Value0: 11861793134961942170
@@ -112,7 +112,7 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: db920e62f70f420bb33c771449926fa4, type: 3}
m_Name: BT_TestBoss
m_Name: BT_Drog
m_EditorClassIdentifier: Unity.Behavior::Unity.Behavior.BehaviorGraph
Graphs:
- rid: 8805855836547055693
@@ -595,7 +595,7 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bad8f2220607dac4db5082ff333fafb8, type: 3}
m_Name: BT_TestBoss
m_Name: BT_Drog
m_EditorClassIdentifier: Unity.Behavior.Authoring::Unity.Behavior.BehaviorAuthoringGraph
Blackboard: {fileID: -6591390698989283165}
m_Description:
@@ -2417,7 +2417,7 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5c02bb70996b49eba31d0c206e28da24, type: 3}
m_Name: BT_TestBoss Blackboard
m_Name: BT_Drog Blackboard
m_EditorClassIdentifier: Unity.Behavior::Unity.Behavior.RuntimeBlackboardAsset
VersionTimestamp: 639094519736977735
AssetID:
@@ -2483,3 +2483,4 @@ MonoBehaviour:
m_Value1: 14990413519379198129
Name: Die
m_Value: 0

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -56,6 +56,13 @@ ModelImporter:
curves: []
events:
- time: 0.35211262
functionName: OnEffect
data:
objectReferenceParameter: {instanceID: 0}
floatParameter: 0
intParameter: 1
messageOptions: 0
- time: 0.3636364
functionName: OnEffect
data:
objectReferenceParameter: {instanceID: 0}

View File

@@ -79,11 +79,21 @@ AnimatorStateMachine:
- serializedVersion: 1
m_State: {fileID: -1032254959699123210}
m_Position: {x: -450, y: 270, z: 0}
- serializedVersion: 1
m_State: {fileID: 1111345123456789012}
m_Position: {x: -700, y: 140, z: 0}
- serializedVersion: 1
m_State: {fileID: 1111345123456789013}
m_Position: {x: -980, y: 140, z: 0}
- serializedVersion: 1
m_State: {fileID: 1111345123456789014}
m_Position: {x: -1260, y: 140, z: 0}
m_ChildStateMachines: []
m_AnyStateTransitions:
- {fileID: -754003289131015157}
- {fileID: 469741948129995159}
- {fileID: 6228136561094308872}
- {fileID: 1111345123456789021}
m_EntryTransitions: []
m_StateMachineTransitions: {}
m_StateMachineBehaviours: []
@@ -323,6 +333,18 @@ AnimatorController:
m_DefaultInt: 0
m_DefaultBool: 0
m_Controller: {fileID: 9100000}
- m_Name: Down
m_Type: 9
m_DefaultFloat: 0
m_DefaultInt: 0
m_DefaultBool: 0
m_Controller: {fileID: 9100000}
- m_Name: Recover
m_Type: 9
m_DefaultFloat: 0
m_DefaultInt: 0
m_DefaultBool: 0
m_Controller: {fileID: 9100000}
m_AnimatorLayers:
- serializedVersion: 5
m_Name: Base Layer
@@ -437,6 +459,181 @@ AnimatorStateTransition:
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1102 &1111345123456789012
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: DownBegin
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: 1111345123456789022}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 3325809351021969952, guid: 9f1ccf176a8592d4997f8a7c48f9571f, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &1111345123456789013
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: DownLoop
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: 1111345123456789023}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 4349338778344460676, guid: b0e7c1111468a924a81b4323e5aa8eb4, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &1111345123456789014
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Recover
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: 1111345123456789024}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: -3512901111006340046, guid: 3866152d4c1922849be562e6e814dce3, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1101 &1111345123456789021
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
m_Conditions:
- m_ConditionMode: 1
m_ConditionEvent: Down
m_EventTreshold: 0
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1111345123456789012}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.05
m_TransitionOffset: 0
m_ExitTime: 0
m_HasExitTime: 0
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1111345123456789022
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1111345123456789013}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.05
m_TransitionOffset: 0
m_ExitTime: 0.95
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1111345123456789023
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
m_Conditions:
- m_ConditionMode: 1
m_ConditionEvent: Recover
m_EventTreshold: 0
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1111345123456789014}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.05
m_TransitionOffset: 0
m_ExitTime: 0
m_HasExitTime: 0
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1111345123456789024
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: -7908033645098541312}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.1
m_TransitionOffset: 0
m_ExitTime: 0.95
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1102 &7693173606830535998
AnimatorState:
serializedVersion: 6

View File

@@ -0,0 +1,24 @@
%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: b08cc671f858a3b409170a5356e960a0, type: 3}
m_Name: Data_Abnormality_Player_Invincible
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Abnormalities.AbnormalityData
abnormalityName: "\uBB34\uC801"
icon: {fileID: 0}
duration: 1
level: 1
isDebuff: 0
statModifiers: []
periodicInterval: 0
periodicValue: 0
controlType: 4
slowMultiplier: 0.5

View File

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

View File

@@ -1,4 +1,4 @@
%YAML 1.1
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
@@ -10,7 +10,7 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1ecdc2379b078b246a0bd5c0fb58e346, type: 3}
m_Name: Data_Enemy_TestBoss
m_Name: Data_Enemy_Drog
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Enemy.EnemyData
enemyName: Boss The Test
description:
@@ -26,3 +26,4 @@ MonoBehaviour:
aggroRange: 10
attackRange: 2
attackCooldown: 1

View File

@@ -0,0 +1,20 @@
%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: 0ce956e0878565343974c31b8111c0c6, type: 3}
m_Name: "Data_Pattern_Drog_\uB2E4\uC6B4\uCD94\uAC00\uD0C0"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.AI.BossPatternData
patternName: "\uB2E4\uC6B4 \uCD94\uAC00\uD0C0"
steps:
- Type: 0
Skill: {fileID: 11400000, guid: fd7a5d5cc9494229b0c475e7f0cda218, type: 2}
Duration: 0
cooldown: 2.5

View File

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

View File

@@ -1,4 +1,4 @@
%YAML 1.1
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
@@ -10,7 +10,7 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 0ce956e0878565343974c31b8111c0c6, type: 3}
m_Name: "Data_Pattern_TestBoss_\uC57D2\uD0C0-\uC2A4\uC719"
m_Name: "Data_Pattern_Drog_\uC57D2\uD0C0-\uC2A4\uC719"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.AI.BossPatternData
patternName: "\uAE30\uBCF8 \uD328\uD134"
steps:
@@ -24,3 +24,4 @@ MonoBehaviour:
Skill: {fileID: 11400000, guid: 3acbf1c5ec71bef4bb13f8534605d554, type: 2}
Duration: 0.5
cooldown: 5

View File

@@ -1,4 +1,4 @@
%YAML 1.1
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
@@ -10,7 +10,7 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 0ce956e0878565343974c31b8111c0c6, type: 3}
m_Name: "Data_Pattern_TestBoss_\uC810\uD504"
m_Name: "Data_Pattern_Drog_\uC810\uD504"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.AI.BossPatternData
patternName: "\uAE30\uBCF8 \uD328\uD134"
steps:
@@ -24,3 +24,4 @@ MonoBehaviour:
Skill: {fileID: 11400000, guid: 16321efbd1f2498458683bac7605b054, type: 2}
Duration: 0
cooldown: 1

View File

@@ -0,0 +1,27 @@
%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_Drog_\uB2E4\uC6B4\uCD94\uAC00\uD0C0"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.SkillData
skillName: "\uB2E4\uC6B4 \uCD94\uAC00\uD0C0"
description:
icon: {fileID: 0}
skillClip: {fileID: -242498254790479478, guid: 585e8961b6c6e9f4ba96bdb4ffb2cbc3, type: 3}
endClip: {fileID: 3627526391332626453, guid: 39aaec38fc96c4842b972f1e991e5a46, type: 3}
animationSpeed: 1
useRootMotion: 1
ignoreRootMotionY: 1
jumpToTarget: 0
cooldown: 0
manaCost: 0
effects:
- {fileID: 11400000, guid: 0f134a897a7e4d0e98c8d9058b1d79d1, type: 2}

View File

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

View File

@@ -1,4 +1,4 @@
%YAML 1.1
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
@@ -10,7 +10,7 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 94f0a76cebcac2f4fb5daf1b675fd79f, type: 3}
m_Name: "Data_Skill_TestBoss_\uC2A4\uC719"
m_Name: "Data_Skill_Drog_\uC2A4\uC719"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.SkillData
skillName: "\uC2A4\uC719"
description:
@@ -23,3 +23,4 @@ MonoBehaviour:
manaCost: 0
effects:
- {fileID: 11400000, guid: 94b0f3305cea88c458a56783a486340e, type: 2}

View File

@@ -1,4 +1,4 @@
%YAML 1.1
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
@@ -10,7 +10,7 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 94f0a76cebcac2f4fb5daf1b675fd79f, type: 3}
m_Name: "Data_Skill_TestBoss_\uC624\uB978\uC190\uCE58\uAE30"
m_Name: "Data_Skill_Drog_\uC624\uB978\uC190\uCE58\uAE30"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.SkillData
skillName: "\uC624\uB978\uC190\uCE58\uAE30"
description:
@@ -23,3 +23,4 @@ MonoBehaviour:
manaCost: 0
effects:
- {fileID: 11400000, guid: 86bf7e282c1639c4889910475aaccdef, type: 2}

View File

@@ -1,4 +1,4 @@
%YAML 1.1
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
@@ -10,7 +10,7 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 94f0a76cebcac2f4fb5daf1b675fd79f, type: 3}
m_Name: "Data_Skill_TestBoss_\uC624\uB978\uC190\uCE58\uAE302"
m_Name: "Data_Skill_Drog_\uC624\uB978\uC190\uCE58\uAE302"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.SkillData
skillName: "\uC624\uB978\uC190\uCE58\uAE302"
description:
@@ -23,3 +23,5 @@ MonoBehaviour:
manaCost: 0
effects:
- {fileID: 11400000, guid: 87b064a0134987b4b9638e184ab07411, type: 2}
- {fileID: 11400000, guid: 0dba6dca651743bc84b0df42f9dbd290, type: 2}

View File

@@ -1,4 +1,4 @@
%YAML 1.1
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
@@ -10,7 +10,7 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 94f0a76cebcac2f4fb5daf1b675fd79f, type: 3}
m_Name: "Data_Skill_TestBoss_\uC810\uD504"
m_Name: "Data_Skill_Drog_\uC810\uD504"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.SkillData
skillName: "\uC810\uD504"
description:
@@ -25,3 +25,4 @@ MonoBehaviour:
manaCost: 0
effects:
- {fileID: 0}

View File

@@ -1,4 +1,4 @@
%YAML 1.1
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
@@ -10,7 +10,7 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 94f0a76cebcac2f4fb5daf1b675fd79f, type: 3}
m_Name: "Data_Skill_TestBoss_\uC810\uD504\uC900\uBE44"
m_Name: "Data_Skill_Drog_\uC810\uD504\uC900\uBE44"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.SkillData
skillName: "\uC810\uD504"
description:
@@ -25,3 +25,4 @@ MonoBehaviour:
manaCost: 0
effects:
- {fileID: 0}

View File

@@ -1,4 +1,4 @@
%YAML 1.1
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
@@ -10,7 +10,7 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 94f0a76cebcac2f4fb5daf1b675fd79f, type: 3}
m_Name: "Data_Skill_TestBoss_\uC810\uD504\uCC29\uC9C0"
m_Name: "Data_Skill_Drog_\uC810\uD504\uCC29\uC9C0"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.SkillData
skillName: "\uC810\uD504"
description:
@@ -25,3 +25,4 @@ MonoBehaviour:
manaCost: 0
effects:
- {fileID: 11400000, guid: 11bd2d1ebdbfc2f4abf5a9d886615eb3, type: 2}

View File

@@ -19,6 +19,10 @@ MonoBehaviour:
endClip: {fileID: 0}
useRootMotion: 1
ignoreRootMotionY: 1
blockMovementWhileCasting: 1
blockJumpWhileCasting: 1
blockOtherSkillsWhileCasting: 1
cooldown: 10
manaCost: 0
effects: []
effects:
- {fileID: 11400000, guid: 8b2c3d4e5f60718293a4b5c6d7e8f901, type: 2}

View File

@@ -0,0 +1,30 @@
%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: 6fd8c6f4c4454ecdb111d13dc8471024, type: 3}
m_Name: "Data_SkillEffect_Drog_\uB2E4\uC6B4\uCD94\uAC00\uD0C0_0_\uD53C\uACA9\uAC00\uC911\uB370\uBBF8\uC9C0"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.Effects.HitReactionDamageEffect
targetType: 1
targetTeam: 0
areaCenter: 0
areaShape: 0
targetLayers:
serializedVersion: 2
m_Bits: 4294967295
areaRadius: 3.25
fanOriginDistance: 1
fanRadius: 3
fanHalfAngle: 45
baseDamage: 1
damageType: 0
statScaling: 1
bonusAgainstDownedTarget: 1
downedDamageMultiplier: 1.5

View File

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

View File

@@ -1,4 +1,4 @@
%YAML 1.1
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
@@ -10,7 +10,7 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 58efb3c775496fa40b801b21127a011e, type: 3}
m_Name: "Data_SkillEffect_TestBoss_\uC2A4\uC719_0_\uB370\uBBF8\uC9C0"
m_Name: "Data_SkillEffect_Drog_\uC2A4\uC719_0_\uB370\uBBF8\uC9C0"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.Effects.DamageEffect
targetType: 1
targetTeam: 0
@@ -26,3 +26,4 @@ MonoBehaviour:
baseDamage: 1
damageType: 0
statScaling: 1

View File

@@ -1,4 +1,4 @@
%YAML 1.1
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
@@ -10,7 +10,7 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 58efb3c775496fa40b801b21127a011e, type: 3}
m_Name: "Data_SkillEffect_TestBoss_\uC624\uB978\uC190\uCE58\uAE302_0_\uB370\uBBF8\uC9C0"
m_Name: "Data_SkillEffect_Drog_\uC624\uB978\uC190\uCE58\uAE302_0_\uB370\uBBF8\uC9C0"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.Effects.DamageEffect
targetType: 1
targetTeam: 0
@@ -26,3 +26,4 @@ MonoBehaviour:
baseDamage: 1
damageType: 0
statScaling: 1

View File

@@ -0,0 +1,26 @@
%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: 41c96b54a96cdb84c9bda774775b0a1a, type: 3}
m_Name: "Data_SkillEffect_Drog_\uC624\uB978\uC190\uCE58\uAE302_1_\uB2E4\uC6B4"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.Effects.DownEffect
targetType: 1
targetTeam: 0
areaCenter: 0
areaShape: 1
targetLayers:
serializedVersion: 2
m_Bits: 4294967295
areaRadius: 3
fanOriginDistance: 0
fanRadius: 3
fanHalfAngle: 45
duration: 2.5

View File

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

View File

@@ -1,4 +1,4 @@
%YAML 1.1
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
@@ -10,7 +10,7 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 58efb3c775496fa40b801b21127a011e, type: 3}
m_Name: "Data_SkillEffect_TestBoss_\uC624\uB978\uC190\uCE58\uAE30_0_\uB370\uBBF8\uC9C0"
m_Name: "Data_SkillEffect_Drog_\uC624\uB978\uC190\uCE58\uAE30_0_\uB370\uBBF8\uC9C0"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.Effects.DamageEffect
targetType: 1
targetTeam: 0
@@ -26,3 +26,4 @@ MonoBehaviour:
baseDamage: 1
damageType: 0
statScaling: 1

View File

@@ -1,4 +1,4 @@
%YAML 1.1
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
@@ -10,7 +10,7 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 58efb3c775496fa40b801b21127a011e, type: 3}
m_Name: "Data_SkillEffect_TestBoss_\uC810\uD504_0_\uB370\uBBF8\uC9C0"
m_Name: "Data_SkillEffect_Drog_\uC810\uD504_0_\uB370\uBBF8\uC9C0"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.Effects.DamageEffect
targetType: 1
targetTeam: 0
@@ -26,3 +26,4 @@ MonoBehaviour:
baseDamage: 1
damageType: 0
statScaling: 1

View File

@@ -0,0 +1,26 @@
%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: 41c96b54a96cdb84c9bda774775b0a1a, type: 3}
m_Name: "Data_SkillEffect_Player_\uB2E4\uC6B4"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.Effects.DownEffect
targetType: 0
targetTeam: 0
areaCenter: 0
areaShape: 0
targetLayers:
serializedVersion: 2
m_Bits: 0
areaRadius: 3
fanOriginDistance: 1
fanRadius: 3
fanHalfAngle: 45
duration: 1

View File

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

View File

@@ -0,0 +1,26 @@
%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: bf750718c64c4bd48af905d2927351de, type: 3}
m_Name: "Data_SkillEffect_Player_\uBB34\uC801"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.Effects.AbnormalityEffect
targetType: 0
targetTeam: 0
areaCenter: 0
areaShape: 0
targetLayers:
serializedVersion: 2
m_Bits: 0
areaRadius: 3
fanOriginDistance: 1
fanRadius: 3
fanHalfAngle: 45
abnormalityData: {fileID: 11400000, guid: 7a1f2b3c4d5e6f708192a3b4c5d6e7f8, type: 2}

View File

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

View File

@@ -1,4 +1,4 @@
%YAML 1.1
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &1008926773928116
GameObject:
@@ -1364,7 +1364,7 @@ GameObject:
- component: {fileID: 4373196678005266}
- component: {fileID: 95998111322576624}
m_Layer: 0
m_Name: Model_Enemy_TestBoss
m_Name: Model_Enemy_Drog
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
@@ -3285,3 +3285,4 @@ Transform:
m_CorrespondingSourceObject: {fileID: 8583892753143838813, guid: 95219051c0a1142448e9ed4beee02fe1, type: 3}
m_PrefabInstance: {fileID: 8587889261130452289}
m_PrefabAsset: {fileID: 0}

View File

@@ -20,7 +20,6 @@ GameObject:
- component: {fileID: 11400005}
- component: {fileID: 11400006}
- component: {fileID: 3792588902782784034}
- component: {fileID: 9069822911508997612}
m_Layer: 0
m_Name: Prefab_Boss_BossTemplate
m_TagString: Untagged
@@ -291,23 +290,3 @@ MonoBehaviour:
SwitchTransformSpaceWhenParented: 0
Interpolate: 1
SlerpPosition: 0
--- !u!114 &9069822911508997612
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 100000}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e8d0727d5ae3244e3b569694d3912374, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.Components.NetworkAnimator
ShowTopMostFoldoutHeaderGroup: 1
NetworkAnimatorExpanded: 0
AuthorityMode: 0
m_Animator: {fileID: 9500000}
TransitionStateInfoList: []
AnimatorParameterEntries:
ParameterEntries: []
AnimatorParametersExpanded: 0

View File

@@ -1929,11 +1929,12 @@ GameObject:
- component: {fileID: 5774766047967133809}
- component: {fileID: 8818883032728065057}
- component: {fileID: -2857689419101920665}
- component: {fileID: 1459274958342653187}
- component: {fileID: 7544406269366897481}
- component: {fileID: 4137653497738922896}
- component: {fileID: 2670434347080613863}
m_Layer: 6
m_Name: Prefab_Boss_TestBoss
m_Name: Prefab_Boss_Drog
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
@@ -2030,6 +2031,10 @@ MonoBehaviour:
animator: {fileID: 4019041888965840580}
navMeshAgent: {fileID: 5153439431748782209}
enemyData: {fileID: 11400000, guid: f21773b42c60c4b40b609966f09146aa, type: 2}
useThreatSystem: 1
damageThreatMultiplier: 1
threatDecayPerSecond: 0
retargetThreshold: 0
phases: []
initialBehaviorGraph: {fileID: -3933356984444701103, guid: 9ade9280028e9da4aa0151fe8e9ec454, type: 2}
phaseTransitionInvincibilityTime: 2
@@ -2149,6 +2154,8 @@ MonoBehaviour:
debugMode: 1
showAreaDebug: 1
debugDrawDuration: 1
lastCancelledSkillName:
lastCancelReason: 0
--- !u!114 &-2857689419101920665
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -2162,6 +2169,33 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Team
teamType: 2
--- !u!114 &1459274958342653187
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5581648761285601425}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c5b2d4ef2f1b4ee49b5f7f2c7175fd10, type: 3}
m_Name:
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Enemy.DrogPatternController
ShowTopMostFoldoutHeaderGroup: 1
bossEnemy: {fileID: 6949205239376088310}
enemyBase: {fileID: 6949205239376088310}
skillController: {fileID: 8818883032728065057}
navMeshAgent: {fileID: 5153439431748782209}
mainPattern: {fileID: 11400000, guid: 5efd8123be76bf844875d386d9d5f73d, type: 2}
leapPattern: {fileID: 11400000, guid: 88e6cc7cab28baf4c8f8a742247000ec, type: 2}
downPunishPattern: {fileID: 11400000, guid: fe5100f855d14c0faac44b6d4f2c771e, type: 2}
phase2HealthThreshold: 0.75
phase3HealthThreshold: 0.4
targetRefreshInterval: 0.2
leapDistanceThreshold: 8
downPunishSearchRadius: 6
disableBehaviorGraph: 1
debugMode: 1
--- !u!114 &7544406269366897481
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -2177,6 +2211,7 @@ MonoBehaviour:
ShowTopMostFoldoutHeaderGroup: 1
characterStats: {fileID: 0}
networkController: {fileID: 0}
skillController: {fileID: 0}
--- !u!114 &4137653497738922896
MonoBehaviour:
m_ObjectHideFlags: 0

View File

@@ -203,6 +203,7 @@ MonoBehaviour:
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Player.PlayerNetworkController
ShowTopMostFoldoutHeaderGroup: 1
characterStats: {fileID: -5132198055668300151}
abnormalityManager: {fileID: 0}
--- !u!114 &8606252901290138286
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -242,6 +243,14 @@ MonoBehaviour:
NameHash: 20298039
Synchronize: 1
ParameterType: 9
- name: Down
NameHash: -1127399675
Synchronize: 1
ParameterType: 9
- name: Recover
NameHash: 976603345
Synchronize: 1
ParameterType: 9
AnimatorParametersExpanded: 0
--- !u!95 &3426985706796420257
Animator:
@@ -304,6 +313,8 @@ MonoBehaviour:
debugMode: 1
showAreaDebug: 1
debugDrawDuration: 1
lastCancelledSkillName:
lastCancelReason: 0
--- !u!114 &6585367215453362640
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -344,6 +355,7 @@ MonoBehaviour:
ShowTopMostFoldoutHeaderGroup: 1
characterStats: {fileID: -5132198055668300151}
networkController: {fileID: 1664515335065415329}
skillController: {fileID: 0}
--- !u!114 &3552488436187204500
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -453,6 +465,8 @@ MonoBehaviour:
abnormalityManager: {fileID: 0}
actionState: {fileID: 0}
networkController: {fileID: 0}
skillInput: {fileID: 0}
skillController: {fileID: 0}
stunData: {fileID: 0}
silenceData: {fileID: 0}
runOnStartInEditor: 0

View File

@@ -4,6 +4,7 @@ using UnityEngine;
using Action = Unity.Behavior.Action;
using Unity.Properties;
using Colosseum.Combat;
using Colosseum.Enemy;
[Serializable, GeneratePropertyBag]
[NodeDescription(name: "FindTarget", story: "[타겟] ", category: "Action", id: "bb947540549026f3c5625c6d19213311")]
@@ -30,6 +31,17 @@ public partial class FindTargetAction : Action
return Status.Failure;
}
EnemyBase enemy = GameObject.GetComponent<EnemyBase>();
if (enemy != null && enemy.UseThreatSystem)
{
GameObject threatTarget = enemy.GetHighestThreatTarget(Target?.Value, Tag.Value);
if (threatTarget != null)
{
Target.Value = threatTarget;
return Status.Success;
}
}
// 사망하지 않은 타겟 찾기
foreach (GameObject candidate in candidates)
{

View File

@@ -4,6 +4,7 @@ using UnityEngine;
using Action = Unity.Behavior.Action;
using Unity.Properties;
using Colosseum.Combat;
using Colosseum.Enemy;
[Serializable, GeneratePropertyBag]
[NodeDescription(name: "SetTargetInRange", story: "[Range] [Tag] ", category: "Action", id: "93b7a5d823a58618d5371c01ef894948")]
@@ -33,6 +34,17 @@ public partial class SetTargetInRangeAction : Action
return Status.Failure;
}
EnemyBase enemy = GameObject.GetComponent<EnemyBase>();
if (enemy != null && enemy.UseThreatSystem)
{
GameObject threatTarget = enemy.GetHighestThreatTarget(Target?.Value, Tag.Value, Range.Value);
if (threatTarget != null)
{
Target.Value = threatTarget;
return Status.Success;
}
}
// 가장 가까운 살아있는 타겟 찾기
GameObject nearestTarget = null;
float nearestDistance = Range.Value; // Range 내에서만 검색

View File

@@ -13,7 +13,8 @@ namespace Colosseum.Abnormalities
None, // 제어 효과 없음
Stun, // 기절 (이동, 스킬 사용 불가)
Silence, // 침묵 (스킬 사용 불가)
Slow // 둔화 (이동 속도 감소)
Slow, // 둔화 (이동 속도 감소)
Invincible // 무적 (대미지 무시)
}
/// <summary>

View File

@@ -4,6 +4,7 @@ using UnityEngine;
using Unity.Netcode;
using Colosseum.Stats;
using Colosseum.Player;
using Colosseum.Skills;
namespace Colosseum.Abnormalities
{
@@ -20,17 +21,22 @@ namespace Colosseum.Abnormalities
[Tooltip("PlayerNetworkController 컴포넌트 (HP/MP 관리용)")]
[SerializeField] private PlayerNetworkController networkController;
[Tooltip("스킬 실행 관리자 (강제 취소 처리용)")]
[SerializeField] private SkillController skillController;
// 활성화된 이상 상태 목록
private readonly List<ActiveAbnormality> activeAbnormalities = new List<ActiveAbnormality>();
// 제어 효과 상태
private int stunCount;
private int silenceCount;
private int invincibleCount;
private float slowMultiplier = 1f;
// 클라이언트 판정용 제어 효과 동기화 변수
private NetworkVariable<int> syncedStunCount = new NetworkVariable<int>(0);
private NetworkVariable<int> syncedSilenceCount = new NetworkVariable<int>(0);
private NetworkVariable<int> syncedInvincibleCount = new NetworkVariable<int>(0);
private NetworkVariable<float> syncedSlowMultiplier = new NetworkVariable<float>(1f);
// 네트워크 동기화용 데이터
@@ -46,6 +52,11 @@ namespace Colosseum.Abnormalities
/// </summary>
public bool IsSilenced => GetCurrentSilenceCount() > 0;
/// <summary>
/// 무적 상태 여부
/// </summary>
public bool IsInvincible => GetCurrentInvincibleCount() > 0;
/// <summary>
/// 이동 속도 배율 (1.0 = 기본, 0.5 = 50% 감소)
/// </summary>
@@ -101,6 +112,9 @@ namespace Colosseum.Abnormalities
if (networkController == null)
networkController = GetComponent<PlayerNetworkController>();
if (skillController == null)
skillController = GetComponent<SkillController>();
syncedAbnormalities = new NetworkList<AbnormalitySyncData>();
}
@@ -110,6 +124,7 @@ namespace Colosseum.Abnormalities
syncedStunCount.OnValueChanged += HandleSyncedStunChanged;
syncedSilenceCount.OnValueChanged += HandleSyncedSilenceChanged;
syncedInvincibleCount.OnValueChanged += HandleSyncedInvincibleChanged;
syncedSlowMultiplier.OnValueChanged += HandleSyncedSlowChanged;
if (networkController != null)
@@ -128,6 +143,7 @@ namespace Colosseum.Abnormalities
syncedAbnormalities.OnListChanged -= OnSyncedAbnormalitiesChanged;
syncedStunCount.OnValueChanged -= HandleSyncedStunChanged;
syncedSilenceCount.OnValueChanged -= HandleSyncedSilenceChanged;
syncedInvincibleCount.OnValueChanged -= HandleSyncedInvincibleChanged;
syncedSlowMultiplier.OnValueChanged -= HandleSyncedSlowChanged;
if (networkController != null)
@@ -405,9 +421,12 @@ namespace Colosseum.Abnormalities
private void ApplyControlEffect(AbnormalityData data)
{
bool enteredStun = false;
switch (data.controlType)
{
case ControlType.Stun:
enteredStun = stunCount == 0;
stunCount++;
break;
@@ -418,9 +437,18 @@ namespace Colosseum.Abnormalities
case ControlType.Slow:
slowMultiplier = Mathf.Min(slowMultiplier, data.slowMultiplier);
break;
case ControlType.Invincible:
invincibleCount++;
break;
}
SyncControlEffects();
if (enteredStun)
{
TryCancelCurrentSkill(SkillCancelReason.Stun, data.abnormalityName);
}
}
private void RemoveControlEffect(AbnormalityData data)
@@ -438,6 +466,10 @@ namespace Colosseum.Abnormalities
case ControlType.Slow:
RecalculateSlowMultiplier();
break;
case ControlType.Invincible:
invincibleCount = Mathf.Max(0, invincibleCount - 1);
break;
}
SyncControlEffects();
@@ -460,6 +492,8 @@ namespace Colosseum.Abnormalities
private int GetCurrentSilenceCount() => IsServer ? silenceCount : syncedSilenceCount.Value;
private int GetCurrentInvincibleCount() => IsServer ? invincibleCount : syncedInvincibleCount.Value;
private float GetCurrentSlowMultiplier() => IsServer ? slowMultiplier : syncedSlowMultiplier.Value;
private void SyncControlEffects()
@@ -469,6 +503,7 @@ namespace Colosseum.Abnormalities
syncedStunCount.Value = stunCount;
syncedSilenceCount.Value = silenceCount;
syncedInvincibleCount.Value = invincibleCount;
syncedSlowMultiplier.Value = slowMultiplier;
}
@@ -541,6 +576,14 @@ namespace Colosseum.Abnormalities
OnAbnormalitiesChanged?.Invoke();
}
private void HandleSyncedInvincibleChanged(int oldValue, int newValue)
{
if (oldValue == newValue)
return;
OnAbnormalitiesChanged?.Invoke();
}
/// <summary>
/// 사망 시 활성 이상 상태를 모두 제거합니다.
/// </summary>
@@ -556,6 +599,17 @@ namespace Colosseum.Abnormalities
Debug.Log($"[Abnormality] Cleared all abnormalities on death: {gameObject.name}");
}
private void TryCancelCurrentSkill(SkillCancelReason reason, string sourceName)
{
if (!IsServer || skillController == null || !skillController.IsPlayingAnimation)
return;
if (skillController.CancelSkill(reason))
{
Debug.Log($"[Abnormality] Cancelled skill because '{sourceName}' applied to {gameObject.name}");
}
}
private AbnormalityData FindAbnormalityDataById(int instanceId)
{
var allData = Resources.FindObjectsOfTypeAll<AbnormalityData>();

View File

@@ -14,6 +14,7 @@ namespace Colosseum.Editor
{
private BossEnemy boss;
private bool showPhaseDetails = true;
private bool showThreatInfo = true;
private bool showDebugTools = true;
private int selectedPhaseIndex = 0;
@@ -45,6 +46,11 @@ namespace Colosseum.Editor
EditorGUILayout.Space(10);
// 위협 정보
DrawThreatInfo();
EditorGUILayout.Space(10);
// 디버그 도구
DrawDebugTools();
}
@@ -136,6 +142,36 @@ namespace Colosseum.Editor
EditorGUI.indentLevel--;
}
/// <summary>
/// 위협 정보 표시
/// </summary>
private void DrawThreatInfo()
{
showThreatInfo = EditorGUILayout.Foldout(showThreatInfo, "위협 정보", true);
if (!showThreatInfo)
return;
EditorGUI.indentLevel++;
if (!boss.UseThreatSystem)
{
EditorGUILayout.HelpBox("위협 시스템이 비활성화되어 있습니다.", MessageType.Info);
EditorGUI.indentLevel--;
return;
}
EditorGUILayout.LabelField("위협 테이블", EditorStyles.boldLabel);
EditorGUILayout.TextArea(boss.GetThreatDebugSummary(), GUILayout.MinHeight(70f));
if (GUILayout.Button("위협 초기화"))
{
boss.ClearAllThreat();
}
EditorGUI.indentLevel--;
}
/// <summary>
/// 디버그 도구 표시
/// </summary>

View File

@@ -33,6 +33,7 @@ namespace Colosseum.Enemy
// 컴포넌트
private BehaviorGraphAgent behaviorAgent;
private DrogPatternController drogPatternController;
// 페이즈 상태
private int currentPhaseIndex = 0;
@@ -77,8 +78,15 @@ namespace Colosseum.Enemy
behaviorAgent = gameObject.AddComponent<BehaviorGraphAgent>();
}
drogPatternController = GetComponent<DrogPatternController>();
// 초기 AI 설정
if (IsServer && initialBehaviorGraph != null)
if (IsServer && drogPatternController != null && drogPatternController.DisableBehaviorGraph)
{
behaviorAgent.enabled = false;
behaviorAgent.Graph = null;
}
else if (IsServer && initialBehaviorGraph != null)
{
behaviorAgent.Graph = initialBehaviorGraph;
}

View File

@@ -0,0 +1,408 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Netcode;
using Colosseum.AI;
using Colosseum.Player;
using Colosseum.Skills;
namespace Colosseum.Enemy
{
/// <summary>
/// 드로그 전용 패턴 선택 컨트롤러입니다.
/// 기본 루프, 도약, 다운 추가타 같은 고우선 패턴을 직접 선택합니다.
/// </summary>
[DisallowMultipleComponent]
[RequireComponent(typeof(BossEnemy))]
[RequireComponent(typeof(SkillController))]
public class DrogPatternController : NetworkBehaviour
{
[Header("References")]
[SerializeField] private BossEnemy bossEnemy;
[SerializeField] private EnemyBase enemyBase;
[SerializeField] private SkillController skillController;
[SerializeField] private UnityEngine.AI.NavMeshAgent navMeshAgent;
[Header("Pattern Data")]
[Tooltip("기본 근접 압박 패턴")]
[SerializeField] private BossPatternData mainPattern;
[Tooltip("먼 대상 징벌용 도약 패턴")]
[SerializeField] private BossPatternData leapPattern;
[Tooltip("다운 대상이 있을 때 우선 발동하는 광역 추가타 패턴")]
[SerializeField] private BossPatternData downPunishPattern;
[Header("Phase Thresholds")]
[Tooltip("2페이즈 진입 체력 비율")]
[Range(0f, 1f)] [SerializeField] private float phase2HealthThreshold = 0.75f;
[Tooltip("3페이즈 진입 체력 비율")]
[Range(0f, 1f)] [SerializeField] private float phase3HealthThreshold = 0.4f;
[Header("Targeting")]
[Tooltip("타겟 재탐색 주기")]
[Min(0.05f)] [SerializeField] private float targetRefreshInterval = 0.2f;
[Tooltip("도약 패턴을 고려하기 시작하는 거리")]
[Min(0f)] [SerializeField] private float leapDistanceThreshold = 8f;
[Tooltip("다운 추가타를 고려할 최대 반경")]
[Min(0f)] [SerializeField] private float downPunishSearchRadius = 6f;
[Header("Behavior")]
[Tooltip("드로그 전용 컨트롤러 사용 시 기존 BehaviorGraph를 비활성화할지 여부")]
[SerializeField] private bool disableBehaviorGraph = true;
[Tooltip("디버그 로그 출력 여부")]
[SerializeField] private bool debugMode = false;
private readonly Dictionary<BossPatternData, float> patternCooldownTracker = new Dictionary<BossPatternData, float>();
private Coroutine activePatternCoroutine;
private GameObject currentTarget;
private float nextTargetRefreshTime;
/// <summary>
/// 드로그 컨트롤러 사용 시 BehaviorGraph를 비활성화할지 여부
/// </summary>
public bool DisableBehaviorGraph => disableBehaviorGraph;
/// <summary>
/// 현재 전용 패턴 실행 중인지 여부
/// </summary>
public bool IsExecutingPattern => activePatternCoroutine != null;
/// <summary>
/// 현재 드로그 패턴 페이즈
/// </summary>
public int CurrentPatternPhase
{
get
{
float healthRatio = bossEnemy != null && bossEnemy.MaxHealth > 0f
? bossEnemy.CurrentHealth / bossEnemy.MaxHealth
: 1f;
if (healthRatio <= phase3HealthThreshold)
return 3;
if (healthRatio <= phase2HealthThreshold)
return 2;
return 1;
}
}
private void Awake()
{
ResolveReferences();
}
public override void OnNetworkSpawn()
{
ResolveReferences();
if (!IsServer)
{
enabled = false;
}
}
private void Update()
{
if (!IsServer)
return;
ResolveReferences();
if (bossEnemy == null || enemyBase == null || skillController == null)
return;
if (bossEnemy.IsDead || bossEnemy.IsTransitioning)
return;
RefreshTargetIfNeeded();
UpdateMovement();
if (activePatternCoroutine != null || skillController.IsPlayingAnimation)
return;
if (TryStartDownPunishPattern())
return;
TryStartMainPattern();
}
private bool TryStartDownPunishPattern()
{
if (!IsPatternReady(downPunishPattern))
return false;
GameObject downedTarget = FindNearestDownedTarget();
if (downedTarget == null)
return false;
StartPattern(downPunishPattern, downedTarget);
return true;
}
private bool TryStartMainPattern()
{
if (currentTarget == null)
return false;
float distanceToTarget = Vector3.Distance(transform.position, currentTarget.transform.position);
if (distanceToTarget >= leapDistanceThreshold && IsPatternReady(leapPattern))
{
StartPattern(leapPattern, currentTarget);
return true;
}
float attackRange = enemyBase.Data != null ? enemyBase.Data.AttackRange : 2f;
if (distanceToTarget <= attackRange + 0.25f && IsPatternReady(mainPattern))
{
StartPattern(mainPattern, currentTarget);
return true;
}
return false;
}
private void StartPattern(BossPatternData pattern, GameObject target)
{
if (pattern == null || activePatternCoroutine != null)
return;
if (debugMode)
{
string targetName = target != null ? target.name : "None";
Debug.Log($"[DrogPattern] 패턴 시작: {pattern.PatternName} / Target={targetName} / Phase={CurrentPatternPhase}");
}
activePatternCoroutine = StartCoroutine(RunPatternCoroutine(pattern, target));
}
private IEnumerator RunPatternCoroutine(BossPatternData pattern, GameObject target)
{
StopMovement();
bool completed = true;
BossPatternData chainedPattern = null;
GameObject chainedTarget = null;
for (int i = 0; i < pattern.Steps.Count; i++)
{
PatternStep step = pattern.Steps[i];
if (step.Type == PatternStepType.Wait)
{
yield return new WaitForSeconds(step.Duration);
continue;
}
if (step.Skill == null)
{
completed = false;
Debug.LogWarning($"[DrogPattern] 패턴 스텝 스킬이 비어 있습니다. Pattern={pattern.PatternName}, Index={i}");
break;
}
if (step.Skill.JumpToTarget && target != null)
{
enemyBase?.SetJumpTarget(target.transform.position);
}
if (!skillController.ExecuteSkill(step.Skill))
{
completed = false;
if (debugMode)
{
Debug.LogWarning($"[DrogPattern] 스킬 실행 실패: {step.Skill.SkillName}");
}
break;
}
yield return new WaitUntil(() => skillController == null || !skillController.IsPlayingAnimation || bossEnemy == null || bossEnemy.IsDead);
if (bossEnemy == null || bossEnemy.IsDead)
{
completed = false;
break;
}
if (pattern != downPunishPattern && TryPrepareDownPunishChain(out chainedTarget))
{
chainedPattern = downPunishPattern;
break;
}
}
if (completed)
{
patternCooldownTracker[pattern] = Time.time + pattern.Cooldown;
}
activePatternCoroutine = null;
if (chainedPattern != null && chainedTarget != null && bossEnemy != null && !bossEnemy.IsDead)
{
StartPattern(chainedPattern, chainedTarget);
}
}
private bool IsPatternReady(BossPatternData pattern)
{
if (pattern == null || pattern.Steps == null || pattern.Steps.Count == 0)
return false;
if (!patternCooldownTracker.TryGetValue(pattern, out float readyTime))
return true;
return Time.time >= readyTime;
}
private void RefreshTargetIfNeeded()
{
if (Time.time < nextTargetRefreshTime)
return;
nextTargetRefreshTime = Time.time + targetRefreshInterval;
GameObject highestThreatTarget = enemyBase.GetHighestThreatTarget(currentTarget, null, enemyBase.Data != null ? enemyBase.Data.AggroRange : Mathf.Infinity);
currentTarget = highestThreatTarget != null ? highestThreatTarget : FindNearestLivingPlayer();
}
private void UpdateMovement()
{
if (navMeshAgent == null || !navMeshAgent.enabled)
return;
if (activePatternCoroutine != null || (skillController != null && skillController.IsPlayingAnimation))
{
StopMovement();
return;
}
if (currentTarget == null)
{
StopMovement();
return;
}
float attackRange = enemyBase.Data != null ? enemyBase.Data.AttackRange : 2f;
float distanceToTarget = Vector3.Distance(transform.position, currentTarget.transform.position);
if (distanceToTarget <= attackRange)
{
StopMovement();
return;
}
navMeshAgent.isStopped = false;
navMeshAgent.SetDestination(currentTarget.transform.position);
}
private void StopMovement()
{
if (navMeshAgent == null || !navMeshAgent.enabled)
return;
navMeshAgent.isStopped = true;
navMeshAgent.ResetPath();
}
private GameObject FindNearestDownedTarget()
{
HitReactionController[] hitReactionControllers = FindObjectsByType<HitReactionController>(FindObjectsSortMode.None);
GameObject bestTarget = null;
float bestDistance = float.MaxValue;
for (int i = 0; i < hitReactionControllers.Length; i++)
{
HitReactionController controller = hitReactionControllers[i];
if (controller == null || !controller.IsDowned)
continue;
GameObject candidate = controller.gameObject;
if (candidate == null || !candidate.activeInHierarchy || Team.IsSameTeam(gameObject, candidate))
continue;
float distance = Vector3.Distance(transform.position, candidate.transform.position);
if (distance > downPunishSearchRadius || distance >= bestDistance)
continue;
bestDistance = distance;
bestTarget = candidate;
}
return bestTarget;
}
private bool TryPrepareDownPunishChain(out GameObject downedTarget)
{
downedTarget = null;
if (!IsPatternReady(downPunishPattern))
return false;
downedTarget = FindNearestDownedTarget();
if (downedTarget == null)
return false;
if (debugMode)
{
Debug.Log($"[DrogPattern] 다운 대상 감지, 다운 추가타 연계 준비: {downedTarget.name}");
}
return true;
}
private GameObject FindNearestLivingPlayer()
{
PlayerNetworkController[] players = FindObjectsByType<PlayerNetworkController>(FindObjectsSortMode.None);
GameObject bestTarget = null;
float bestDistance = float.MaxValue;
float aggroRange = enemyBase.Data != null ? enemyBase.Data.AggroRange : Mathf.Infinity;
for (int i = 0; i < players.Length; i++)
{
PlayerNetworkController player = players[i];
if (player == null || player.IsDead || !player.gameObject.activeInHierarchy)
continue;
GameObject candidate = player.gameObject;
if (Team.IsSameTeam(gameObject, candidate))
continue;
float distance = Vector3.Distance(transform.position, candidate.transform.position);
if (distance > aggroRange || distance >= bestDistance)
continue;
bestDistance = distance;
bestTarget = candidate;
}
return bestTarget;
}
private void ResolveReferences()
{
if (bossEnemy == null)
bossEnemy = GetComponent<BossEnemy>();
if (enemyBase == null)
enemyBase = GetComponent<EnemyBase>();
if (skillController == null)
skillController = GetComponent<SkillController>();
if (navMeshAgent == null)
navMeshAgent = GetComponent<UnityEngine.AI.NavMeshAgent>();
}
}
}

View File

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

View File

@@ -1,8 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using Unity.Netcode;
using Colosseum.Stats;
using Colosseum.Combat;
using Colosseum.Skills;
namespace Colosseum.Enemy
{
@@ -23,6 +28,15 @@ namespace Colosseum.Enemy
[Header("Data")]
[SerializeField] protected EnemyData enemyData;
[Header("Threat Settings")]
[Tooltip("피격 시 공격자 기준 위협 수치를 누적할지 여부")]
[SerializeField] private bool useThreatSystem = true;
[Tooltip("실제 적용된 피해량에 곱해지는 위협 배율")]
[Min(0f)] [SerializeField] private float damageThreatMultiplier = 1f;
[Tooltip("초당 감소하는 위협 수치")]
[Min(0f)] [SerializeField] private float threatDecayPerSecond = 0f;
[Tooltip("현재 타겟보다 이 값 이상 높을 때만 새 타겟으로 전환합니다.")]
[Min(0f)] [SerializeField] private float retargetThreshold = 0f;
// 네트워크 동기화 변수
@@ -32,6 +46,8 @@ namespace Colosseum.Enemy
// 플레이어 분리용 (레이어 의존 없이 CharacterController로 식별)
private readonly Collider[] overlapBuffer = new Collider[8];
private readonly Dictionary<GameObject, float> threatTable = new Dictionary<GameObject, float>();
private readonly List<GameObject> threatTargetBuffer = new List<GameObject>();
// 점프 등 Y 루트모션 스킬 중 NavMeshAgent 비활성화 상태 추적
private bool isAirborne = false;
@@ -55,6 +71,7 @@ namespace Colosseum.Enemy
public CharacterStats Stats => characterStats;
public EnemyData Data => enemyData;
public Animator Animator => animator;
public bool UseThreatSystem => useThreatSystem;
public override void OnNetworkSpawn()
{
@@ -80,6 +97,7 @@ namespace Colosseum.Enemy
{
if (!IsServer || IsDead) return;
UpdateThreatState(Time.deltaTime);
OnServerUpdate();
}
@@ -250,6 +268,7 @@ namespace Colosseum.Enemy
float actualDamage = Mathf.Min(damage, currentHealth.Value);
currentHealth.Value = Mathf.Max(0f, currentHealth.Value - actualDamage);
RegisterThreatFromDamage(actualDamage, source);
OnDamageTaken?.Invoke(actualDamage);
// 대미지 피드백 (애니메이션, 이펙트 등)
@@ -323,12 +342,13 @@ namespace Colosseum.Enemy
protected virtual void HandleDeath()
{
isDead.Value = true;
ClearAllThreat();
// 실행 중인 스킬 즉시 취소
var skillController = GetComponent<Colosseum.Skills.SkillController>();
if (skillController != null)
{
skillController.CancelSkill();
skillController.CancelSkill(SkillCancelReason.Death);
}
// 모든 클라이언트에서 사망 애니메이션 재생
@@ -352,6 +372,7 @@ namespace Colosseum.Enemy
if (!IsServer) return;
isDead.Value = false;
ClearAllThreat();
InitializeStats();
if (navMeshAgent != null)
@@ -363,6 +384,12 @@ namespace Colosseum.Enemy
{
animator.Rebind();
}
var skillController = GetComponent<SkillController>();
if (skillController != null)
{
skillController.CancelSkill(SkillCancelReason.Respawn);
}
}
// 체력 변화 이벤트 전파
@@ -370,5 +397,280 @@ namespace Colosseum.Enemy
{
OnHealthChanged?.Invoke(newValue, MaxHealth);
}
/// <summary>
/// 공격자 기준 위협 수치를 누적합니다.
/// </summary>
public virtual void AddThreat(GameObject source, float amount)
{
if (!IsServer || !useThreatSystem || amount <= 0f)
return;
if (!IsValidThreatTarget(source))
return;
threatTable.TryGetValue(source, out float currentThreat);
threatTable[source] = currentThreat + amount;
}
/// <summary>
/// 특정 대상의 위협 수치를 강제로 설정합니다.
/// </summary>
public virtual void SetThreat(GameObject source, float amount)
{
if (!IsServer || !useThreatSystem)
return;
if (!IsValidThreatTarget(source) || amount <= 0f)
{
ClearThreat(source);
return;
}
threatTable[source] = amount;
}
/// <summary>
/// 특정 대상의 위협 수치를 제거합니다.
/// </summary>
public virtual void ClearThreat(GameObject source)
{
if (source == null)
return;
threatTable.Remove(source);
}
/// <summary>
/// 모든 위협 수치를 초기화합니다.
/// </summary>
public virtual void ClearAllThreat()
{
threatTable.Clear();
}
/// <summary>
/// 가장 높은 위협 수치를 가진 타겟을 반환합니다.
/// </summary>
public virtual GameObject GetHighestThreatTarget(GameObject currentTarget = null, string requiredTag = null, float maxDistance = Mathf.Infinity)
{
if (!useThreatSystem)
return null;
CleanupThreatTable();
GameObject highestThreatTarget = null;
float highestThreat = float.MinValue;
float currentThreat = -1f;
if (currentTarget != null
&& threatTable.TryGetValue(currentTarget, out float cachedCurrentThreat)
&& IsSelectableThreatTarget(currentTarget, requiredTag, maxDistance))
{
currentThreat = cachedCurrentThreat;
}
foreach (var pair in threatTable)
{
if (!IsSelectableThreatTarget(pair.Key, requiredTag, maxDistance))
continue;
if (highestThreatTarget == null || pair.Value > highestThreat)
{
highestThreatTarget = pair.Key;
highestThreat = pair.Value;
}
}
if (highestThreatTarget == null)
return null;
if (currentThreat >= 0f
&& currentTarget != null
&& currentTarget != highestThreatTarget
&& highestThreat < currentThreat + retargetThreshold)
{
return currentTarget;
}
return highestThreatTarget;
}
/// <summary>
/// 특정 대상의 현재 위협 수치를 반환합니다.
/// </summary>
public float GetThreat(GameObject source)
{
if (source == null)
return 0f;
return threatTable.TryGetValue(source, out float threat) ? threat : 0f;
}
/// <summary>
/// 런타임 디버그용 위협 요약 문자열을 반환합니다.
/// </summary>
public string GetThreatDebugSummary()
{
if (!useThreatSystem)
return "위협 시스템이 비활성화되어 있습니다.";
CleanupThreatTable();
if (threatTable.Count == 0)
return "등록된 위협 대상이 없습니다.";
threatTargetBuffer.Clear();
foreach (var pair in threatTable)
{
threatTargetBuffer.Add(pair.Key);
}
threatTargetBuffer.Sort((a, b) => GetThreat(b).CompareTo(GetThreat(a)));
StringBuilder builder = new StringBuilder();
for (int i = 0; i < threatTargetBuffer.Count; i++)
{
GameObject target = threatTargetBuffer[i];
if (target == null)
continue;
if (builder.Length > 0)
{
builder.AppendLine();
}
builder.Append(i + 1);
builder.Append(". ");
builder.Append(target.name);
builder.Append(" : ");
builder.Append(GetThreat(target).ToString("F1"));
}
return builder.Length > 0 ? builder.ToString() : "등록된 위협 대상이 없습니다.";
}
/// <summary>
/// 피격 정보를 위협 수치로 변환합니다.
/// </summary>
protected virtual void RegisterThreatFromDamage(float damage, object source)
{
if (!useThreatSystem || damage <= 0f)
return;
GameObject sourceObject = ResolveThreatSource(source);
if (sourceObject == null)
return;
AddThreat(sourceObject, damage * damageThreatMultiplier);
}
/// <summary>
/// 위협 테이블의 무효 대상을 정리하고 자연 감소를 적용합니다.
/// </summary>
private void UpdateThreatState(float deltaTime)
{
if (!useThreatSystem || threatTable.Count == 0)
return;
CleanupThreatTable();
if (threatDecayPerSecond <= 0f || threatTable.Count == 0)
return;
threatTargetBuffer.Clear();
foreach (var pair in threatTable)
{
threatTargetBuffer.Add(pair.Key);
}
for (int i = 0; i < threatTargetBuffer.Count; i++)
{
GameObject target = threatTargetBuffer[i];
if (target == null || !threatTable.TryGetValue(target, out float currentThreat))
continue;
float nextThreat = Mathf.Max(0f, currentThreat - (threatDecayPerSecond * deltaTime));
if (nextThreat <= 0f)
{
threatTable.Remove(target);
continue;
}
threatTable[target] = nextThreat;
}
}
/// <summary>
/// 위협 대상이 유효한 선택 후보인지 확인합니다.
/// </summary>
private bool IsSelectableThreatTarget(GameObject target, string requiredTag, float maxDistance)
{
if (!IsValidThreatTarget(target))
return false;
if (!string.IsNullOrEmpty(requiredTag) && !target.CompareTag(requiredTag))
return false;
if (!float.IsInfinity(maxDistance))
{
float distance = Vector3.Distance(transform.position, target.transform.position);
if (distance > maxDistance)
return false;
}
return true;
}
/// <summary>
/// 위협 누적 대상이 유효한지 확인합니다.
/// </summary>
private bool IsValidThreatTarget(GameObject target)
{
if (target == null || !target.activeInHierarchy)
return false;
if (Colosseum.Team.IsSameTeam(gameObject, target))
return false;
IDamageable damageable = target.GetComponent<IDamageable>();
return damageable != null && !damageable.IsDead;
}
/// <summary>
/// 위협 테이블에서 무효한 대상을 제거합니다.
/// </summary>
private void CleanupThreatTable()
{
if (threatTable.Count == 0)
return;
threatTargetBuffer.Clear();
foreach (var pair in threatTable)
{
if (!IsValidThreatTarget(pair.Key))
{
threatTargetBuffer.Add(pair.Key);
}
}
for (int i = 0; i < threatTargetBuffer.Count; i++)
{
threatTable.Remove(threatTargetBuffer[i]);
}
}
/// <summary>
/// 다양한 source 타입에서 실제 GameObject를 추출합니다.
/// </summary>
private static GameObject ResolveThreatSource(object source)
{
return source switch
{
GameObject sourceObject => sourceObject,
Component component => component.gameObject,
_ => null,
};
}
}
}

View File

@@ -0,0 +1,4 @@
namespace Colosseum.Network
{
// 임시 삭제 후 Unity 컴파일 캐시 정리를 위한 빈 파일
}

View File

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

View File

@@ -0,0 +1,211 @@
using UnityEngine;
using Unity.Netcode;
using Colosseum.Skills;
namespace Colosseum.Player
{
/// <summary>
/// 플레이어의 피격 제어 상태를 관리합니다.
/// 넉백 강제 이동과 다운 상태, 피격 애니메이션 재생을 담당합니다.
/// </summary>
[DisallowMultipleComponent]
[RequireComponent(typeof(PlayerMovement))]
public class HitReactionController : NetworkBehaviour
{
[Header("References")]
[Tooltip("플레이어 이동 컴포넌트")]
[SerializeField] private PlayerMovement playerMovement;
[Tooltip("스킬 실행 관리자")]
[SerializeField] private SkillController skillController;
[Tooltip("플레이어 네트워크 상태")]
[SerializeField] private PlayerNetworkController networkController;
[Tooltip("피격 애니메이션을 재생할 Animator")]
[SerializeField] private Animator animator;
[Header("Animation")]
[Tooltip("일반 피격 트리거 이름")]
[SerializeField] private string hitTriggerParam = "Hit";
[Tooltip("다운 시작 트리거 이름")]
[SerializeField] private string downTriggerParam = "Down";
[Tooltip("기상 트리거 이름")]
[SerializeField] private string recoverTriggerParam = "Recover";
[Header("Settings")]
[Tooltip("애니메이션 파라미터가 없을 때 경고 로그 출력")]
[SerializeField] private bool logMissingAnimationParams = false;
private readonly NetworkVariable<bool> isDowned = new NetworkVariable<bool>(false);
private float downRemainingTime;
/// <summary>
/// 다운 상태 여부
/// </summary>
public bool IsDowned => isDowned.Value;
/// <summary>
/// 넉백 강제 이동 진행 여부
/// </summary>
public bool IsKnockbackActive => playerMovement != null && playerMovement.IsForcedMoving;
private void Awake()
{
ResolveReferences();
}
public override void OnNetworkSpawn()
{
ResolveReferences();
}
private void Update()
{
if (!IsServer || !isDowned.Value)
return;
downRemainingTime -= Time.deltaTime;
if (downRemainingTime <= 0f)
{
RecoverFromDown();
}
}
/// <summary>
/// 넉백을 적용합니다.
/// </summary>
public void ApplyKnockback(Vector3 worldVelocity, float duration, bool playHitAnimation = true)
{
if (!IsServer)
return;
ResolveReferences();
if (networkController != null && networkController.IsDead)
return;
playerMovement?.ApplyForcedMovement(worldVelocity, duration);
if (playHitAnimation)
{
TriggerAnimationRpc(hitTriggerParam);
}
}
/// <summary>
/// 다운 상태를 적용합니다.
/// </summary>
public void ApplyDown(float duration)
{
if (!IsServer)
return;
ResolveReferences();
if (networkController != null && networkController.IsDead)
return;
downRemainingTime = Mathf.Max(downRemainingTime, duration);
if (isDowned.Value)
return;
isDowned.Value = true;
skillController?.CancelSkill(SkillCancelReason.HitReaction);
playerMovement?.ClearForcedMovement();
TriggerAnimationRpc(downTriggerParam);
}
/// <summary>
/// 다운 상태를 해제합니다.
/// </summary>
public void RecoverFromDown()
{
if (!IsServer || !isDowned.Value)
return;
isDowned.Value = false;
downRemainingTime = 0f;
TriggerAnimationRpc(recoverTriggerParam);
}
/// <summary>
/// 피격 상태를 즉시 초기화합니다.
/// </summary>
public void ClearHitReactionState(bool playRecoverAnimation = false)
{
if (!IsServer)
return;
ResolveReferences();
playerMovement?.ClearForcedMovement();
if (!isDowned.Value)
return;
isDowned.Value = false;
downRemainingTime = 0f;
if (playRecoverAnimation)
{
TriggerAnimationRpc(recoverTriggerParam);
}
}
[Rpc(SendTo.Everyone)]
private void TriggerAnimationRpc(string triggerName)
{
ResolveReferences();
if (animator == null || string.IsNullOrWhiteSpace(triggerName))
return;
if (!HasTrigger(triggerName))
{
if (logMissingAnimationParams)
{
Debug.LogWarning($"[HitReaction] Animator trigger not found: {triggerName}");
}
return;
}
animator.SetTrigger(triggerName);
}
private bool HasTrigger(string triggerName)
{
if (animator == null || string.IsNullOrWhiteSpace(triggerName))
return false;
for (int i = 0; i < animator.parameterCount; i++)
{
AnimatorControllerParameter parameter = animator.GetParameter(i);
if (parameter.type == AnimatorControllerParameterType.Trigger && parameter.name == triggerName)
return true;
}
return false;
}
private void ResolveReferences()
{
if (playerMovement == null)
playerMovement = GetComponent<PlayerMovement>();
if (skillController == null)
skillController = GetComponent<SkillController>();
if (networkController == null)
networkController = GetComponent<PlayerNetworkController>();
if (animator == null)
animator = GetComponentInChildren<Animator>();
}
}
}

View File

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

View File

@@ -91,6 +91,10 @@ namespace Colosseum.Player
GUILayout.BeginVertical();
GUILayout.Label($"사망 상태: {(networkController != null && networkController.IsDead ? "Dead" : "Alive")}");
if (TryGetComponent<PlayerActionState>(out var actionState))
{
GUILayout.Label($"무적:{(actionState.IsDamageImmune ? "On" : "Off")} / 다운:{(actionState.IsDowned ? "On" : "Off")} / 이동:{actionState.CanMove} / 스킬:{actionState.CanUseSkills}");
}
GUILayout.Label("입력 예시: 기절 / Data_Abnormality_Player_Stun / 0");
GUI.SetNextControlName("AbnormalityInputField");

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections;
using UnityEngine;
@@ -5,6 +6,7 @@ using UnityEngine;
using Unity.Netcode;
using Colosseum.Abnormalities;
using Colosseum.Skills;
#if UNITY_EDITOR
using UnityEditor;
@@ -23,6 +25,10 @@ namespace Colosseum.Player
[SerializeField] private AbnormalityManager abnormalityManager;
[SerializeField] private PlayerActionState actionState;
[SerializeField] private PlayerNetworkController networkController;
[SerializeField] private PlayerSkillInput skillInput;
[SerializeField] private SkillController skillController;
[SerializeField] private PlayerMovement playerMovement;
[SerializeField] private HitReactionController hitReactionController;
[Header("Test Data")]
[SerializeField] private AbnormalityData stunData;
@@ -80,7 +86,7 @@ namespace Colosseum.Player
ResolveReferences();
LoadDefaultAssetsIfNeeded();
if (abnormalityManager == null || actionState == null || networkController == null || stunData == null || silenceData == null)
if (abnormalityManager == null || actionState == null || networkController == null || playerMovement == null || hitReactionController == null || stunData == null || silenceData == null)
{
Debug.LogWarning("[AbnormalityVerification] Missing references or test data.");
yield break;
@@ -100,6 +106,16 @@ namespace Colosseum.Player
Verify("초기 상태: 사망 아님", !networkController.IsDead);
Verify("초기 상태: 이동 가능", actionState.CanMove);
Verify("초기 상태: 스킬 사용 가능", actionState.CanUseSkills);
Verify("초기 상태: 무적 상태 아님", !actionState.IsDamageImmune);
Verify("초기 상태: 마지막 취소 이유 없음", skillController == null || skillController.LastCancelReason == SkillCancelReason.None);
yield return RunInvincibilitySkillVerification();
yield return RunStunCancellationVerification();
yield return RunDownVerification();
yield return RunKnockbackVerification();
abnormalityManager.ApplyAbnormality(stunData, gameObject);
yield return new WaitForSeconds(settleDelay);
@@ -176,6 +192,14 @@ namespace Colosseum.Player
actionState = GetComponent<PlayerActionState>();
if (networkController == null)
networkController = GetComponent<PlayerNetworkController>();
if (skillInput == null)
skillInput = GetComponent<PlayerSkillInput>();
if (skillController == null)
skillController = GetComponent<SkillController>();
if (playerMovement == null)
playerMovement = GetComponent<PlayerMovement>();
if (hitReactionController == null)
hitReactionController = GetComponent<HitReactionController>();
}
private void LoadDefaultAssetsIfNeeded()
@@ -222,5 +246,186 @@ namespace Colosseum.Player
return Debug.isDebugBuild;
#endif
}
private IEnumerator RunInvincibilitySkillVerification()
{
SkillData invincibilitySkill = skillInput != null ? skillInput.GetSkill(6) : null;
if (invincibilitySkill == null)
{
AppendLine("[SKIP] 무적 스킬 검증: 7번 슬롯 스킬이 없습니다.");
yield break;
}
if (skillController == null || !skillController.ExecuteSkill(invincibilitySkill))
{
Verify("무적 스킬 검증: 스킬 실행 성공", false);
yield break;
}
yield return WaitForConditionOrTimeout(() => actionState.IsDamageImmune, GetSkillDuration(invincibilitySkill) + 0.5f);
float healthBeforeDamage = networkController.Health;
Verify("무적 적용: IsDamageImmune", actionState.IsDamageImmune);
networkController.TakeDamageRpc(15f);
yield return new WaitForSeconds(settleDelay);
Verify("무적 적용: 대미지 무시", Mathf.Approximately(networkController.Health, healthBeforeDamage));
if (silenceData != null)
{
abnormalityManager.ApplyAbnormality(silenceData, gameObject);
yield return WaitForConditionOrTimeout(() => actionState.IsSilenced, settleDelay + 0.5f);
Verify("무적 중 침묵 적용: IsSilenced", actionState.IsSilenced);
Verify("무적 중 침묵 적용: 무적 상태 유지", actionState.IsDamageImmune);
Verify("무적 중 침묵 적용: 스킬 신규 사용 불가", !actionState.CanStartSkill(invincibilitySkill));
abnormalityManager.RemoveAbnormality(silenceData);
yield return new WaitForSeconds(settleDelay);
}
yield return WaitForConditionOrTimeout(() => !actionState.IsDamageImmune, GetSkillDuration(invincibilitySkill) + 1.5f);
Verify("무적 해제: IsDamageImmune false", !actionState.IsDamageImmune);
}
private IEnumerator RunStunCancellationVerification()
{
SkillData cancellableSkill = FindCancellableSkill();
if (cancellableSkill == null)
{
AppendLine("[SKIP] 기절 강제 취소 검증: 테스트용 스킬이 없습니다.");
yield break;
}
if (skillController != null)
{
yield return WaitForConditionOrTimeout(() => !skillController.IsPlayingAnimation, 1.5f);
}
if (skillController == null || !skillController.ExecuteSkill(cancellableSkill))
{
Verify("기절 강제 취소 검증: 스킬 실행 성공", false);
yield break;
}
yield return new WaitForSeconds(0.05f);
abnormalityManager.ApplyAbnormality(stunData, gameObject);
yield return WaitForConditionOrTimeout(() => abnormalityManager.IsStunned, settleDelay + 0.5f);
yield return new WaitForSeconds(0.05f);
Verify("기절 강제 취소: 스킬 애니메이션 중단", !skillController.IsPlayingAnimation);
Verify("기절 강제 취소: 취소 이유 기록", skillController.LastCancelReason == SkillCancelReason.Stun);
yield return new WaitForSeconds(stunData.duration + settleDelay);
}
private IEnumerator RunKnockbackVerification()
{
abnormalityManager.RemoveAllAbnormalities();
hitReactionController.ClearHitReactionState();
yield return new WaitForSeconds(settleDelay);
Vector3 startPosition = transform.position;
Vector3 knockbackVelocity = Vector3.back * 6f;
RequestKnockbackRpc(knockbackVelocity, 0.2f);
yield return new WaitForSeconds(0.3f);
float movedDistance = Vector3.Distance(startPosition, transform.position);
Verify("넉백 적용: 위치 이동 발생", movedDistance > 0.2f);
Verify("넉백 적용: 강제 이동 종료", !playerMovement.IsForcedMoving);
}
private IEnumerator RunDownVerification()
{
SkillData cancellableSkill = FindCancellableSkill();
if (cancellableSkill == null)
{
AppendLine("[SKIP] 다운 검증: 테스트용 스킬이 없습니다.");
yield break;
}
if (skillController != null)
{
yield return WaitForConditionOrTimeout(() => !skillController.IsPlayingAnimation, 1.5f);
}
if (skillController == null || !skillController.ExecuteSkill(cancellableSkill))
{
Verify("다운 강제 취소 검증: 스킬 실행 성공", false);
yield break;
}
yield return new WaitForSeconds(0.05f);
RequestDownRpc(0.6f);
yield return WaitForConditionOrTimeout(() => hitReactionController.IsDowned, settleDelay + 0.5f);
yield return new WaitForSeconds(0.05f);
Verify("다운 적용: IsDowned", hitReactionController.IsDowned);
Verify("다운 적용: ActionState.IsDowned", actionState.IsDowned);
Verify("다운 강제 취소: 스킬 애니메이션 중단", !skillController.IsPlayingAnimation);
Verify("다운 강제 취소: 취소 이유 기록", skillController.LastCancelReason == SkillCancelReason.HitReaction);
Verify("다운 적용: 이동 불가", !actionState.CanMove);
Verify("다운 적용: 점프 불가", !actionState.CanJump);
Verify("다운 적용: 스킬 사용 불가", !actionState.CanUseSkills);
Verify("다운 적용: 이동속도 0", Mathf.Approximately(actionState.MoveSpeedMultiplier, 0f));
yield return WaitForConditionOrTimeout(() => !hitReactionController.IsDowned, 1.5f);
Verify("다운 해제: IsDowned false", !hitReactionController.IsDowned);
Verify("다운 해제: 이동 가능 복구", actionState.CanMove);
Verify("다운 해제: 스킬 사용 가능 복구", actionState.CanUseSkills);
}
[Rpc(SendTo.Server)]
private void RequestDownRpc(float duration)
{
hitReactionController?.ApplyDown(duration);
}
[Rpc(SendTo.Server)]
private void RequestKnockbackRpc(Vector3 velocity, float duration)
{
hitReactionController?.ApplyKnockback(velocity, duration, false);
}
private float GetSkillDuration(SkillData skill)
{
if (skill == null || skill.SkillClip == null)
return settleDelay;
return Mathf.Max(settleDelay, skill.SkillClip.length / Mathf.Max(0.1f, skill.AnimationSpeed));
}
private SkillData FindCancellableSkill()
{
if (skillInput == null)
return null;
for (int i = 0; i < 6; i++)
{
SkillData skill = skillInput.GetSkill(i);
if (skill != null)
return skill;
}
return null;
}
private IEnumerator WaitForConditionOrTimeout(Func<bool> predicate, float timeout)
{
float elapsed = 0f;
while (elapsed < timeout)
{
if (predicate())
yield break;
elapsed += Time.deltaTime;
yield return null;
}
}
}
}

View File

@@ -22,6 +22,9 @@ namespace Colosseum.Player
[Tooltip("스킬 실행 관리자")]
[SerializeField] private SkillController skillController;
[Tooltip("피격 제어 관리자")]
[SerializeField] private HitReactionController hitReactionController;
[Tooltip("관전 관리자")]
[SerializeField] private PlayerSpectator spectator;
@@ -35,6 +38,11 @@ namespace Colosseum.Player
/// </summary>
public bool IsStunned => abnormalityManager != null && abnormalityManager.IsStunned;
/// <summary>
/// 다운 상태 여부
/// </summary>
public bool IsDowned => hitReactionController != null && hitReactionController.IsDowned;
/// <summary>
/// 침묵 상태 여부
/// </summary>
@@ -50,6 +58,16 @@ namespace Colosseum.Player
/// </summary>
public bool IsCastingSkill => skillController != null && skillController.IsPlayingAnimation;
/// <summary>
/// 현재 시전 중인 스킬
/// </summary>
public SkillData CurrentSkill => skillController != null ? skillController.CurrentSkill : null;
/// <summary>
/// 무적 이상상태 여부
/// </summary>
public bool IsDamageImmune => abnormalityManager != null && abnormalityManager.IsInvincible;
/// <summary>
/// 입력을 받아도 되는지 여부
/// </summary>
@@ -58,22 +76,32 @@ namespace Colosseum.Player
/// <summary>
/// 플레이어가 직접 이동 입력을 사용할 수 있는지 여부
/// </summary>
public bool CanMove => CanReceiveInput && !IsStunned && !IsCastingSkill;
public bool CanMove => CanReceiveInput && !IsStunned && !IsDowned && !BlocksMovementForCurrentSkill();
/// <summary>
/// 점프 가능 여부
/// </summary>
public bool CanJump => CanMove;
public bool CanJump => CanReceiveInput && !IsStunned && !IsDowned && !BlocksJumpForCurrentSkill();
/// <summary>
/// 스킬 사용 가능 여부
/// 일반 스킬 시작 가능 여부
/// </summary>
public bool CanUseSkills => CanReceiveInput && !IsStunned && !IsSilenced && !IsCastingSkill;
public bool CanUseSkills => CanReceiveInput && !IsStunned && !IsDowned && !IsSilenced && !BlocksSkillUseForCurrentSkill();
/// <summary>
/// 회피 사용 가능 여부
/// 특정 스킬의 시작 가능 여부.
/// 스킬 이름과 무관하게 동일한 시작 규칙을 사용합니다.
/// </summary>
public bool CanEvade => CanUseSkills;
public bool CanStartSkill(SkillData skill)
{
if (skill == null)
return false;
if (!CanReceiveInput || IsStunned || IsDowned || IsSilenced)
return false;
return !BlocksSkillUseForCurrentSkill();
}
/// <summary>
/// 현재 이동 속도 배율
@@ -82,7 +110,7 @@ namespace Colosseum.Player
{
get
{
if (!CanReceiveInput || IsStunned)
if (!CanReceiveInput || IsStunned || IsDowned)
return 0f;
return abnormalityManager != null ? abnormalityManager.MoveSpeedMultiplier : 1f;
@@ -97,8 +125,43 @@ namespace Colosseum.Player
abnormalityManager = GetComponent<AbnormalityManager>();
if (skillController == null)
skillController = GetComponent<SkillController>();
if (hitReactionController == null)
hitReactionController = GetOrCreateHitReactionController();
if (spectator == null)
spectator = GetComponentInChildren<PlayerSpectator>();
}
private bool BlocksMovementForCurrentSkill()
{
if (!IsCastingSkill)
return false;
return CurrentSkill == null || CurrentSkill.BlockMovementWhileCasting;
}
private bool BlocksJumpForCurrentSkill()
{
if (!IsCastingSkill)
return false;
return CurrentSkill == null || CurrentSkill.BlockJumpWhileCasting;
}
private bool BlocksSkillUseForCurrentSkill()
{
if (!IsCastingSkill)
return false;
return CurrentSkill == null || CurrentSkill.BlockOtherSkillsWhileCasting;
}
private HitReactionController GetOrCreateHitReactionController()
{
HitReactionController foundController = GetComponent<HitReactionController>();
if (foundController != null)
return foundController;
return gameObject.AddComponent<HitReactionController>();
}
}
}

View File

@@ -34,6 +34,8 @@ namespace Colosseum.Player
private InputSystem_Actions inputActions;
private bool isJumping;
private bool wasGrounded;
private Vector3 forcedMovementVelocity;
private float forcedMovementRemaining;
// 클라이언트가 기록, 서버가 소비하는 월드 스페이스 이동 방향
private NetworkVariable<Vector2> netMoveInput = new NetworkVariable<Vector2>(
@@ -48,6 +50,7 @@ namespace Colosseum.Player
public float CurrentMoveSpeed => netMoveInput.Value.magnitude * moveSpeed * GetMoveSpeedMultiplier();
public bool IsGrounded => characterController != null ? characterController.isGrounded : false;
public bool IsJumping => isJumping;
public bool IsForcedMoving => forcedMovementRemaining > 0f && forcedMovementVelocity.sqrMagnitude > 0.0001f;
public override void OnNetworkSpawn()
{
@@ -115,6 +118,7 @@ namespace Colosseum.Player
{
CleanupInputActions();
moveInput = Vector2.zero;
ClearForcedMovement();
}
private void OnEnable()
@@ -143,6 +147,7 @@ namespace Colosseum.Player
public override void OnNetworkDespawn()
{
CleanupInputActions();
ClearForcedMovement();
}
private void OnMovePerformed(InputAction.CallbackContext context) => moveInput = context.ReadValue<Vector2>();
@@ -259,10 +264,12 @@ namespace Colosseum.Player
{
if (characterController == null) return;
Vector3 forcedDelta = ConsumeForcedMovementDelta(Time.deltaTime);
if (skillController != null && skillController.IsPlayingAnimation)
{
if (!skillController.UsesRootMotion)
characterController.Move(velocity * Time.deltaTime);
characterController.Move(velocity * Time.deltaTime + forcedDelta);
return;
}
@@ -278,7 +285,7 @@ namespace Colosseum.Player
moveDirection = Vector3.zero;
float actualMoveSpeed = moveSpeed * GetMoveSpeedMultiplier();
characterController.Move((moveDirection * actualMoveSpeed + velocity) * Time.deltaTime);
characterController.Move((moveDirection * actualMoveSpeed + velocity) * Time.deltaTime + forcedDelta);
if (moveDirection != Vector3.zero)
{
@@ -323,6 +330,33 @@ namespace Colosseum.Player
return actionState.MoveSpeedMultiplier;
}
/// <summary>
/// 넉백처럼 입력과 무관한 강제 이동을 적용합니다.
/// </summary>
public void ApplyForcedMovement(Vector3 worldVelocity, float duration)
{
if (!IsServer)
return;
if (duration <= 0f || worldVelocity.sqrMagnitude <= 0.0001f)
{
ClearForcedMovement();
return;
}
forcedMovementVelocity = worldVelocity;
forcedMovementRemaining = duration;
}
/// <summary>
/// 강제 이동 상태를 즉시 초기화합니다.
/// </summary>
public void ClearForcedMovement()
{
forcedMovementVelocity = Vector3.zero;
forcedMovementRemaining = 0f;
}
private PlayerActionState GetOrCreateActionState()
{
var foundState = GetComponent<PlayerActionState>();
@@ -343,6 +377,7 @@ namespace Colosseum.Player
if (!skillController.UsesRootMotion) return;
Vector3 deltaPosition = animator.deltaPosition;
Vector3 forcedDelta = ConsumeForcedMovementDelta(Time.deltaTime);
if (blockedDirection != Vector3.zero)
{
@@ -357,11 +392,11 @@ namespace Colosseum.Player
if (skillController.IgnoreRootMotionY)
{
deltaPosition.y = 0f;
characterController.Move(deltaPosition + velocity * Time.deltaTime);
characterController.Move(deltaPosition + velocity * Time.deltaTime + forcedDelta);
}
else
{
characterController.Move(deltaPosition);
characterController.Move(deltaPosition + forcedDelta);
}
if (animator.deltaRotation != Quaternion.identity)
@@ -372,5 +407,22 @@ namespace Colosseum.Player
wasGrounded = characterController.isGrounded;
}
private Vector3 ConsumeForcedMovementDelta(float deltaTime)
{
if (forcedMovementRemaining <= 0f || forcedMovementVelocity.sqrMagnitude <= 0.0001f)
return Vector3.zero;
float appliedDeltaTime = Mathf.Min(deltaTime, forcedMovementRemaining);
forcedMovementRemaining = Mathf.Max(0f, forcedMovementRemaining - appliedDeltaTime);
Vector3 delta = forcedMovementVelocity * appliedDeltaTime;
if (forcedMovementRemaining <= 0f)
{
forcedMovementVelocity = Vector3.zero;
}
return delta;
}
}
}

View File

@@ -18,6 +18,9 @@ namespace Colosseum.Player
[Tooltip("CharacterStats 컴포넌트 (없으면 자동 검색)")]
[SerializeField] private CharacterStats characterStats;
[Tooltip("이상상태 관리자 (없으면 자동 검색)")]
[SerializeField] private AbnormalityManager abnormalityManager;
// 네트워크 동기화 변수
private NetworkVariable<float> currentHealth = new NetworkVariable<float>(100f);
private NetworkVariable<float> currentMana = new NetworkVariable<float>(50f);
@@ -50,6 +53,11 @@ namespace Colosseum.Player
characterStats = GetComponent<CharacterStats>();
}
if (abnormalityManager == null)
{
abnormalityManager = GetComponent<AbnormalityManager>();
}
// 네트워크 변수 변경 콜백 등록
currentHealth.OnValueChanged += HandleHealthChanged;
currentMana.OnValueChanged += HandleManaChanged;
@@ -93,7 +101,7 @@ namespace Colosseum.Player
[Rpc(SendTo.Server)]
public void TakeDamageRpc(float damage)
{
if (isDead.Value) return;
if (isDead.Value || IsDamageImmune()) return;
currentHealth.Value = Mathf.Max(0f, currentHealth.Value - damage);
@@ -159,7 +167,6 @@ namespace Colosseum.Player
isDead.Value = true;
// 사망 시 활성 이상 상태를 정리해 리스폰 시 잔존하지 않게 합니다.
var abnormalityManager = GetComponent<AbnormalityManager>();
if (abnormalityManager != null)
{
abnormalityManager.RemoveAllAbnormalities();
@@ -169,9 +176,16 @@ namespace Colosseum.Player
var movement = GetComponent<PlayerMovement>();
if (movement != null)
{
movement.ClearForcedMovement();
movement.enabled = false;
}
var hitReactionController = GetComponent<HitReactionController>();
if (hitReactionController != null)
{
hitReactionController.ClearHitReactionState();
}
// 스킬 입력 비활성화
var skillInput = GetComponent<PlayerSkillInput>();
if (skillInput != null)
@@ -183,7 +197,7 @@ namespace Colosseum.Player
var skillController = GetComponent<SkillController>();
if (skillController != null)
{
skillController.CancelSkill();
skillController.CancelSkill(SkillCancelReason.Death);
}
// 모든 클라이언트에서 사망 애니메이션 재생
@@ -202,7 +216,6 @@ namespace Colosseum.Player
{
if (!IsServer) return;
var abnormalityManager = GetComponent<AbnormalityManager>();
if (abnormalityManager != null)
{
abnormalityManager.RemoveAllAbnormalities();
@@ -216,9 +229,16 @@ namespace Colosseum.Player
var movement = GetComponent<PlayerMovement>();
if (movement != null)
{
movement.ClearForcedMovement();
movement.enabled = true;
}
var hitReactionController = GetComponent<HitReactionController>();
if (hitReactionController != null)
{
hitReactionController.ClearHitReactionState();
}
// 스킬 입력 재활성화
var skillInput = GetComponent<PlayerSkillInput>();
if (skillInput != null)
@@ -233,6 +253,12 @@ namespace Colosseum.Player
animator.Rebind();
}
var skillController = GetComponent<SkillController>();
if (skillController != null)
{
skillController.CancelSkill(SkillCancelReason.Respawn);
}
OnRespawned?.Invoke(this);
Debug.Log($"[Player] Player {OwnerClientId} respawned!");
@@ -244,7 +270,7 @@ namespace Colosseum.Player
/// </summary>
public float TakeDamage(float damage, object source = null)
{
if (!IsServer || isDead.Value) return 0f;
if (!IsServer || isDead.Value || IsDamageImmune()) return 0f;
float actualDamage = Mathf.Min(damage, currentHealth.Value);
currentHealth.Value = Mathf.Max(0f, currentHealth.Value - damage);
@@ -269,6 +295,11 @@ namespace Colosseum.Player
return actualHeal;
}
private bool IsDamageImmune()
{
return abnormalityManager != null && abnormalityManager.IsInvincible;
}
#endregion
}
}

View File

@@ -15,7 +15,7 @@ namespace Colosseum.Player
public class PlayerSkillInput : NetworkBehaviour
{
[Header("Skill Slots")]
[Tooltip("각 슬롯에 등록할 스킬 데이터 (6개 + 긴급회피)")]
[Tooltip("각 슬롯에 등록할 스킬 데이터 (6개 + 추가 슬롯)")]
[SerializeField] private SkillData[] skillSlots = new SkillData[7];
[Header("References")]
@@ -131,7 +131,7 @@ namespace Colosseum.Player
}
// 사망 상태 체크
if (actionState != null && !actionState.CanUseSkills)
if (actionState != null && !actionState.CanStartSkill(skill))
return;
// 로컬 체크 (빠른 피드백용)
@@ -172,8 +172,7 @@ namespace Colosseum.Player
if (skill == null) return;
// 서버에서 다시 검증
// 사망 상태 체크
if (actionState != null && !actionState.CanUseSkills)
if (actionState != null && !actionState.CanStartSkill(skill))
return;
if (skillController.IsExecutingSkill || skillController.IsOnCooldown(skill))
@@ -263,7 +262,7 @@ namespace Colosseum.Player
SkillData skill = GetSkill(slotIndex);
if (skill == null) return false;
if (actionState != null && !actionState.CanUseSkills)
if (actionState != null && !actionState.CanStartSkill(skill))
return false;
return !skillController.IsOnCooldown(skill) && !skillController.IsExecutingSkill;

View File

@@ -0,0 +1,46 @@
using UnityEngine;
using Colosseum.Player;
namespace Colosseum.Skills.Effects
{
/// <summary>
/// 대상에게 다운 상태를 적용하는 스킬 효과입니다.
/// </summary>
[CreateAssetMenu(fileName = "DownEffect", menuName = "Colosseum/Skills/Effects/Down")]
public class DownEffect : SkillEffect
{
[Header("Settings")]
[Tooltip("다운 지속 시간")]
[Min(0f)] [SerializeField] private float duration = 1f;
/// <summary>
/// 다운 지속 시간
/// </summary>
public float Duration => duration;
/// <summary>
/// 다운 효과를 적용합니다.
/// </summary>
protected override void ApplyEffect(GameObject source, GameObject target)
{
if (target == null)
{
Debug.LogWarning("[DownEffect] Target is null.");
return;
}
HitReactionController hitReactionController = target.GetComponent<HitReactionController>();
if (hitReactionController == null)
hitReactionController = target.GetComponentInParent<HitReactionController>();
if (hitReactionController == null)
{
Debug.LogWarning($"[DownEffect] HitReactionController not found on target: {target.name}");
return;
}
hitReactionController.ApplyDown(duration);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 41c96b54a96cdb84c9bda774775b0a1a

View File

@@ -0,0 +1,93 @@
using UnityEngine;
using Colosseum.Combat;
using Colosseum.Player;
using Colosseum.Stats;
using Colosseum.Weapons;
namespace Colosseum.Skills.Effects
{
/// <summary>
/// 피격 제어 상태에 따라 추가 피해를 줄 수 있는 대미지 효과입니다.
/// 현재는 다운 상태인 대상에게 추가 배율을 적용합니다.
/// </summary>
[CreateAssetMenu(fileName = "HitReactionDamageEffect", menuName = "Colosseum/Skills/Effects/HitReaction Damage")]
public class HitReactionDamageEffect : SkillEffect
{
[Header("Damage Settings")]
[Min(0f)] [SerializeField] private float baseDamage = 10f;
[SerializeField] private DamageType damageType = DamageType.Physical;
[Tooltip("스탯 계수 (1.0 = 100%)")]
[Min(0f)] [SerializeField] private float statScaling = 1f;
[Header("Hit Reaction Bonus")]
[Tooltip("다운 상태인 대상에게 추가 배율을 적용할지 여부")]
[SerializeField] private bool bonusAgainstDownedTarget = true;
[Tooltip("다운 상태 대상에게 적용되는 추가 피해 배율")]
[Min(1f)] [SerializeField] private float downedDamageMultiplier = 1.5f;
protected override void ApplyEffect(GameObject caster, GameObject target)
{
if (target == null)
return;
IDamageable damageable = target.GetComponent<IDamageable>();
if (damageable == null)
return;
float totalDamage = CalculateDamage(caster);
HitReactionController hitReactionController = target.GetComponent<HitReactionController>();
if (hitReactionController == null)
hitReactionController = target.GetComponentInParent<HitReactionController>();
if (bonusAgainstDownedTarget
&& hitReactionController != null
&& hitReactionController.IsDowned)
{
totalDamage *= downedDamageMultiplier;
}
damageable.TakeDamage(totalDamage, caster);
}
/// <summary>
/// 시전자 스탯 기반 대미지 계산
/// 공식: baseDamage + (statDamage * scaling)
/// </summary>
private float CalculateDamage(GameObject caster)
{
if (damageType == DamageType.True)
{
return baseDamage;
}
CharacterStats stats = caster != null ? caster.GetComponent<CharacterStats>() : null;
if (stats == null)
{
return baseDamage;
}
float statDamage = damageType switch
{
DamageType.Physical => stats.PhysicalDamage,
DamageType.Magical => stats.MagicDamage,
DamageType.Ranged => stats.Dexterity.FinalValue * 2f,
_ => 0f,
};
float baseTotal = baseDamage + (statDamage * statScaling);
return baseTotal * GetDamageMultiplier(caster);
}
/// <summary>
/// 시전자의 무기 데미지 배율 조회
/// </summary>
private float GetDamageMultiplier(GameObject caster)
{
WeaponEquipment weaponEquipment = caster != null ? caster.GetComponent<WeaponEquipment>() : null;
return weaponEquipment != null ? weaponEquipment.DamageMultiplier : 1f;
}
}
}

View File

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

View File

@@ -1,5 +1,7 @@
using UnityEngine;
using Colosseum.Player;
namespace Colosseum.Skills.Effects
{
/// <summary>
@@ -11,6 +13,7 @@ namespace Colosseum.Skills.Effects
[Header("Knockback Settings")]
[Min(0f)] [SerializeField] private float force = 5f;
[SerializeField] private float upwardForce = 2f;
[Min(0.05f)] [SerializeField] private float duration = 0.2f;
protected override void ApplyEffect(GameObject caster, GameObject target)
{
@@ -18,13 +21,31 @@ namespace Colosseum.Skills.Effects
Vector3 direction = target.transform.position - caster.transform.position;
direction.y = 0f;
direction.Normalize();
if (direction.sqrMagnitude <= 0.0001f)
direction = caster.transform.forward;
else
direction.Normalize();
Vector3 knockback = direction * force + Vector3.up * upwardForce;
Vector3 knockbackVelocity = direction * force + Vector3.up * upwardForce;
// TODO: 실제 물리 시스템 연동
// if (target.TryGetComponent<Rigidbody>(out var rb))
// rb.AddForce(knockback, ForceMode.Impulse);
HitReactionController hitReactionController = target.GetComponent<HitReactionController>();
if (hitReactionController == null)
hitReactionController = target.GetComponentInParent<HitReactionController>();
if (hitReactionController != null)
{
hitReactionController.ApplyKnockback(knockbackVelocity, duration);
return;
}
PlayerMovement playerMovement = target.GetComponent<PlayerMovement>();
if (playerMovement == null)
playerMovement = target.GetComponentInParent<PlayerMovement>();
if (playerMovement != null)
{
playerMovement.ApplyForcedMovement(knockbackVelocity, duration);
}
}
}
}

View File

@@ -4,6 +4,19 @@ using Unity.Netcode;
namespace Colosseum.Skills
{
/// <summary>
/// 스킬 강제 취소 이유
/// </summary>
public enum SkillCancelReason
{
None,
Manual,
Death,
Stun,
HitReaction,
Respawn,
}
/// <summary>
/// 스킬 실행을 관리하는 컴포넌트.
/// 애니메이션 이벤트 기반으로 효과가 발동됩니다.
@@ -31,6 +44,12 @@ namespace Colosseum.Skills
[Tooltip("범위 표시 지속 시간")]
[Min(0.1f)] [SerializeField] private float debugDrawDuration = 1f;
[Header("디버그")]
[Tooltip("마지막으로 강제 취소된 스킬 이름")]
[SerializeField] private string lastCancelledSkillName = string.Empty;
[Tooltip("마지막 강제 취소 이유")]
[SerializeField] private SkillCancelReason lastCancelReason = SkillCancelReason.None;
// 현재 실행 중인 스킬
private SkillData currentSkill;
private bool skillEndRequested; // OnSkillEnd 이벤트 호출 여부
@@ -47,9 +66,14 @@ namespace Colosseum.Skills
public bool IgnoreRootMotionY => currentSkill != null && currentSkill.IgnoreRootMotionY;
public SkillData CurrentSkill => currentSkill;
public Animator Animator => animator;
public SkillCancelReason LastCancelReason => lastCancelReason;
public string LastCancelledSkillName => lastCancelledSkillName;
private void Awake()
{
lastCancelledSkillName = string.Empty;
lastCancelReason = SkillCancelReason.None;
if (animator == null)
{
animator = GetComponentInChildren<Animator>();
@@ -135,6 +159,7 @@ namespace Colosseum.Skills
currentSkill = skill;
skillEndRequested = false;
waitingForEndAnimation = false;
lastCancelReason = SkillCancelReason.None;
if (debugMode) Debug.Log($"[Skill] Cast: {skill.SkillName}");
@@ -148,9 +173,34 @@ namespace Colosseum.Skills
PlaySkillClip(skill.SkillClip);
}
TriggerImmediateSelfEffectsIfNeeded(skill);
return true;
}
/// <summary>
/// 애니메이션 이벤트가 없는 자기 자신 대상 효과는 시전 즉시 발동합니다.
/// 버프/무적 같은 자기 강화 스킬이 이벤트 누락으로 동작하지 않는 상황을 막기 위한 보정입니다.
/// </summary>
private void TriggerImmediateSelfEffectsIfNeeded(SkillData skill)
{
if (skill == null || skill.Effects == null || skill.Effects.Count == 0)
return;
if (skill.SkillClip != null && skill.SkillClip.events != null && skill.SkillClip.events.Length > 0)
return;
for (int i = 0; i < skill.Effects.Count; i++)
{
SkillEffect effect = skill.Effects[i];
if (effect == null || effect.TargetType != TargetType.Self)
continue;
if (debugMode) Debug.Log($"[Skill] Immediate self effect: {effect.name} (index {i})");
effect.ExecuteOnCast(gameObject);
}
}
/// <summary>
/// 스킬 클립으로 Override Controller 생성 후 재생
/// </summary>
@@ -314,16 +364,24 @@ namespace Colosseum.Skills
if (debugMode) Debug.Log($"[Skill] End requested: {currentSkill.SkillName} (will complete after animation)");
}
public void CancelSkill()
/// <summary>
/// 현재 스킬을 강제 취소합니다.
/// </summary>
public bool CancelSkill(SkillCancelReason reason = SkillCancelReason.Manual)
{
if (currentSkill != null)
{
if (debugMode) Debug.Log($"Skill cancelled: {currentSkill.SkillName}");
RestoreBaseController();
currentSkill = null;
skillEndRequested = false;
waitingForEndAnimation = false;
}
if (currentSkill == null)
return false;
lastCancelledSkillName = currentSkill.SkillName;
lastCancelReason = reason;
Debug.Log($"[Skill] Cancelled: {currentSkill.SkillName} / reason={reason}");
RestoreBaseController();
currentSkill = null;
skillEndRequested = false;
waitingForEndAnimation = false;
return true;
}
public bool IsOnCooldown(SkillData skill)

View File

@@ -31,6 +31,14 @@ namespace Colosseum.Skills
[Tooltip("스킬 시전 시 대상 위치로 점프 이동 (UseRootMotion + IgnoreRootMotionY=false 필요)")]
[SerializeField] private bool jumpToTarget = false;
[Header("행동 제한")]
[Tooltip("시전 중 이동 입력 차단 여부")]
[SerializeField] private bool blockMovementWhileCasting = true;
[Tooltip("시전 중 점프 입력 차단 여부")]
[SerializeField] private bool blockJumpWhileCasting = true;
[Tooltip("시전 중 다른 스킬 입력 차단 여부")]
[SerializeField] private bool blockOtherSkillsWhileCasting = true;
[Header("쿨타임 & 비용")]
[Min(0f)] [SerializeField] private float cooldown = 1f;
[Min(0f)] [SerializeField] private float manaCost = 0f;
@@ -51,6 +59,9 @@ namespace Colosseum.Skills
public bool UseRootMotion => useRootMotion;
public bool IgnoreRootMotionY => ignoreRootMotionY;
public bool JumpToTarget => jumpToTarget;
public bool BlockMovementWhileCasting => blockMovementWhileCasting;
public bool BlockJumpWhileCasting => blockJumpWhileCasting;
public bool BlockOtherSkillsWhileCasting => blockOtherSkillsWhileCasting;
public IReadOnlyList<SkillEffect> Effects => effects;
}
}