diff --git a/.plastic/plastic.changes b/.plastic/plastic.changes
index e6e6549b..f263e34e 100644
Binary files a/.plastic/plastic.changes and b/.plastic/plastic.changes differ
diff --git a/.plastic/plastic.wktree b/.plastic/plastic.wktree
index a07c34ed..8ca797e9 100644
Binary files a/.plastic/plastic.wktree and b/.plastic/plastic.wktree differ
diff --git a/Assets/0.SCENE/MainGame.unity b/Assets/0.SCENE/MainGame.unity
index 4c03038b..d6255195 100644
--- a/Assets/0.SCENE/MainGame.unity
+++ b/Assets/0.SCENE/MainGame.unity
@@ -4406,6 +4406,10 @@ PrefabInstance:
propertyPath: m_Name
value: SwordMonster
objectReference: {fileID: 0}
+ - target: {fileID: 5674935864780053661, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3}
+ propertyPath: m_IsActive
+ value: 0
+ objectReference: {fileID: 0}
- target: {fileID: 8892907556271350198, guid: 50c1bf70f87f8124baeacfb51b165d44, type: 3}
propertyPath: m_LocalPosition.x
value: 19.30664
@@ -9125,10 +9129,10 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 1f525fb5022b0754b9c5e1d725f8b2a4, type: 3}
m_Name:
m_EditorClassIdentifier:
- spawnRange: 15
- respawnCooldown: 3
- mobTag: Throw Monster
- optimizationRange: 60
+ monsterPrefabs: []
+ spawnPoints: []
+ spawnInterval: 5
+ maxMonsters: 20
--- !u!4 &85056390
Transform:
m_ObjectHideFlags: 0
@@ -39081,165 +39085,17 @@ Transform:
m_CorrespondingSourceObject: {fileID: 4899585634617078, guid: 9b42f9d14b52bd840afbef0e34f754f2, type: 3}
m_PrefabInstance: {fileID: 358590105}
m_PrefabAsset: {fileID: 0}
---- !u!1 &358757529 stripped
-GameObject:
- m_CorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- m_PrefabInstance: {fileID: 1101415473}
- m_PrefabAsset: {fileID: 0}
---- !u!114 &358757530
+--- !u!114 &358757531 stripped
MonoBehaviour:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
+ m_CorrespondingSourceObject: {fileID: 9108673629704904850, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ m_PrefabInstance: {fileID: 7311604695488881352}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 358757529}
- m_Enabled: 1
- m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: 5e36870a705f4fd44923bc5704c4a93b, type: 3}
- m_Name:
- m_EditorClassIdentifier:
- config: {fileID: 11400000, guid: 4c6a2993f687f65409c4ece370872444, type: 2}
- OnCounterActivated:
- m_PersistentCalls:
- m_Calls: []
- OnCounterDeactivated:
- m_PersistentCalls:
- m_Calls: []
- OnCounterPatternSelected:
- m_PersistentCalls:
- m_Calls: []
- OnHabitChangeRewarded:
- m_PersistentCalls:
- m_Calls: []
- bossPatterns:
- - patternName: Smash
- baseWeight: 30
- counterType: 1
- subCounterType: 0
- - patternName: DashAttack
- baseWeight: 30
- counterType: 2
- subCounterType: 0
- - patternName: ShieldWall
- baseWeight: 20
- counterType: 3
- subCounterType: 0
---- !u!114 &358757531
-MonoBehaviour:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 358757529}
+ m_GameObject: {fileID: 1231090171}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 04f39bf969074e6488f84b47bd517dc9, type: 3}
m_Name:
m_EditorClassIdentifier:
- optimizationDistance: 40
- maxHP: 100
- attackDamage: 10
- expReward: 10
- moveSpeed: 3.5
- myWeapon: {fileID: 0}
- Monster_Idle: Monster_Idle
- Monster_GetDamage: Monster_GetDamage
- Monster_Die: Monster_Die
- attackRestDuration: 1.5
- hitSound: {fileID: 0}
- deathSound: {fileID: 0}
- deathEffectPrefab: {fileID: 0}
- hitEffect: {fileID: 0}
- impactSpawnPoint: {fileID: 0}
- counterSystem: {fileID: 358757530}
- patternInterval: 3
- attackRange: 3
- ironBall: {fileID: 1550577165}
- handHolder: {fileID: 1325714516}
- bossHealthBar: {fileID: 0}
- anim_Roar: Roar
- anim_Idle: Monster_Idle
- anim_Walk: Monster_Walk
- anim_Pickup: Skill_Pickup
- anim_Throw: Attack_Throw
- anim_DashReady: Skill_Dash_Ready
- anim_DashGo: Skill_Dash_Go
- anim_SmashCharge: Skill_Smash_Charge
- anim_SmashImpact: Skill_Smash_Impact
- anim_Sweep: Skill_Sweep
- debugState: "\uB300\uAE30 \uC911"
---- !u!136 &358757532
-CapsuleCollider:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 358757529}
- m_Material: {fileID: 0}
- m_IncludeLayers:
- serializedVersion: 2
- m_Bits: 0
- m_ExcludeLayers:
- serializedVersion: 2
- m_Bits: 0
- m_LayerOverridePriority: 0
- m_IsTrigger: 0
- m_ProvidesContacts: 0
- m_Enabled: 1
- serializedVersion: 2
- m_Radius: 0.5
- m_Height: 2.02
- m_Direction: 1
- m_Center: {x: 0, y: 0.9, z: 0}
---- !u!195 &358757533
-NavMeshAgent:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 358757529}
- m_Enabled: 1
- m_AgentTypeID: 0
- m_Radius: 0.5
- m_Speed: 3.5
- m_Acceleration: 8
- avoidancePriority: 50
- m_AngularSpeed: 120
- m_StoppingDistance: 0
- m_AutoTraverseOffMeshLink: 1
- m_AutoBraking: 1
- m_AutoRepath: 1
- m_Height: 2
- m_BaseOffset: 0
- m_WalkableMask: 4294967295
- m_ObstacleAvoidanceType: 4
---- !u!54 &358757536
-Rigidbody:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 358757529}
- serializedVersion: 4
- m_Mass: 1
- m_Drag: 0
- m_AngularDrag: 0.05
- m_CenterOfMass: {x: 0, y: 0, z: 0}
- m_InertiaTensor: {x: 1, y: 1, z: 1}
- m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1}
- m_IncludeLayers:
- serializedVersion: 2
- m_Bits: 0
- m_ExcludeLayers:
- serializedVersion: 2
- m_Bits: 0
- m_ImplicitCom: 1
- m_ImplicitTensor: 1
- m_UseGravity: 1
- m_IsKinematic: 1
- m_Interpolate: 0
- m_Constraints: 80
- m_CollisionDetection: 0
--- !u!1001 &358842824
PrefabInstance:
m_ObjectHideFlags: 0
@@ -66111,10 +65967,10 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 1f525fb5022b0754b9c5e1d725f8b2a4, type: 3}
m_Name:
m_EditorClassIdentifier:
- spawnRange: 15
- respawnCooldown: 3
- mobTag: Sword Monster
- optimizationRange: 60
+ monsterPrefabs: []
+ spawnPoints: []
+ spawnInterval: 5
+ maxMonsters: 20
--- !u!4 &600231664
Transform:
m_ObjectHideFlags: 0
@@ -66531,9 +66387,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 9eb5b341f7503494e9b0f88cc4b1c17d, type: 3}
m_Name:
m_EditorClassIdentifier:
- healthSource: {fileID: 0}
- hpFillImage: {fileID: 0}
- hpText: {fileID: 0}
+ targetObject: {fileID: 0}
--- !u!1001 &603601734
PrefabInstance:
m_ObjectHideFlags: 0
@@ -68621,11 +68475,6 @@ Transform:
m_CorrespondingSourceObject: {fileID: 4012526298001850, guid: 1b1f948f132c1cc429df46b11756f4a2, type: 3}
m_PrefabInstance: {fileID: 619812415}
m_PrefabAsset: {fileID: 0}
---- !u!4 &620355262 stripped
-Transform:
- m_CorrespondingSourceObject: {fileID: 1852576806548013000, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- m_PrefabInstance: {fileID: 1101415473}
- m_PrefabAsset: {fileID: 0}
--- !u!1 &620650552
GameObject:
m_ObjectHideFlags: 0
@@ -115738,7 +115587,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
boss: {fileID: 358757531}
- fogWall: {fileID: 0}
+ entranceWall: {fileID: 0}
--- !u!65 &1081393078
BoxCollider:
m_ObjectHideFlags: 0
@@ -119033,105 +118882,6 @@ Transform:
m_CorrespondingSourceObject: {fileID: 4062256764471868, guid: 6c3328059b63be44f96b3571e2691511, type: 3}
m_PrefabInstance: {fileID: 1101322778}
m_PrefabAsset: {fileID: 0}
---- !u!1001 &1101415473
-PrefabInstance:
- m_ObjectHideFlags: 0
- serializedVersion: 2
- m_Modification:
- serializedVersion: 3
- m_TransformParent: {fileID: 0}
- m_Modifications:
- - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- propertyPath: m_LocalScale.x
- value: 1.8233726
- objectReference: {fileID: 0}
- - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- propertyPath: m_LocalScale.y
- value: 1.8233726
- objectReference: {fileID: 0}
- - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- propertyPath: m_LocalScale.z
- value: 1.8233726
- objectReference: {fileID: 0}
- - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- propertyPath: m_LocalPosition.x
- value: 27.059
- objectReference: {fileID: 0}
- - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- propertyPath: m_LocalPosition.y
- value: 5.48
- objectReference: {fileID: 0}
- - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- propertyPath: m_LocalPosition.z
- value: 33.461
- objectReference: {fileID: 0}
- - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- propertyPath: m_LocalRotation.w
- value: 1
- objectReference: {fileID: 0}
- - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- propertyPath: m_LocalRotation.x
- value: 0
- objectReference: {fileID: 0}
- - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- propertyPath: m_LocalRotation.y
- value: 0
- objectReference: {fileID: 0}
- - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- propertyPath: m_LocalRotation.z
- value: 0
- objectReference: {fileID: 0}
- - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- propertyPath: m_LocalEulerAnglesHint.x
- value: 0
- objectReference: {fileID: 0}
- - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- propertyPath: m_LocalEulerAnglesHint.y
- value: 0
- objectReference: {fileID: 0}
- - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- propertyPath: m_LocalEulerAnglesHint.z
- value: 0
- objectReference: {fileID: 0}
- - target: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- propertyPath: m_Name
- value: BossMonster 3D model
- objectReference: {fileID: 0}
- - target: {fileID: 5866666021909216657, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- propertyPath: m_Enabled
- value: 1
- objectReference: {fileID: 0}
- - target: {fileID: 5866666021909216657, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- propertyPath: m_Controller
- value:
- objectReference: {fileID: 9100000, guid: b207531287111e74d890e730f7278dfa, type: 2}
- - target: {fileID: 5866666021909216657, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- propertyPath: m_ApplyRootMotion
- value: 0
- objectReference: {fileID: 0}
- m_RemovedComponents: []
- m_RemovedGameObjects: []
- m_AddedGameObjects:
- - targetCorrespondingSourceObject: {fileID: 1852576806548013000, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- insertIndex: -1
- addedObject: {fileID: 1325714516}
- m_AddedComponents:
- - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- insertIndex: -1
- addedObject: {fileID: 358757533}
- - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- insertIndex: -1
- addedObject: {fileID: 358757532}
- - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- insertIndex: -1
- addedObject: {fileID: 358757531}
- - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- insertIndex: -1
- addedObject: {fileID: 358757530}
- - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
- insertIndex: -1
- addedObject: {fileID: 358757536}
- m_SourcePrefab: {fileID: 100100000, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
--- !u!1001 &1101521501
PrefabInstance:
m_ObjectHideFlags: 0
@@ -134213,6 +133963,28 @@ PrefabInstance:
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: 26a3cbfd6906bd74e848cf2c8b765e85, type: 3}
+--- !u!1 &1231090171 stripped
+GameObject:
+ m_CorrespondingSourceObject: {fileID: 5696671955299564549, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ m_PrefabInstance: {fileID: 7311604695488881352}
+ m_PrefabAsset: {fileID: 0}
+--- !u!4 &1231090172 stripped
+Transform:
+ m_CorrespondingSourceObject: {fileID: 4919466100338403007, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ m_PrefabInstance: {fileID: 7311604695488881352}
+ m_PrefabAsset: {fileID: 0}
+--- !u!114 &1231090178
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1231090171}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: c23a9c1b76724ab459bf32a732f6a2f7, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
--- !u!1001 &1231957954
PrefabInstance:
m_ObjectHideFlags: 0
@@ -143010,38 +142782,6 @@ Transform:
m_CorrespondingSourceObject: {fileID: 4708095961488610, guid: 01d14f7c3a49b4f4e86caee63ef815b1, type: 3}
m_PrefabInstance: {fileID: 1323788198}
m_PrefabAsset: {fileID: 0}
---- !u!1 &1325714515
-GameObject:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- serializedVersion: 6
- m_Component:
- - component: {fileID: 1325714516}
- m_Layer: 12
- m_Name: handPos
- m_TagString: Untagged
- m_Icon: {fileID: 0}
- m_NavMeshLayer: 0
- m_StaticEditorFlags: 0
- m_IsActive: 1
---- !u!4 &1325714516
-Transform:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1325714515}
- serializedVersion: 2
- m_LocalRotation: {x: -0.038344428, y: 0.70765644, z: -0.0024401005, w: -0.7055112}
- m_LocalPosition: {x: 1.304071, y: 1.8877394, z: -1.5619158}
- m_LocalScale: {x: 234.34999, y: 234.35004, z: 234.35002}
- m_ConstrainProportionsScale: 0
- m_Children:
- - {fileID: 1550577166}
- m_Father: {fileID: 620355262}
- m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1001 &1326407535
PrefabInstance:
m_ObjectHideFlags: 0
@@ -165685,111 +165425,6 @@ Transform:
m_CorrespondingSourceObject: {fileID: 4822900260135478, guid: 75fdec6c596df2b44b567836ba593557, type: 3}
m_PrefabInstance: {fileID: 1548942974}
m_PrefabAsset: {fileID: 0}
---- !u!1 &1550577165
-GameObject:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- serializedVersion: 6
- m_Component:
- - component: {fileID: 1550577166}
- - component: {fileID: 1550577169}
- - component: {fileID: 1550577168}
- - component: {fileID: 1550577167}
- m_Layer: 12
- m_Name: SilverBall
- m_TagString: Untagged
- m_Icon: {fileID: 0}
- m_NavMeshLayer: 0
- m_StaticEditorFlags: 0
- m_IsActive: 1
---- !u!4 &1550577166
-Transform:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1550577165}
- serializedVersion: 2
- m_LocalRotation: {x: 0.002655249, y: -0.03898664, z: -0.72380346, w: -0.68889886}
- m_LocalPosition: {x: 0.00886, y: -0.00478, z: 0.0054}
- m_LocalScale: {x: 0.0055073895, y: 0.0055073905, z: 0.0055073895}
- m_ConstrainProportionsScale: 0
- m_Children: []
- m_Father: {fileID: 1325714516}
- m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
---- !u!135 &1550577167
-SphereCollider:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1550577165}
- m_Material: {fileID: 0}
- m_IncludeLayers:
- serializedVersion: 2
- m_Bits: 0
- m_ExcludeLayers:
- serializedVersion: 2
- m_Bits: 0
- m_LayerOverridePriority: 0
- m_IsTrigger: 0
- m_ProvidesContacts: 0
- m_Enabled: 1
- serializedVersion: 3
- m_Radius: 0.5
- m_Center: {x: 0, y: 0, z: 0}
---- !u!23 &1550577168
-MeshRenderer:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1550577165}
- m_Enabled: 1
- m_CastShadows: 1
- m_ReceiveShadows: 1
- m_DynamicOccludee: 1
- m_StaticShadowCaster: 0
- m_MotionVectors: 1
- m_LightProbeUsage: 1
- m_ReflectionProbeUsage: 1
- m_RayTracingMode: 2
- m_RayTraceProcedural: 0
- m_RenderingLayerMask: 1
- m_RendererPriority: 0
- m_Materials:
- - {fileID: 2100000, guid: aea925dd0ba5758438a4e26ced28244e, type: 2}
- m_StaticBatchInfo:
- firstSubMesh: 0
- subMeshCount: 0
- m_StaticBatchRoot: {fileID: 0}
- m_ProbeAnchor: {fileID: 0}
- m_LightProbeVolumeOverride: {fileID: 0}
- m_ScaleInLightmap: 1
- m_ReceiveGI: 1
- m_PreserveUVs: 0
- m_IgnoreNormalsForChartDetection: 0
- m_ImportantGI: 0
- m_StitchLightmapSeams: 1
- m_SelectedEditorRenderState: 3
- m_MinimumChartSize: 4
- m_AutoUVMaxDistance: 0.5
- m_AutoUVMaxAngle: 89
- m_LightmapParameters: {fileID: 0}
- m_SortingLayerID: 0
- m_SortingLayer: 0
- m_SortingOrder: 0
- m_AdditionalVertexStreams: {fileID: 0}
---- !u!33 &1550577169
-MeshFilter:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1550577165}
- m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0}
--- !u!1001 &1551121841
PrefabInstance:
m_ObjectHideFlags: 0
@@ -187825,6 +187460,11 @@ Transform:
m_CorrespondingSourceObject: {fileID: 4062256764471868, guid: 6c3328059b63be44f96b3571e2691511, type: 3}
m_PrefabInstance: {fileID: 1759213612}
m_PrefabAsset: {fileID: 0}
+--- !u!1 &1760701545 stripped
+GameObject:
+ m_CorrespondingSourceObject: {fileID: 7767661558022019919, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
+ m_PrefabInstance: {fileID: 374250731514228064}
+ m_PrefabAsset: {fileID: 0}
--- !u!1001 &1762494340
PrefabInstance:
m_ObjectHideFlags: 0
@@ -204619,10 +204259,10 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 1f525fb5022b0754b9c5e1d725f8b2a4, type: 3}
m_Name:
m_EditorClassIdentifier:
- spawnRange: 15
- respawnCooldown: 3
- mobTag: Run Monster
- optimizationRange: 60
+ monsterPrefabs: []
+ spawnPoints: []
+ spawnInterval: 5
+ maxMonsters: 20
--- !u!4 &1927423250
Transform:
m_ObjectHideFlags: 0
@@ -219436,10 +219076,10 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 1f525fb5022b0754b9c5e1d725f8b2a4, type: 3}
m_Name:
m_EditorClassIdentifier:
- spawnRange: 15
- respawnCooldown: 3
- mobTag: Kamikaze
- optimizationRange: 60
+ monsterPrefabs: []
+ spawnPoints: []
+ spawnInterval: 5
+ maxMonsters: 20
--- !u!4 &2067270637
Transform:
m_ObjectHideFlags: 0
@@ -224983,7 +224623,21 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: cecfa58891603f842944dcc723b41699, type: 3}
m_Name:
m_EditorClassIdentifier:
+ optimizationDistance: 40
maxHP: 100
+ attackDamage: 10
+ expReward: 10
+ moveSpeed: 3.5
+ myWeapon: {fileID: 0}
+ Monster_Idle: Monster_Idle
+ Monster_GetDamage: Monster_GetDamage
+ Monster_Die: Monster_Die
+ attackRestDuration: 1.5
+ hitSound: {fileID: 0}
+ deathSound: {fileID: 0}
+ deathEffectPrefab: {fileID: 0}
+ hitEffect: {fileID: 0}
+ impactSpawnPoint: {fileID: 0}
--- !u!1001 &2112985138
PrefabInstance:
m_ObjectHideFlags: 0
@@ -227924,6 +227578,63 @@ GameObject:
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
+--- !u!1001 &374250731514228064
+PrefabInstance:
+ m_ObjectHideFlags: 0
+ serializedVersion: 2
+ m_Modification:
+ serializedVersion: 3
+ m_TransformParent: {fileID: 0}
+ m_Modifications:
+ - target: {fileID: 3799945176027843355, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
+ propertyPath: m_LocalPosition.x
+ value: 29.463804
+ objectReference: {fileID: 0}
+ - target: {fileID: 3799945176027843355, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
+ propertyPath: m_LocalPosition.y
+ value: 6.54
+ objectReference: {fileID: 0}
+ - target: {fileID: 3799945176027843355, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
+ propertyPath: m_LocalPosition.z
+ value: 37.02
+ objectReference: {fileID: 0}
+ - target: {fileID: 3799945176027843355, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
+ propertyPath: m_LocalRotation.w
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 3799945176027843355, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
+ propertyPath: m_LocalRotation.x
+ value: -0.00000005819078
+ objectReference: {fileID: 0}
+ - target: {fileID: 3799945176027843355, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
+ propertyPath: m_LocalRotation.y
+ value: -0.00000007397057
+ objectReference: {fileID: 0}
+ - target: {fileID: 3799945176027843355, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
+ propertyPath: m_LocalRotation.z
+ value: -0.000000045624613
+ objectReference: {fileID: 0}
+ - target: {fileID: 3799945176027843355, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3799945176027843355, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3799945176027843355, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.z
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 7767661558022019919, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
+ propertyPath: m_Name
+ value: SilverBall (1)
+ objectReference: {fileID: 0}
+ m_RemovedComponents: []
+ m_RemovedGameObjects: []
+ m_AddedGameObjects: []
+ m_AddedComponents: []
+ m_SourcePrefab: {fileID: 100100000, guid: bef6d4b496fcaf94ea9cd20c3ba24d78, type: 3}
--- !u!224 &510035933647342717
RectTransform:
m_ObjectHideFlags: 0
@@ -228755,6 +228466,10 @@ PrefabInstance:
serializedVersion: 3
m_TransformParent: {fileID: 0}
m_Modifications:
+ - target: {fileID: -903076858900737616, guid: c34da37720b95c84887eda34e2d90e5b, type: 3}
+ propertyPath: bossTransform
+ value:
+ objectReference: {fileID: 1231090172}
- target: {fileID: 265854990890279069, guid: c34da37720b95c84887eda34e2d90e5b, type: 3}
propertyPath: m_Name
value: Player
@@ -229245,6 +228960,235 @@ MonoBehaviour:
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
+--- !u!1001 &7311604695488881352
+PrefabInstance:
+ m_ObjectHideFlags: 0
+ serializedVersion: 2
+ m_Modification:
+ serializedVersion: 3
+ m_TransformParent: {fileID: 0}
+ m_Modifications:
+ - target: {fileID: 4766546730822684, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 944323988545650569, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 1649737928517973471, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 1906678252579283472, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 2349772954066482596, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 2368488738584937952, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 2632517516497060400, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 3140071916763302165, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 3214020809470585975, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 3917557170397923783, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 4051200230809419435, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 4148014776267224279, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 4295377773018066043, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 4531760650702335315, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 4742553587330777874, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 4895228312472489901, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 4919466100338403007, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_LocalPosition.x
+ value: 27.059
+ objectReference: {fileID: 0}
+ - target: {fileID: 4919466100338403007, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_LocalPosition.y
+ value: 5.48
+ objectReference: {fileID: 0}
+ - target: {fileID: 4919466100338403007, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_LocalPosition.z
+ value: 33.461
+ objectReference: {fileID: 0}
+ - target: {fileID: 4919466100338403007, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_LocalRotation.w
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 4919466100338403007, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_LocalRotation.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 4919466100338403007, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_LocalRotation.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 4919466100338403007, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_LocalRotation.z
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 4919466100338403007, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 4919466100338403007, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 4919466100338403007, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.z
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 5533461296662355806, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 5696671955299564549, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Name
+ value: BossMonster 3D model
+ objectReference: {fileID: 0}
+ - target: {fileID: 5696671955299564549, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 5862204985775830713, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 5992951552372829280, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 6230346118947057135, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 6491536537420366651, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 7102369222656777545, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: config
+ value:
+ objectReference: {fileID: 11400000, guid: a7ebef2d438cdef42a54a32e572e2421, type: 2}
+ - target: {fileID: 7102369222656777545, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: OnCounterActivated.m_PersistentCalls.m_Calls.Array.size
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 7102369222656777545, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: OnHabitChangeRewarded.m_PersistentCalls.m_Calls.Array.size
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 7102369222656777545, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: OnCounterActivated.m_PersistentCalls.m_Calls.Array.data[0].m_CallState
+ value: 2
+ objectReference: {fileID: 0}
+ - target: {fileID: 7102369222656777545, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: OnHabitChangeRewarded.m_PersistentCalls.m_Calls.Array.data[0].m_CallState
+ value: 2
+ objectReference: {fileID: 0}
+ - target: {fileID: 7230287829579350873, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 7377015375545003093, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 7511188995001642512, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 7515041959607683961, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 7607217621834580820, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 7779629221345539216, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 8151139788885434819, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 8207281218213573469, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 8370141904445535276, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 8424341814146284104, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 8431478448715994143, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 9002269045746146953, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 9056329910564997272, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ - target: {fileID: 9108673629704904850, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: ironBall
+ value:
+ objectReference: {fileID: 1760701545}
+ - target: {fileID: 9109463353651491833, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ propertyPath: m_Layer
+ value: 14
+ objectReference: {fileID: 0}
+ m_RemovedComponents: []
+ m_RemovedGameObjects:
+ - {fileID: 7803200391641535762, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ m_AddedGameObjects: []
+ m_AddedComponents:
+ - targetCorrespondingSourceObject: {fileID: 5696671955299564549, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
+ insertIndex: -1
+ addedObject: {fileID: 1231090178}
+ m_SourcePrefab: {fileID: 100100000, guid: fa6d549ce18d8bb479c3660d83e704d3, type: 3}
--- !u!1001 &7449230931284885269
PrefabInstance:
m_ObjectHideFlags: 0
@@ -229434,9 +229378,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 9eb5b341f7503494e9b0f88cc4b1c17d, type: 3}
m_Name:
m_EditorClassIdentifier:
- healthSource: {fileID: 1432447526}
- hpFillImage: {fileID: 4391954558244079021}
- hpText: {fileID: 7128939526209516821}
+ targetObject: {fileID: 0}
--- !u!224 &8997534549618499257
RectTransform:
m_ObjectHideFlags: 0
@@ -229489,6 +229431,7 @@ SceneRoots:
- {fileID: 321682814584122314}
- {fileID: 1824697940}
- {fileID: 85056390}
+ - {fileID: 374250731514228064}
- {fileID: 1175521096951328089}
- {fileID: 600231664}
- {fileID: 1927423250}
@@ -229499,6 +229442,6 @@ SceneRoots:
- {fileID: 198901434}
- {fileID: 929255795}
- {fileID: 1844125250}
- - {fileID: 1101415473}
+ - {fileID: 7311604695488881352}
- {fileID: 1081393079}
- {fileID: 48215575}
diff --git a/Assets/1.myPrefab/MyMonster/BossMonster 3D model.prefab b/Assets/1.myPrefab/MyMonster/BossMonster 3D model.prefab
new file mode 100644
index 00000000..d4cace76
--- /dev/null
+++ b/Assets/1.myPrefab/MyMonster/BossMonster 3D model.prefab
@@ -0,0 +1,284 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &1906678252579283472
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 6707239907687147596}
+ m_Layer: 12
+ m_Name: handPos
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &6707239907687147596
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1906678252579283472}
+ serializedVersion: 2
+ m_LocalRotation: {x: -0.038344428, y: 0.70765644, z: -0.0024401005, w: -0.7055112}
+ m_LocalPosition: {x: -0.035, y: 0.139, z: 0.001}
+ m_LocalScale: {x: 234.34999, y: 234.35004, z: 234.35002}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 6519631083874139804}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1001 &4886349692092432724
+PrefabInstance:
+ m_ObjectHideFlags: 0
+ serializedVersion: 2
+ m_Modification:
+ serializedVersion: 3
+ m_TransformParent: {fileID: 0}
+ m_Modifications:
+ - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ propertyPath: m_LocalScale.x
+ value: 1.8233726
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ propertyPath: m_LocalScale.y
+ value: 1.8233726
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ propertyPath: m_LocalScale.z
+ value: 1.8233726
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ propertyPath: m_LocalPosition.x
+ value: 27.059
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ propertyPath: m_LocalPosition.y
+ value: 5.48
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ propertyPath: m_LocalPosition.z
+ value: 33.461
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ propertyPath: m_LocalRotation.w
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ propertyPath: m_LocalRotation.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ propertyPath: m_LocalRotation.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ propertyPath: m_LocalRotation.z
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.z
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ propertyPath: m_Name
+ value: BossMonster 3D model
+ objectReference: {fileID: 0}
+ - target: {fileID: 5866666021909216657, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ propertyPath: m_Enabled
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 5866666021909216657, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ propertyPath: m_Controller
+ value:
+ objectReference: {fileID: 9100000, guid: b207531287111e74d890e730f7278dfa, type: 2}
+ - target: {fileID: 5866666021909216657, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ propertyPath: m_ApplyRootMotion
+ value: 0
+ objectReference: {fileID: 0}
+ m_RemovedComponents: []
+ m_RemovedGameObjects: []
+ m_AddedGameObjects:
+ - targetCorrespondingSourceObject: {fileID: 1852576806548013000, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ insertIndex: -1
+ addedObject: {fileID: 6707239907687147596}
+ m_AddedComponents:
+ - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ insertIndex: -1
+ addedObject: {fileID: 9077611284751873490}
+ - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ insertIndex: -1
+ addedObject: {fileID: 2677911960108179361}
+ - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ insertIndex: -1
+ addedObject: {fileID: 9108673629704904850}
+ - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ insertIndex: -1
+ addedObject: {fileID: 7102369222656777545}
+ - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ insertIndex: -1
+ addedObject: {fileID: 3377266925740425433}
+ m_SourcePrefab: {fileID: 100100000, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+--- !u!1 &5696671955299564549 stripped
+GameObject:
+ m_CorrespondingSourceObject: {fileID: 919132149155446097, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ m_PrefabInstance: {fileID: 4886349692092432724}
+ m_PrefabAsset: {fileID: 0}
+--- !u!195 &9077611284751873490
+NavMeshAgent:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5696671955299564549}
+ m_Enabled: 1
+ m_AgentTypeID: 0
+ m_Radius: 0.5
+ m_Speed: 3.5
+ m_Acceleration: 8
+ avoidancePriority: 50
+ m_AngularSpeed: 120
+ m_StoppingDistance: 0
+ m_AutoTraverseOffMeshLink: 1
+ m_AutoBraking: 1
+ m_AutoRepath: 1
+ m_Height: 2
+ m_BaseOffset: 0
+ m_WalkableMask: 4294967295
+ m_ObstacleAvoidanceType: 4
+--- !u!136 &2677911960108179361
+CapsuleCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5696671955299564549}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 2
+ m_Radius: 0.5
+ m_Height: 2.02
+ m_Direction: 1
+ m_Center: {x: 0, y: 0.9, z: 0}
+--- !u!114 &9108673629704904850
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5696671955299564549}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 04f39bf969074e6488f84b47bd517dc9, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ optimizationDistance: 40
+ maxHP: 100
+ attackDamage: 10
+ expReward: 10
+ moveSpeed: 3.5
+ myWeapon: {fileID: 0}
+ Monster_Idle: Monster_Idle
+ Monster_GetDamage: Monster_GetDamage
+ Monster_Die: Monster_Die
+ attackRestDuration: 1.5
+ hitSound: {fileID: 0}
+ deathSound: {fileID: 0}
+ deathEffectPrefab: {fileID: 0}
+ hitEffect: {fileID: 0}
+ impactSpawnPoint: {fileID: 0}
+ counterSystem: {fileID: 7102369222656777545}
+ patternInterval: 3
+ attackRange: 3
+ ironBall: {fileID: 0}
+ handHolder: {fileID: 6707239907687147596}
+ bossHealthBar: {fileID: 0}
+ anim_Roar: Roar
+ anim_Idle: Monster_Idle
+ anim_Walk: Monster_Walk
+ anim_Pickup: Skill_Pickup
+ anim_Throw: Attack_Throw
+ anim_DashReady: Skill_Dash_Ready
+ anim_DashGo: Skill_Dash_Go
+ anim_SmashCharge: Skill_Smash_Charge
+ anim_SmashImpact: Skill_Smash_Impact
+ anim_Sweep: Skill_Sweep
+ debugState: "\uB300\uAE30 \uC911"
+--- !u!114 &7102369222656777545
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5696671955299564549}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 5e36870a705f4fd44923bc5704c4a93b, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ config: {fileID: 11400000, guid: 4c6a2993f687f65409c4ece370872444, type: 2}
+ usePersistence: 1
+ OnCounterUpdated:
+ m_PersistentCalls:
+ m_Calls: []
+ OnCounterActivated:
+ m_PersistentCalls:
+ m_Calls: []
+ OnCounterDeactivated:
+ m_PersistentCalls:
+ m_Calls: []
+ OnHabitChangeRewarded:
+ m_PersistentCalls:
+ m_Calls: []
+--- !u!54 &3377266925740425433
+Rigidbody:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5696671955299564549}
+ serializedVersion: 4
+ m_Mass: 1
+ m_Drag: 0
+ m_AngularDrag: 0.05
+ m_CenterOfMass: {x: 0, y: 0, z: 0}
+ m_InertiaTensor: {x: 1, y: 1, z: 1}
+ m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ImplicitCom: 1
+ m_ImplicitTensor: 1
+ m_UseGravity: 1
+ m_IsKinematic: 1
+ m_Interpolate: 0
+ m_Constraints: 80
+ m_CollisionDetection: 0
+--- !u!4 &6519631083874139804 stripped
+Transform:
+ m_CorrespondingSourceObject: {fileID: 1852576806548013000, guid: 1cbe1e69b0d0be345be0872afa3e381e, type: 3}
+ m_PrefabInstance: {fileID: 4886349692092432724}
+ m_PrefabAsset: {fileID: 0}
diff --git a/Assets/1.myPrefab/MyMonster/BossMonster.prefab.meta b/Assets/1.myPrefab/MyMonster/BossMonster 3D model.prefab.meta
similarity index 74%
rename from Assets/1.myPrefab/MyMonster/BossMonster.prefab.meta
rename to Assets/1.myPrefab/MyMonster/BossMonster 3D model.prefab.meta
index 25b49637..d0940200 100644
--- a/Assets/1.myPrefab/MyMonster/BossMonster.prefab.meta
+++ b/Assets/1.myPrefab/MyMonster/BossMonster 3D model.prefab.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: d600b7cf8cd5668488dc657bfb881e96
+guid: fa6d549ce18d8bb479c3660d83e704d3
PrefabImporter:
externalObjects: {}
userData:
diff --git a/Assets/1.myPrefab/MyMonster/BossMonster.prefab b/Assets/1.myPrefab/MyMonster/BossMonster.prefab
deleted file mode 100644
index 70785df3..00000000
--- a/Assets/1.myPrefab/MyMonster/BossMonster.prefab
+++ /dev/null
@@ -1,245 +0,0 @@
-%YAML 1.1
-%TAG !u! tag:unity3d.com,2011:
---- !u!1001 &3382000909673495095
-PrefabInstance:
- m_ObjectHideFlags: 0
- serializedVersion: 2
- m_Modification:
- serializedVersion: 3
- m_TransformParent: {fileID: 0}
- m_Modifications:
- - target: {fileID: -8679921383154817045, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
- propertyPath: m_LocalPosition.x
- value: 28.749569
- objectReference: {fileID: 0}
- - target: {fileID: -8679921383154817045, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
- propertyPath: m_LocalPosition.y
- value: 5.692
- objectReference: {fileID: 0}
- - target: {fileID: -8679921383154817045, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
- propertyPath: m_LocalPosition.z
- value: 33.23644
- objectReference: {fileID: 0}
- - target: {fileID: -8679921383154817045, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
- propertyPath: m_LocalRotation.w
- value: 1
- objectReference: {fileID: 0}
- - target: {fileID: -8679921383154817045, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
- propertyPath: m_LocalRotation.x
- value: 0
- objectReference: {fileID: 0}
- - target: {fileID: -8679921383154817045, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
- propertyPath: m_LocalRotation.y
- value: 0
- objectReference: {fileID: 0}
- - target: {fileID: -8679921383154817045, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
- propertyPath: m_LocalRotation.z
- value: 0
- objectReference: {fileID: 0}
- - target: {fileID: -8679921383154817045, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
- propertyPath: m_LocalEulerAnglesHint.x
- value: 0
- objectReference: {fileID: 0}
- - target: {fileID: -8679921383154817045, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
- propertyPath: m_LocalEulerAnglesHint.y
- value: 0
- objectReference: {fileID: 0}
- - target: {fileID: -8679921383154817045, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
- propertyPath: m_LocalEulerAnglesHint.z
- value: 0
- objectReference: {fileID: 0}
- - target: {fileID: 919132149155446097, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
- propertyPath: m_Name
- value: Mesh_0
- objectReference: {fileID: 0}
- m_RemovedComponents: []
- m_RemovedGameObjects: []
- m_AddedGameObjects: []
- m_AddedComponents:
- - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
- insertIndex: -1
- addedObject: {fileID: 7262612340710800498}
- - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
- insertIndex: -1
- addedObject: {fileID: 4046718751856758141}
- - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
- insertIndex: -1
- addedObject: {fileID: 390961823920064902}
- - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
- insertIndex: -1
- addedObject: {fileID: 7314625792715952847}
- - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
- insertIndex: -1
- addedObject: {fileID: 4718319889433233023}
- - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
- insertIndex: -1
- addedObject: {fileID: 431623032249840988}
- m_SourcePrefab: {fileID: 100100000, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
---- !u!1 &2462956869760567142 stripped
-GameObject:
- m_CorrespondingSourceObject: {fileID: 919132149155446097, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
- m_PrefabInstance: {fileID: 3382000909673495095}
- m_PrefabAsset: {fileID: 0}
---- !u!95 &7262612340710800498
-Animator:
- serializedVersion: 5
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 2462956869760567142}
- m_Enabled: 1
- m_Avatar: {fileID: 9000000, guid: b0cd4fbfe1914384e8657f8debeddba0, type: 3}
- m_Controller: {fileID: 9100000, guid: b207531287111e74d890e730f7278dfa, type: 2}
- m_CullingMode: 0
- m_UpdateMode: 0
- m_ApplyRootMotion: 0
- m_LinearVelocityBlending: 0
- m_StabilizeFeet: 0
- m_WarningMessage:
- m_HasTransformHierarchy: 1
- m_AllowConstantClipSamplingOptimization: 1
- m_KeepAnimatorStateOnDisable: 0
- m_WriteDefaultValuesOnDisable: 0
---- !u!54 &4046718751856758141
-Rigidbody:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 2462956869760567142}
- serializedVersion: 4
- m_Mass: 1
- m_Drag: 0
- m_AngularDrag: 0.05
- m_CenterOfMass: {x: 0, y: 0, z: 0}
- m_InertiaTensor: {x: 1, y: 1, z: 1}
- m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1}
- m_IncludeLayers:
- serializedVersion: 2
- m_Bits: 0
- m_ExcludeLayers:
- serializedVersion: 2
- m_Bits: 0
- m_ImplicitCom: 1
- m_ImplicitTensor: 1
- m_UseGravity: 1
- m_IsKinematic: 0
- m_Interpolate: 0
- m_Constraints: 80
- m_CollisionDetection: 0
---- !u!195 &390961823920064902
-NavMeshAgent:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 2462956869760567142}
- m_Enabled: 1
- m_AgentTypeID: 0
- m_Radius: 0.5
- m_Speed: 3.5
- m_Acceleration: 8
- avoidancePriority: 50
- m_AngularSpeed: 120
- m_StoppingDistance: 0
- m_AutoTraverseOffMeshLink: 1
- m_AutoBraking: 1
- m_AutoRepath: 1
- m_Height: 2
- m_BaseOffset: 0
- m_WalkableMask: 4294967295
- m_ObstacleAvoidanceType: 4
---- !u!136 &7314625792715952847
-CapsuleCollider:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 2462956869760567142}
- m_Material: {fileID: 0}
- m_IncludeLayers:
- serializedVersion: 2
- m_Bits: 0
- m_ExcludeLayers:
- serializedVersion: 2
- m_Bits: 0
- m_LayerOverridePriority: 0
- m_IsTrigger: 0
- m_ProvidesContacts: 0
- m_Enabled: 1
- serializedVersion: 2
- m_Radius: 0.5
- m_Height: 2.12
- m_Direction: 1
- m_Center: {x: 0, y: 0.93, z: 0}
---- !u!114 &4718319889433233023
-MonoBehaviour:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 2462956869760567142}
- m_Enabled: 1
- m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: 04f39bf969074e6488f84b47bd517dc9, type: 3}
- m_Name:
- m_EditorClassIdentifier:
- optimizationDistance: 40
- maxHP: 100
- attackDamage: 10
- expReward: 10
- moveSpeed: 3.5
- myWeapon: {fileID: 0}
- Monster_Idle: Monster_Idle
- Monster_GetDamage: Monster_GetDamage
- Monster_Die: Monster_Die
- attackRestDuration: 1.5
- hitSound: {fileID: 0}
- deathSound: {fileID: 0}
- deathEffectPrefab: {fileID: 0}
- hitEffect: {fileID: 0}
- impactSpawnPoint: {fileID: 0}
- counterSystem: {fileID: 0}
- patternInterval: 3
- ironBall: {fileID: 0}
- handHolder: {fileID: 0}
- bossHealthBar: {fileID: 0}
---- !u!114 &431623032249840988
-MonoBehaviour:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 2462956869760567142}
- m_Enabled: 1
- m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: 5e36870a705f4fd44923bc5704c4a93b, type: 3}
- m_Name:
- m_EditorClassIdentifier:
- config: {fileID: 11400000, guid: 4c6a2993f687f65409c4ece370872444, type: 2}
- OnCounterActivated:
- m_PersistentCalls:
- m_Calls: []
- OnCounterDeactivated:
- m_PersistentCalls:
- m_Calls: []
- OnCounterPatternSelected:
- m_PersistentCalls:
- m_Calls: []
- OnHabitChangeRewarded:
- m_PersistentCalls:
- m_Calls: []
- bossPatterns:
- - patternName: Smash
- baseWeight: 30
- counterType: 1
- subCounterType: 0
- - patternName: DashAttack
- baseWeight: 30
- counterType: 2
- subCounterType: 0
- - patternName: ShieldWall
- baseWeight: 20
- counterType: 3
- subCounterType: 0
diff --git a/Assets/1.myPrefab/MyMonster/BossMonsterAnimater.controller b/Assets/1.myPrefab/MyMonster/BossMonsterAnimater.controller
index a9503ce2..3b3fe612 100644
--- a/Assets/1.myPrefab/MyMonster/BossMonsterAnimater.controller
+++ b/Assets/1.myPrefab/MyMonster/BossMonsterAnimater.controller
@@ -1,5 +1,31 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
+--- !u!1102 &-734848756713148517
+AnimatorState:
+ serializedVersion: 6
+ m_ObjectHideFlags: 1
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: Skill_Pickup
+ m_Speed: 1
+ m_CycleOffset: 0
+ m_Transitions: []
+ 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: 1827226128182048838, guid: db245789d27145b4abbed35593cbaad9, type: 3}
+ m_Tag:
+ m_SpeedParameter:
+ m_MirrorParameter:
+ m_CycleOffsetParameter:
+ m_TimeParameter:
--- !u!91 &9100000
AnimatorController:
m_ObjectHideFlags: 0
@@ -8,13 +34,7 @@ AnimatorController:
m_PrefabAsset: {fileID: 0}
m_Name: BossMonsterAnimater
serializedVersion: 5
- m_AnimatorParameters:
- - m_Name: Speed
- m_Type: 1
- m_DefaultFloat: 0
- m_DefaultInt: 0
- m_DefaultBool: 0
- m_Controller: {fileID: 9100000}
+ m_AnimatorParameters: []
m_AnimatorLayers:
- serializedVersion: 5
m_Name: Base Layer
@@ -285,7 +305,7 @@ AnimatorStateMachine:
m_Position: {x: 530, y: 500, z: 0}
- serializedVersion: 1
m_State: {fileID: 6315225014600423063}
- m_Position: {x: 310, y: 320, z: 0}
+ m_Position: {x: 300, y: 320, z: 0}
- serializedVersion: 1
m_State: {fileID: 2976953331190317660}
m_Position: {x: 530, y: 440, z: 0}
@@ -301,6 +321,9 @@ AnimatorStateMachine:
- serializedVersion: 1
m_State: {fileID: 2223377946697074049}
m_Position: {x: 320, y: 440, z: 0}
+ - serializedVersion: 1
+ m_State: {fileID: -734848756713148517}
+ m_Position: {x: 320, y: 550, z: 0}
m_ChildStateMachines: []
m_AnyStateTransitions: []
m_EntryTransitions: []
diff --git a/Assets/1.myPrefab/MyMonster/Monster Weapon/SilverBall (1).prefab b/Assets/1.myPrefab/MyMonster/Monster Weapon/SilverBall (1).prefab
new file mode 100644
index 00000000..71563a63
--- /dev/null
+++ b/Assets/1.myPrefab/MyMonster/Monster Weapon/SilverBall (1).prefab
@@ -0,0 +1,122 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &7767661558022019919
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 3799945176027843355}
+ - component: {fileID: 2802573740909485063}
+ - component: {fileID: 3397712377444290059}
+ - component: {fileID: 5299568709773687803}
+ - component: {fileID: 6858471677023048159}
+ m_Layer: 12
+ m_Name: SilverBall (1)
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &3799945176027843355
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 7767661558022019919}
+ serializedVersion: 2
+ m_LocalRotation: {x: -0.00000005819078, y: -0.00000007397057, z: -0.000000045624613, w: 1}
+ m_LocalPosition: {x: 29.463804, y: 6.54, z: 37.02}
+ m_LocalScale: {x: 2.35335, y: 2.35335, z: 2.35335}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!33 &2802573740909485063
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 7767661558022019919}
+ m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!23 &3397712377444290059
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 7767661558022019919}
+ m_Enabled: 1
+ m_CastShadows: 1
+ m_ReceiveShadows: 1
+ m_DynamicOccludee: 1
+ m_StaticShadowCaster: 0
+ m_MotionVectors: 1
+ m_LightProbeUsage: 1
+ m_ReflectionProbeUsage: 1
+ m_RayTracingMode: 2
+ m_RayTraceProcedural: 0
+ m_RenderingLayerMask: 1
+ m_RendererPriority: 0
+ m_Materials:
+ - {fileID: 2100000, guid: aea925dd0ba5758438a4e26ced28244e, type: 2}
+ m_StaticBatchInfo:
+ firstSubMesh: 0
+ subMeshCount: 0
+ m_StaticBatchRoot: {fileID: 0}
+ m_ProbeAnchor: {fileID: 0}
+ m_LightProbeVolumeOverride: {fileID: 0}
+ m_ScaleInLightmap: 1
+ m_ReceiveGI: 1
+ m_PreserveUVs: 0
+ m_IgnoreNormalsForChartDetection: 0
+ m_ImportantGI: 0
+ m_StitchLightmapSeams: 1
+ m_SelectedEditorRenderState: 3
+ m_MinimumChartSize: 4
+ m_AutoUVMaxDistance: 0.5
+ m_AutoUVMaxAngle: 89
+ m_LightmapParameters: {fileID: 0}
+ m_SortingLayerID: 0
+ m_SortingLayer: 0
+ m_SortingOrder: 0
+ m_AdditionalVertexStreams: {fileID: 0}
+--- !u!135 &5299568709773687803
+SphereCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 7767661558022019919}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 3
+ m_Radius: 0.5
+ m_Center: {x: 0, y: 0, z: 0}
+--- !u!114 &6858471677023048159
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 7767661558022019919}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 8aab89a77c422e841ab70b85a3a041eb, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ damage: 20
+ targetTag: Player
diff --git a/Assets/Scripts/Player/Combat/BossCounterConfig.asset.meta b/Assets/1.myPrefab/MyMonster/Monster Weapon/SilverBall (1).prefab.meta
similarity index 52%
rename from Assets/Scripts/Player/Combat/BossCounterConfig.asset.meta
rename to Assets/1.myPrefab/MyMonster/Monster Weapon/SilverBall (1).prefab.meta
index 11dabedf..5787054c 100644
--- a/Assets/Scripts/Player/Combat/BossCounterConfig.asset.meta
+++ b/Assets/1.myPrefab/MyMonster/Monster Weapon/SilverBall (1).prefab.meta
@@ -1,8 +1,7 @@
fileFormatVersion: 2
-guid: 4c6a2993f687f65409c4ece370872444
-NativeFormatImporter:
+guid: bef6d4b496fcaf94ea9cd20c3ba24d78
+PrefabImporter:
externalObjects: {}
- mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
diff --git a/Assets/5.Enemy Animation/BossAnimation'/박치기 애니메이션 2/박치기 애니메이션 2_Unreal5.5.fbx.meta b/Assets/5.Enemy Animation/BossAnimation'/박치기 애니메이션 2/박치기 애니메이션 2_Unreal5.5.fbx.meta
index 5d4c2c80..5b82809d 100644
--- a/Assets/5.Enemy Animation/BossAnimation'/박치기 애니메이션 2/박치기 애니메이션 2_Unreal5.5.fbx.meta
+++ b/Assets/5.Enemy Animation/BossAnimation'/박치기 애니메이션 2/박치기 애니메이션 2_Unreal5.5.fbx.meta
@@ -19,7 +19,11 @@ ModelImporter:
rigImportErrors:
rigImportWarnings:
animationImportErrors:
- animationImportWarnings:
+ animationImportWarnings: "\nClip 'Unreal Take' has import animation warnings
+ that might lower retargeting quality:\nNote: Activate translation DOF on avatar
+ to improve retargeting quality.\n\t'spine_04' is inbetween humanoid transforms
+ and has rotation animation that will be discarded.\n\t'spine_05' is inbetween
+ humanoid transforms and has rotation animation that will be discarded.\n"
animationRetargetingWarnings:
animationDoRetargetingWarnings: 0
importAnimatedCustomProperties: 0
@@ -31,7 +35,36 @@ ModelImporter:
animationWrapMode: 0
extraExposedTransformPaths: []
extraUserProperties: []
- clipAnimations: []
+ clipAnimations:
+ - serializedVersion: 16
+ name: Unreal Take
+ takeName: Unreal Take
+ internalID: 8993902097722301239
+ firstFrame: 0
+ lastFrame: 476
+ wrapMode: 0
+ orientationOffsetY: 0
+ level: 0
+ cycleOffset: 0
+ loop: 0
+ hasAdditiveReferencePose: 0
+ loopTime: 1
+ loopBlend: 0
+ loopBlendOrientation: 0
+ loopBlendPositionY: 0
+ loopBlendPositionXZ: 0
+ keepOriginalOrientation: 0
+ keepOriginalPositionY: 1
+ keepOriginalPositionXZ: 0
+ heightFromFeet: 0
+ mirror: 0
+ bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
+ curves: []
+ events: []
+ transformMask: []
+ maskType: 3
+ maskSource: {instanceID: 0}
+ additiveReferencePoseFrame: 0
isReadable: 0
meshes:
lODScreenPercentages: []
@@ -81,8 +114,1239 @@ ModelImporter:
importAnimation: 1
humanDescription:
serializedVersion: 3
- human: []
- skeleton: []
+ human:
+ - boneName: pelvis
+ humanName: Hips
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: thigh_l
+ humanName: LeftUpperLeg
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: thigh_r
+ humanName: RightUpperLeg
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: calf_l
+ humanName: LeftLowerLeg
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: calf_r
+ humanName: RightLowerLeg
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: foot_l
+ humanName: LeftFoot
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: foot_r
+ humanName: RightFoot
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: spine_01
+ humanName: Spine
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: spine_02
+ humanName: Chest
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: neck_01
+ humanName: Neck
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: head
+ humanName: Head
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: clavicle_l
+ humanName: LeftShoulder
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: clavicle_r
+ humanName: RightShoulder
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: upperarm_l
+ humanName: LeftUpperArm
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: upperarm_r
+ humanName: RightUpperArm
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: lowerarm_l
+ humanName: LeftLowerArm
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: lowerarm_r
+ humanName: RightLowerArm
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: hand_l
+ humanName: LeftHand
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: hand_r
+ humanName: RightHand
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: ball_l
+ humanName: LeftToes
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: ball_r
+ humanName: RightToes
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: thumb_01_l
+ humanName: Left Thumb Proximal
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: thumb_02_l
+ humanName: Left Thumb Intermediate
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: thumb_03_l
+ humanName: Left Thumb Distal
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: index_metacarpal_l
+ humanName: Left Index Proximal
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: index_01_l
+ humanName: Left Index Intermediate
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: index_02_l
+ humanName: Left Index Distal
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: middle_metacarpal_l
+ humanName: Left Middle Proximal
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: middle_01_l
+ humanName: Left Middle Intermediate
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: middle_02_l
+ humanName: Left Middle Distal
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: ring_metacarpal_l
+ humanName: Left Ring Proximal
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: ring_01_l
+ humanName: Left Ring Intermediate
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: ring_02_l
+ humanName: Left Ring Distal
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: pinky_metacarpal_l
+ humanName: Left Little Proximal
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: pinky_01_l
+ humanName: Left Little Intermediate
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: pinky_02_l
+ humanName: Left Little Distal
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: thumb_01_r
+ humanName: Right Thumb Proximal
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: thumb_02_r
+ humanName: Right Thumb Intermediate
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: thumb_03_r
+ humanName: Right Thumb Distal
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: index_metacarpal_r
+ humanName: Right Index Proximal
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: index_01_r
+ humanName: Right Index Intermediate
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: index_02_r
+ humanName: Right Index Distal
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: middle_metacarpal_r
+ humanName: Right Middle Proximal
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: middle_01_r
+ humanName: Right Middle Intermediate
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: middle_02_r
+ humanName: Right Middle Distal
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: ring_metacarpal_r
+ humanName: Right Ring Proximal
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: ring_01_r
+ humanName: Right Ring Intermediate
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: ring_02_r
+ humanName: Right Ring Distal
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: pinky_metacarpal_r
+ humanName: Right Little Proximal
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: pinky_01_r
+ humanName: Right Little Intermediate
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: pinky_02_r
+ humanName: Right Little Distal
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ - boneName: spine_03
+ humanName: UpperChest
+ limit:
+ min: {x: 0, y: 0, z: 0}
+ max: {x: 0, y: 0, z: 0}
+ value: {x: 0, y: 0, z: 0}
+ length: 0
+ modified: 0
+ skeleton:
+ - name: "\uBC15\uCE58\uAE30 \uC560\uB2C8\uBA54\uC774\uC158 2_Unreal5.5(Clone)"
+ parentName:
+ position: {x: 0, y: 0, z: 0}
+ rotation: {x: 0, y: 0, z: 0, w: 1}
+ scale: {x: 1, y: 1, z: 1}
+ - name: SKM_Quinn
+ parentName: "\uBC15\uCE58\uAE30 \uC560\uB2C8\uBA54\uC774\uC158 2_Unreal5.5(Clone)"
+ position: {x: -0, y: 0, z: 0}
+ rotation: {x: -0.7071068, y: 0, z: -0, w: 0.7071068}
+ scale: {x: 1, y: 1, z: 1}
+ - name: root
+ parentName: "\uBC15\uCE58\uAE30 \uC560\uB2C8\uBA54\uC774\uC158 2_Unreal5.5(Clone)"
+ position: {x: -0, y: 0, z: 0}
+ rotation: {x: -0.7071068, y: -0, z: -0, w: 0.7071067}
+ scale: {x: 1, y: 1.0000001, z: 1.0000001}
+ - name: pelvis
+ parentName: root
+ position: {x: -0.000011185143, y: 0.009869634, z: 1.0035745}
+ rotation: {x: 0.02222711, y: -0.70675737, z: 0.02222705, w: -0.7067574}
+ scale: {x: 1.0000005, y: 1.0000005, z: 1.0000001}
+ - name: spine_01
+ parentName: pelvis
+ position: {x: -0.02471853, y: 7.424998e-13, z: 4.440892e-18}
+ rotation: {x: -0.00000088475645, y: -0.00000023841858, z: -0.14993864, w: -0.9886954}
+ scale: {x: 1.0000001, y: 1.0000002, z: 1.0000002}
+ - name: spine_02
+ parentName: spine_01
+ position: {x: -0.049875133, y: -1.5134915e-12, z: 1.7763568e-17}
+ rotation: {x: -0.0000013418369, y: -0.00000012063312, z: -0.05952423, w: 0.9982269}
+ scale: {x: 1.0000004, y: 1.0000002, z: 1.0000004}
+ - name: spine_03
+ parentName: spine_02
+ position: {x: -0.07625883, y: -6.589662e-12, z: 8.881784e-18}
+ rotation: {x: -0.0000017168172, y: 0.00000011698852, z: 0.08994772, w: -0.99594647}
+ scale: {x: 0.9999998, y: 1, z: 0.9999998}
+ - name: spine_04
+ parentName: spine_03
+ position: {x: -0.08851115, y: -4.108312e-11, z: 1.7763567e-16}
+ rotation: {x: 0.0000017556838, y: -0.00000027544232, z: -0.07392207, w: 0.9972641}
+ scale: {x: 1.0000001, y: 0.99999994, z: 1.0000002}
+ - name: spine_05
+ parentName: spine_04
+ position: {x: -0.17498758, y: 2.151257e-11, z: 3.5527136e-17}
+ rotation: {x: -0.0000019174527, y: 0.00000006185966, z: -0.0022556556, w: 0.99999744}
+ scale: {x: 1.0000001, y: 1.0000001, z: 1}
+ - name: neck_01
+ parentName: spine_05
+ position: {x: -0.11915044, y: 1.5764966e-10, z: 7.283063e-16}
+ rotation: {x: -0.000000066546136, y: -0.00000043146113, z: -0.2175843, w: -0.97604156}
+ scale: {x: 1.0000005, y: 1.0000002, z: 1.0000004}
+ - name: neck_02
+ parentName: neck_01
+ position: {x: -0.058488358, y: -1.5143609e-10, z: 1.7763567e-16}
+ rotation: {x: -0.0000041762582, y: 0.000000026874055, z: -0.005270763, w: 0.9999861}
+ scale: {x: 1.0000002, y: 1, z: 1.0000001}
+ - name: head
+ parentName: neck_02
+ position: {x: -0.05758485, y: -4.5170717e-10, z: 1.8651746e-16}
+ rotation: {x: -0.0000030194774, y: -0.0000003748147, z: 0.10705546, w: -0.99425304}
+ scale: {x: 1.0000001, y: 1.0000006, z: 1}
+ - name: clavicle_l
+ parentName: spine_05
+ position: {x: -0.058308754, y: 0.010048158, z: -0.009313622}
+ rotation: {x: -0.08991266, y: 0.75046283, z: -0.059580162, w: -0.65205175}
+ scale: {x: 1.0000004, y: 1.0000006, z: 1.0000008}
+ - name: upperarm_l
+ parentName: clavicle_l
+ position: {x: -0.15286094, y: -6.24778e-16, z: 6.078471e-16}
+ rotation: {x: -0.034483388, y: 0.05042947, z: 0.016398646, w: 0.9979974}
+ scale: {x: 1.0000002, y: 0.99999994, z: 0.9999998}
+ - name: lowerarm_l
+ parentName: upperarm_l
+ position: {x: -0.27090353, y: -6.7390534e-16, z: -2.842171e-16}
+ rotation: {x: 0.0062392927, y: -0.018810716, z: -0.028457943, w: -0.9993985}
+ scale: {x: 1.0000002, y: 1, z: 1.0000001}
+ - name: lowerarm_twist_02_l
+ parentName: lowerarm_l
+ position: {x: -0.08698386, y: 2.8393953e-16, z: -1.1990408e-16}
+ rotation: {x: 0.0012477271, y: 0.0016747995, z: -0.0005854666, w: 0.9999977}
+ scale: {x: 1.0000002, y: 0.9999997, z: 1.0000001}
+ - name: lowerarm_twist_01_l
+ parentName: lowerarm_l
+ position: {x: -0.17396772, y: -7.771561e-17, z: 2.5757173e-16}
+ rotation: {x: 0.0012477955, y: 0.001674651, z: -0.00058546657, w: 0.99999774}
+ scale: {x: 1.0000001, y: 0.99999994, z: 1.0000001}
+ - name: lowerarm_correctiveRoot_l
+ parentName: lowerarm_l
+ position: {x: -4.0194363e-16, y: 7.083511e-17, z: -5.5783625e-18}
+ rotation: {x: 0.0012489959, y: 0.0021751288, z: -0.0000026225991, w: 0.99999684}
+ scale: {x: 1.0000001, y: 0.99999994, z: 1.0000001}
+ - name: lowerarm_in_l
+ parentName: lowerarm_correctiveRoot_l
+ position: {x: -0.014277789, y: 0.002633935, z: -0.02900858}
+ rotation: {x: -1, y: 6.139088e-11, z: 0.00000007939528, w: 0.0000000126890365}
+ scale: {x: 0.9999998, y: 1, z: 1.0000001}
+ - name: lowerarm_out_l
+ parentName: lowerarm_correctiveRoot_l
+ position: {x: -0.0062548737, y: 0.009754476, z: 0.01985306}
+ rotation: {x: -0, y: -2.3283059e-10, z: -0, w: 1}
+ scale: {x: 0.9999998, y: 1.0000001, z: 1.0000001}
+ - name: lowerarm_fwd_l
+ parentName: lowerarm_correctiveRoot_l
+ position: {x: -0.014256632, y: -0.027075524, z: 0.0048040296}
+ rotation: {x: -0.7071067, y: -0.000000050407824, z: 0.00000020586022, w: -0.7071068}
+ scale: {x: 1, y: 1, z: 0.99999994}
+ - name: lowerarm_bck_l
+ parentName: lowerarm_correctiveRoot_l
+ position: {x: -0.014871552, y: 0.035853036, z: -0.012619874}
+ rotation: {x: -0.7071068, y: 0.00000006717163, z: 0.00000014464578, w: 0.7071067}
+ scale: {x: 1, y: 1.0000001, z: 1.0000002}
+ - name: hand_l
+ parentName: lowerarm_l
+ position: {x: -0.26095158, y: 1.3433698e-16, z: 3.0198067e-16}
+ rotation: {x: -0.5928798, y: -0.044589598, z: -0.0718477, w: 0.8008391}
+ scale: {x: 1.0000004, y: 1.0000007, z: 1.0000006}
+ - name: middle_metacarpal_l
+ parentName: hand_l
+ position: {x: -0.031166257, y: -0.0006771632, z: -0.0036451041}
+ rotation: {x: 0.0007870494, y: 0.010219394, z: 0.05744393, w: 0.99829614}
+ scale: {x: 1.0000001, y: 1.0000004, z: 1.0000008}
+ - name: middle_01_l
+ parentName: middle_metacarpal_l
+ position: {x: -0.055605467, y: 4.565237e-15, z: 2.9043434e-15}
+ rotation: {x: -0.024070833, y: 0.0067156744, z: -0.032141432, w: 0.9991709}
+ scale: {x: 0.9999998, y: 0.9999998, z: 0.9999998}
+ - name: middle_02_l
+ parentName: middle_01_l
+ position: {x: -0.049196705, y: 5.0204282e-15, z: 3.1596946e-15}
+ rotation: {x: 0.00010059768, y: -0.00089236576, z: -0.035178054, w: 0.9993807}
+ scale: {x: 0.9999997, y: 1.0000004, z: 1.0000001}
+ - name: middle_03_l
+ parentName: middle_02_l
+ position: {x: -0.029020518, y: -6.37268e-16, z: -5.995204e-17}
+ rotation: {x: -0.00004748381, y: 0.0019094627, z: -0.024870792, w: 0.99968886}
+ scale: {x: 1.0000004, y: 0.99999994, z: 1}
+ - name: pinky_metacarpal_l
+ parentName: hand_l
+ position: {x: -0.029831223, y: 0.0024203737, z: 0.019275011}
+ rotation: {x: -0.2063544, y: 0.014052147, z: -0.011174254, w: 0.9783127}
+ scale: {x: 0.9999998, y: 1.0000005, z: 1.0000004}
+ - name: pinky_01_l
+ parentName: pinky_metacarpal_l
+ position: {x: -0.047179468, y: 1.5232259e-15, z: 5.107026e-15}
+ rotation: {x: 0.001316904, y: -0.003706346, z: -0.038287397, w: 0.99925905}
+ scale: {x: 0.9999998, y: 1.0000001, z: 1.0000002}
+ - name: pinky_02_l
+ parentName: pinky_01_l
+ position: {x: -0.028932974, y: 3.7592152e-15, z: 2.877698e-15}
+ rotation: {x: -0.0005951703, y: 0.00034444965, z: -0.03467951, w: 0.99939823}
+ scale: {x: 1.0000006, y: 1.0000001, z: 1.0000005}
+ - name: pinky_03_l
+ parentName: pinky_02_l
+ position: {x: -0.017915197, y: -1.1934898e-15, z: 8.1046276e-17}
+ rotation: {x: -0.000025395755, y: 0.00073224766, z: -0.028393017, w: 0.9995966}
+ scale: {x: 1.0000004, y: 1.0000001, z: 1.0000007}
+ - name: ring_metacarpal_l
+ parentName: hand_l
+ position: {x: -0.031086082, y: 0.0006031174, z: 0.008013592}
+ rotation: {x: -0.08976159, y: 0.018284941, z: 0.018743677, w: 0.99561906}
+ scale: {x: 1, y: 1.0000002, z: 1.0000007}
+ - name: ring_01_l
+ parentName: ring_metacarpal_l
+ position: {x: -0.049927615, y: 2.4202861e-15, z: 3.5949023e-15}
+ rotation: {x: -0.0068992856, y: -0.0013207357, z: -0.035714183, w: 0.99933743}
+ scale: {x: 1.0000002, y: 1.0000002, z: 1.0000004}
+ - name: ring_02_l
+ parentName: ring_01_l
+ position: {x: -0.042513885, y: 3.3217873e-15, z: 3.3462121e-15}
+ rotation: {x: -0.0005665683, y: -0.00054316915, z: -0.03194108, w: 0.99948955}
+ scale: {x: 0.99999994, y: 1.0000001, z: 1}
+ - name: ring_03_l
+ parentName: ring_02_l
+ position: {x: -0.03234828, y: -6.550316e-16, z: -2.5757173e-16}
+ rotation: {x: -0.00013445696, y: 0.0032156154, z: -0.04037307, w: 0.9991795}
+ scale: {x: 1.0000007, y: 1, z: 1.0000006}
+ - name: thumb_01_l
+ parentName: hand_l
+ position: {x: -0.023100065, y: 0.014519229, z: -0.025470773}
+ rotation: {x: -0.51424074, y: 0.31792796, z: -0.23781383, w: -0.7602124}
+ scale: {x: 1.0000001, y: 1, z: 1.0000005}
+ - name: thumb_02_l
+ parentName: thumb_01_l
+ position: {x: -0.04631789, y: 8.7707615e-17, z: 5.4178885e-16}
+ rotation: {x: 0.00049138034, y: 0.010405369, z: -0.032677762, w: 0.9994117}
+ scale: {x: 1.0000005, y: 1.0000008, z: 1.0000006}
+ - name: thumb_03_l
+ parentName: thumb_02_l
+ position: {x: -0.027105905, y: 3.524958e-16, z: -5.6732393e-16}
+ rotation: {x: 0.00014039228, y: -0.0017162551, z: -0.073276125, w: 0.9973102}
+ scale: {x: 1.0000001, y: 1.0000001, z: 1}
+ - name: index_metacarpal_l
+ parentName: hand_l
+ position: {x: -0.034526736, y: 0.0011279581, z: -0.020519018}
+ rotation: {x: 0.15400735, y: -0.043460015, z: 0.040791992, w: 0.98627025}
+ scale: {x: 1, y: 0.99999994, z: 1.0000006}
+ - name: index_01_l
+ parentName: index_metacarpal_l
+ position: {x: -0.053769063, y: 2.8066437e-15, z: 1.4699353e-15}
+ rotation: {x: -0.08578597, y: 0.010916823, z: -0.033191707, w: 0.9957007}
+ scale: {x: 1.0000001, y: 1.000001, z: 1.0000001}
+ - name: index_02_l
+ parentName: index_01_l
+ position: {x: -0.045645483, y: 4.8627768e-15, z: 2.9398705e-15}
+ rotation: {x: 0.0003460719, y: -0.0008319474, z: -0.03848945, w: 0.99925864}
+ scale: {x: 1.0000002, y: 1, z: 1.0000002}
+ - name: index_03_l
+ parentName: index_02_l
+ position: {x: -0.024864709, y: 2.0872193e-16, z: 2.5202062e-16}
+ rotation: {x: 0.0000000056752447, y: -0.0005250194, z: 0.00010770664, w: 0.9999999}
+ scale: {x: 1.0000004, y: 0.9999997, z: 1.0000001}
+ - name: wrist_inner_l
+ parentName: hand_l
+ position: {x: 0.0017634814, y: 0.016798155, z: -0.0028417164}
+ rotation: {x: -0.6848369, y: -0.051896557, z: -0.093080886, w: 0.7208614}
+ scale: {x: 0.99999994, y: 1.0000002, z: 1}
+ - name: wrist_outer_l
+ parentName: hand_l
+ position: {x: 0.00081474695, y: -0.016855558, z: -0.0031309773}
+ rotation: {x: -0.7208614, y: 0.09308068, z: -0.051896457, w: -0.68483686}
+ scale: {x: 1.0000001, y: 1.0000011, z: 1.0000002}
+ - name: weapon_l
+ parentName: hand_l
+ position: {x: -0.01073547, y: 0.014863692, z: 0.004871866}
+ rotation: {x: -0.5909328, y: -0.55624866, z: -0.078170665, w: -0.5790296}
+ scale: {x: 1.0000001, y: 1.0000001, z: 0.9999998}
+ - name: upperarm_twist_01_l
+ parentName: upperarm_l
+ position: {x: -0.09030118, y: -2.1704859e-16, z: -5.107026e-16}
+ rotation: {x: -0.000000055879347, y: 0.002088159, z: -0.0001194123, w: 0.9999978}
+ scale: {x: 1, y: 1.0000004, z: 1.0000002}
+ - name: upperarm_twistCor_01_l
+ parentName: upperarm_twist_01_l
+ position: {x: -1.5808415e-15, y: 9.6697314e-17, z: -4.4263766e-16}
+ rotation: {x: 0.00000008759634, y: 0.0020883472, z: -0.00011944793, w: 0.99999785}
+ scale: {x: 1.0000004, y: 1.0000001, z: 1.0000001}
+ - name: upperarm_twist_02_l
+ parentName: upperarm_l
+ position: {x: -0.18060236, y: -4.8849814e-16, z: -8.970602e-16}
+ rotation: {x: -0, y: -0, z: -0, w: 1}
+ scale: {x: 1, y: 1, z: 1.0000002}
+ - name: upperarm_tricep_l
+ parentName: upperarm_twist_02_l
+ position: {x: -0.0011777227, y: 0.047947254, z: -0.0014219055}
+ rotation: {x: 0.7363054, y: 0.011613336, z: 0.032473993, w: -0.67576987}
+ scale: {x: 1.0000001, y: 1.0000004, z: 1.000001}
+ - name: upperarm_bicep_l
+ parentName: upperarm_twist_02_l
+ position: {x: -0.0046326653, y: -0.032203976, z: -0.0035825723}
+ rotation: {x: 0.67576987, y: -0.032474093, z: 0.011613331, w: 0.73630536}
+ scale: {x: 1, y: 1.0000002, z: 1.0000002}
+ - name: upperarm_twistCor_02_l
+ parentName: upperarm_twist_02_l
+ position: {x: -7.5364444e-16, y: 1.4879345e-16, z: -6.211551e-17}
+ rotation: {x: -0.000000013038514, y: 0.0041764076, z: -0.0002388274, w: 0.9999913}
+ scale: {x: 1, y: 1.0000002, z: 0.9999998}
+ - name: upperarm_correctiveRoot_l
+ parentName: upperarm_l
+ position: {x: 5.024295e-16, y: 2.2537758e-17, z: -5.057598e-16}
+ rotation: {x: -0, y: -0, z: -0, w: 1}
+ scale: {x: 1, y: 1, z: 1.0000002}
+ - name: upperarm_bck_l
+ parentName: upperarm_correctiveRoot_l
+ position: {x: -0.01559413, y: 0.06354662, z: 0.0055722166}
+ rotation: {x: 0.58310604, y: -0.000000029802308, z: -0.000000024214376, w: -0.81239605}
+ scale: {x: 1.0000002, y: 1.0000001, z: 1.0000004}
+ - name: upperarm_fwd_l
+ parentName: upperarm_correctiveRoot_l
+ position: {x: -0.032174252, y: -0.06522155, z: -0.0059551536}
+ rotation: {x: 0.75705135, y: -0.06762322, z: -0.08010138, w: 0.64489084}
+ scale: {x: 1.0000001, y: 0.9999998, z: 1.0000001}
+ - name: upperarm_in_l
+ parentName: upperarm_correctiveRoot_l
+ position: {x: -0.059814952, y: -0.015915679, z: -0.046137996}
+ rotation: {x: 0.91285515, y: 0.000000117346616, z: -0.4082836, w: 0.00000001658918}
+ scale: {x: 1.0000002, y: 1.0000002, z: 0.9999998}
+ - name: upperarm_out_l
+ parentName: upperarm_correctiveRoot_l
+ position: {x: 0.001483597, y: 0.0028834047, z: 0.056798987}
+ rotation: {x: -0, y: -0, z: -0, w: 1}
+ scale: {x: 1.0000001, y: 1, z: 0.9999998}
+ - name: clavicle_out_l
+ parentName: clavicle_l
+ position: {x: -0.10794139, y: 0.0005109975, z: 0.054980624}
+ rotation: {x: -0.002589769, y: 0.02706095, z: 0.037471194, w: 0.99892795}
+ scale: {x: 0.99999994, y: 1, z: 0.99999994}
+ - name: clavicle_scap_l
+ parentName: clavicle_l
+ position: {x: -0.08872707, y: 0.061139088, z: -0.023973433}
+ rotation: {x: 0.027061066, y: 0.0025898665, z: 0.99892795, w: -0.037471183}
+ scale: {x: 0.9999998, y: 0.9999998, z: 0.9999998}
+ - name: clavicle_r
+ parentName: spine_05
+ position: {x: -0.058304302, y: 0.010049138, z: 0.009313574}
+ rotation: {x: -0.7504628, y: -0.089914106, z: -0.6520517, w: 0.05957855}
+ scale: {x: 1.0000004, y: 1.0000007, z: 1.0000008}
+ - name: upperarm_r
+ parentName: clavicle_r
+ position: {x: 0.15285988, y: -0.00000005468613, z: -0.000004020983}
+ rotation: {x: -0.03448297, y: 0.05042993, z: 0.016398046, w: 0.9979975}
+ scale: {x: 0.99999994, y: 0.99999946, z: 0.99999994}
+ - name: lowerarm_r
+ parentName: upperarm_r
+ position: {x: 0.27089924, y: 7.1609383e-16, z: 2.0605738e-14}
+ rotation: {x: -0.0062391507, y: 0.018810457, z: 0.02845723, w: 0.9993985}
+ scale: {x: 1.0000002, y: 0.99999994, z: 0.99999994}
+ - name: lowerarm_twist_02_r
+ parentName: lowerarm_r
+ position: {x: 0.0869851, y: -2.8366196e-16, z: -2.842171e-16}
+ rotation: {x: 0.0012478582, y: 0.0016748812, z: -0.0005856155, w: 0.99999774}
+ scale: {x: 1, y: 1.0000001, z: 1.0000001}
+ - name: lowerarm_twist_01_r
+ parentName: lowerarm_r
+ position: {x: 0.17397001, y: 5.551115e-19, z: 3.0198067e-16}
+ rotation: {x: 0.0012477591, y: 0.0016747889, z: -0.0005855262, w: 0.9999977}
+ scale: {x: 0.99999994, y: 1, z: 0.99999994}
+ - name: lowerarm_correctiveRoot_r
+ parentName: lowerarm_r
+ position: {x: 0.00000016877833, y: 0.00000022643009, z: -0.0000035186088}
+ rotation: {x: 0.0012490596, y: 0.0021753905, z: -0.0000026821813, w: 0.9999969}
+ scale: {x: 1.0000001, y: 0.99999994, z: 1.0000004}
+ - name: lowerarm_out_r
+ parentName: lowerarm_correctiveRoot_r
+ position: {x: 0.0066188704, y: -0.013741538, z: -0.022719068}
+ rotation: {x: -0, y: -0, z: 2.2737362e-13, w: 1}
+ scale: {x: 1, y: 1.0000002, z: 0.9999998}
+ - name: lowerarm_in_r
+ parentName: lowerarm_correctiveRoot_r
+ position: {x: 0.01664528, y: -0.0022968135, z: 0.024498712}
+ rotation: {x: 1, y: -0.000000029760251, z: 0.000000102715624, w: 0.0000000064539925}
+ scale: {x: 1, y: 1.0000004, z: 0.9999998}
+ - name: lowerarm_fwd_r
+ parentName: lowerarm_correctiveRoot_r
+ position: {x: 0.014935093, y: 0.02424894, z: -0.0060789287}
+ rotation: {x: 0.7071067, y: -0.00000016135162, z: 0.000000067756666, w: 0.707107}
+ scale: {x: 1, y: 0.99999946, z: 0.99999994}
+ - name: lowerarm_bck_r
+ parentName: lowerarm_correctiveRoot_r
+ position: {x: 0.017019121, y: -0.03649876, z: 0.009544169}
+ rotation: {x: -0.7071069, y: 0.00000009918584, z: -0.000000134317, w: 0.7071067}
+ scale: {x: 0.9999998, y: 0.9999996, z: 1}
+ - name: hand_r
+ parentName: lowerarm_r
+ position: {x: 0.26095495, y: -2.853273e-16, z: 1.4210854e-16}
+ rotation: {x: -0.5928787, y: -0.04459775, z: -0.071854755, w: 0.8008388}
+ scale: {x: 0.9999998, y: 1, z: 1.0000002}
+ - name: middle_metacarpal_r
+ parentName: hand_r
+ position: {x: 0.031166097, y: 0.0006771668, z: 0.0036420475}
+ rotation: {x: 0.0007851717, y: 0.010256985, z: 0.057465896, w: 0.9982945}
+ scale: {x: 1.0000002, y: 1.0000001, z: 1.0000004}
+ - name: middle_01_r
+ parentName: middle_metacarpal_r
+ position: {x: 0.055606164, y: 0.00000013257932, z: 0.0000004771214}
+ rotation: {x: -0.02407103, y: 0.0067105866, z: -0.032136038, w: 0.99917114}
+ scale: {x: 1, y: 1.0000007, z: 1}
+ - name: middle_02_r
+ parentName: middle_01_r
+ position: {x: 0.049196083, y: -0.0000003385173, z: -0.00000032841686}
+ rotation: {x: 0.00010126638, y: -0.00088801427, z: -0.035184123, w: 0.9993805}
+ scale: {x: 1.0000001, y: 1.0000002, z: 1}
+ - name: middle_03_r
+ parentName: middle_02_r
+ position: {x: 0.029020706, y: 0.00000012209041, z: 0.00000026500769}
+ rotation: {x: -0.00004768003, y: 0.0019092853, z: -0.02487078, w: 0.99968886}
+ scale: {x: 1.0000002, y: 1.0000005, z: 1}
+ - name: pinky_metacarpal_r
+ parentName: hand_r
+ position: {x: 0.029830864, y: -0.0024203244, z: -0.019277586}
+ rotation: {x: -0.20635283, y: 0.014098107, z: -0.011162405, w: 0.9783124}
+ scale: {x: 0.99999994, y: 1.0000002, z: 1.0000002}
+ - name: pinky_01_r
+ parentName: pinky_metacarpal_r
+ position: {x: 0.04717989, y: -0.0000004648216, z: -0.00000029418626}
+ rotation: {x: 0.0013162638, y: -0.0036984892, z: -0.03830453, w: 0.99925846}
+ scale: {x: 0.99999994, y: 1.0000002, z: 1}
+ - name: pinky_02_r
+ parentName: pinky_01_r
+ position: {x: 0.028933203, y: 0.00000032843099, z: 0.0000005785032}
+ rotation: {x: -0.00059504644, y: 0.00033197194, z: -0.034673627, w: 0.99939847}
+ scale: {x: 1.0000002, y: 1.0000006, z: 1.0000001}
+ - name: pinky_03_r
+ parentName: pinky_02_r
+ position: {x: 0.017914742, y: 0.00000018642146, z: -0.0000003445621}
+ rotation: {x: -0.00002534581, y: 0.00073218084, z: -0.028392894, w: 0.99959654}
+ scale: {x: 1.0000006, y: 1.0000007, z: 1.0000001}
+ - name: ring_metacarpal_r
+ parentName: hand_r
+ position: {x: 0.031086218, y: -0.0006036395, z: -0.008016386}
+ rotation: {x: -0.08976168, y: 0.018334245, z: 0.01874775, w: 0.995618}
+ scale: {x: 1, y: 1, z: 1.0000002}
+ - name: ring_01_r
+ parentName: ring_metacarpal_r
+ position: {x: 0.049928054, y: 0.000000588085, z: 0.0000002642582}
+ rotation: {x: -0.0068990565, y: -0.001325944, z: -0.035699807, w: 0.99933785}
+ scale: {x: 0.99999994, y: 1.0000007, z: 1.0000002}
+ - name: ring_02_r
+ parentName: ring_01_r
+ position: {x: 0.042513832, y: -0.000000828428, z: -0.00000020854412}
+ rotation: {x: -0.0005668815, y: -0.00053685886, z: -0.0319524, w: 0.9994892}
+ scale: {x: 1.0000002, y: 1.0000008, z: 1}
+ - name: ring_03_r
+ parentName: ring_02_r
+ position: {x: 0.032347318, y: 0.00000051180757, z: 0.00000033301117}
+ rotation: {x: -0.00013473853, y: 0.0032157172, z: -0.040373147, w: 0.9991795}
+ scale: {x: 1.0000004, y: 1.0000001, z: 1.0000002}
+ - name: thumb_01_r
+ parentName: hand_r
+ position: {x: 0.023100778, y: -0.014519234, z: 0.025468409}
+ rotation: {x: 0.51425165, y: -0.31789488, z: 0.23779587, w: 0.7602245}
+ scale: {x: 1.0000004, y: 0.99999994, z: 1.0000005}
+ - name: thumb_02_r
+ parentName: thumb_01_r
+ position: {x: 0.04631802, y: 0.00000016432628, z: 0.00000049452717}
+ rotation: {x: 0.0004893835, y: 0.010402448, z: -0.032673884, w: 0.9994118}
+ scale: {x: 1.0000002, y: 1.0000001, z: 1.0000005}
+ - name: thumb_03_r
+ parentName: thumb_02_r
+ position: {x: 0.02710575, y: -0.00000050043053, z: 0.00000007969171}
+ rotation: {x: 0.00014042067, y: -0.0017163894, z: -0.07327599, w: 0.9973102}
+ scale: {x: 1, y: 1.0000002, z: 1.0000005}
+ - name: index_metacarpal_r
+ parentName: hand_r
+ position: {x: 0.034526568, y: -0.0011278965, z: 0.020515975}
+ rotation: {x: 0.15400729, y: -0.043459874, z: 0.040791906, w: 0.9862703}
+ scale: {x: 1.0000001, y: 0.99999994, z: 1.0000005}
+ - name: index_01_r
+ parentName: index_metacarpal_r
+ position: {x: 0.053768992, y: -0.00000041306845, z: 0.0000004135864}
+ rotation: {x: -0.0857863, y: 0.010917289, z: -0.0332065, w: 0.99570024}
+ scale: {x: 1.0000001, y: 1.0000004, z: 0.9999997}
+ - name: index_02_r
+ parentName: index_01_r
+ position: {x: 0.045645688, y: 0.0000010120132, z: 0.0000002721985}
+ rotation: {x: 0.00034552038, y: -0.0008337789, z: -0.038482036, w: 0.99925894}
+ scale: {x: 1, y: 1.0000006, z: 1}
+ - name: index_03_r
+ parentName: index_02_r
+ position: {x: 0.024864469, y: -0.000000095848286, z: 0.00000005134596}
+ rotation: {x: -0.00000003530293, y: -0.0005251288, z: 0.00010778835, w: 0.9999999}
+ scale: {x: 1.0000005, y: 1.0000005, z: 1}
+ - name: wrist_inner_r
+ parentName: hand_r
+ position: {x: -0.0017632203, y: -0.016797837, z: 0.0028392773}
+ rotation: {x: -0.68483704, y: -0.051896375, z: -0.09308087, w: 0.7208613}
+ scale: {x: 0.99999994, y: 1, z: 0.99999994}
+ - name: wrist_outer_r
+ parentName: hand_r
+ position: {x: -0.0008143313, y: 0.016855745, z: 0.003128231}
+ rotation: {x: 0.7208613, y: -0.09308088, z: 0.051896255, w: 0.684837}
+ scale: {x: 1.0000002, y: 1.0000007, z: 1.000001}
+ - name: weapon_r
+ parentName: hand_r
+ position: {x: 0.0107356105, y: -0.014863772, z: -0.0048742844}
+ rotation: {x: -0.57902956, y: -0.07817065, z: 0.5562487, w: 0.5909328}
+ scale: {x: 1.0000004, y: 1.0000002, z: 0.99999946}
+ - name: upperarm_twist_01_r
+ parentName: upperarm_r
+ position: {x: 0.09029975, y: 0.00000010100946, z: -0.0000012584891}
+ rotation: {x: 0.00000001117587, y: 0.0020881293, z: -0.000119434655, w: 0.99999785}
+ scale: {x: 1.0000001, y: 1, z: 1}
+ - name: upperarm_twistCor_01_r
+ parentName: upperarm_twist_01_r
+ position: {x: -5.533129e-16, y: -1.4989135e-16, z: 1.6067846e-16}
+ rotation: {x: 0.000000014896706, y: 0.0020882573, z: -0.00011943981, w: 0.9999978}
+ scale: {x: 1.0000001, y: 1.0000002, z: 1.0000004}
+ - name: upperarm_twist_02_r
+ parentName: upperarm_r
+ position: {x: 0.1805995, y: 0.00000020201892, z: -0.0000025169782}
+ rotation: {x: -0, y: -0.000000029802319, z: -9.3132246e-10, w: 1}
+ scale: {x: 1, y: 0.99999994, z: 0.9999998}
+ - name: upperarm_tricep_r
+ parentName: upperarm_twist_02_r
+ position: {x: 0.0028466443, y: -0.047872346, z: -0.00066920987}
+ rotation: {x: -0.7363055, y: -0.011613376, z: -0.032474104, w: 0.67576975}
+ scale: {x: 1, y: 1.0000004, z: 1}
+ - name: upperarm_bicep_r
+ parentName: upperarm_twist_02_r
+ position: {x: 0.0061306674, y: 0.032275256, z: 0.0016008666}
+ rotation: {x: 0.6757698, y: -0.0324741, z: 0.011613397, w: 0.7363055}
+ scale: {x: 0.9999998, y: 1.0000005, z: 1.0000002}
+ - name: upperarm_twistCor_02_r
+ parentName: upperarm_twist_02_r
+ position: {x: 1.5072876e-15, y: -1.9906136e-16, z: 9.228503e-16}
+ rotation: {x: -0.000000029813325, y: 0.004176378, z: -0.00023883951, w: 0.9999913}
+ scale: {x: 0.9999998, y: 0.9999998, z: 0.9999998}
+ - name: upperarm_correctiveRoot_r
+ parentName: upperarm_r
+ position: {x: 5.275498e-16, y: 7.326533e-18, z: 1.0829913e-15}
+ rotation: {x: -0, y: -0.000000029802319, z: -9.3132246e-10, w: 1}
+ scale: {x: 1, y: 0.99999994, z: 0.9999998}
+ - name: upperarm_bck_r
+ parentName: upperarm_correctiveRoot_r
+ position: {x: 0.017315991, y: -0.06330215, z: -0.007334748}
+ rotation: {x: -0.58310604, y: 0.000000023668225, z: -0.000000019118374, w: 0.81239605}
+ scale: {x: 0.99999994, y: 1.0000001, z: 0.99999994}
+ - name: upperarm_in_r
+ parentName: upperarm_correctiveRoot_r
+ position: {x: 0.056036085, y: 0.013639714, z: 0.041672286}
+ rotation: {x: -0.9128551, y: -0.000000076287286, z: 0.40828386, w: -0.00000005841327}
+ scale: {x: 1.0000006, y: 0.9999998, z: 0.99999946}
+ - name: upperarm_fwd_r
+ parentName: upperarm_correctiveRoot_r
+ position: {x: 0.033673074, y: 0.06529402, z: 0.0039745467}
+ rotation: {x: 0.7570514, y: -0.06762327, z: -0.08010131, w: 0.6448907}
+ scale: {x: 1, y: 0.9999998, z: 0.99999994}
+ - name: upperarm_out_r
+ parentName: upperarm_correctiveRoot_r
+ position: {x: 0.00001522557, y: -0.0028120768, z: -0.0587804}
+ rotation: {x: -0, y: -0, z: -0, w: 1}
+ scale: {x: 0.99999994, y: 0.9999998, z: 0.9999998}
+ - name: clavicle_out_r
+ parentName: clavicle_r
+ position: {x: 0.11048164, y: 0.0018364313, z: -0.055069216}
+ rotation: {x: -0.0025899105, y: 0.027061073, z: 0.037471168, w: 0.99892795}
+ scale: {x: 0.99999994, y: 0.9999998, z: 1}
+ - name: clavicle_scap_r
+ parentName: clavicle_r
+ position: {x: 0.09116939, y: -0.061032064, z: 0.023641225}
+ rotation: {x: 0.02706113, y: 0.0025899147, z: 0.9989279, w: -0.037471168}
+ scale: {x: 1, y: 0.9999996, z: 0.9999998}
+ - name: clavicle_pec_r
+ parentName: spine_05
+ position: {x: 0.084378414, y: -0.10153745, z: 0.10180426}
+ rotation: {x: -0.72548264, y: -0.10850443, z: -0.6792962, w: 0.021410003}
+ scale: {x: 1.0000002, y: 1.0000006, z: 1.0000006}
+ - name: spine_04_latissimus_l
+ parentName: spine_05
+ position: {x: 0.083826855, y: 0.032562666, z: -0.12805118}
+ rotation: {x: -0.16731097, y: 0.7577242, z: 0.019259652, w: -0.63046813}
+ scale: {x: 1, y: 1.0000004, z: 1.0000005}
+ - name: spine_04_latissimus_r
+ parentName: spine_05
+ position: {x: 0.08388604, y: 0.032558173, z: 0.12816326}
+ rotation: {x: -0.7320417, y: -0.047045436, z: -0.67508256, w: 0.07851966}
+ scale: {x: 1, y: 1, z: 1.0000002}
+ - name: clavicle_pec_l
+ parentName: spine_05
+ position: {x: 0.08435377, y: -0.09910113, z: -0.0982911}
+ rotation: {x: -0.108505875, y: 0.7254653, z: -0.021406172, w: -0.6793146}
+ scale: {x: 1.0000002, y: 1.0000004, z: 1.0000005}
+ - name: thigh_r
+ parentName: pelvis
+ position: {x: 0.032320436, y: -0.0006799185, z: 0.111546}
+ rotation: {x: 0.023873633, y: -0.07294369, z: -0.99623996, w: 0.04019044}
+ scale: {x: 0.9999998, y: 1.0000001, z: 1.0000001}
+ - name: calf_r
+ parentName: thigh_r
+ position: {x: -0.45751938, y: 1.4654943e-16, z: 2.0605739e-15}
+ rotation: {x: -0.00000007997732, y: 0.000000035855916, z: 0.0095420545, w: 0.9999545}
+ scale: {x: 1.0000001, y: 1, z: 1.0000002}
+ - name: foot_r
+ parentName: calf_r
+ position: {x: -0.41705465, y: -8.881784e-18, z: 7.105427e-16}
+ rotation: {x: 0.000022594852, y: -0.022162396, z: -0.0009918192, w: 0.9997539}
+ scale: {x: 1.0000001, y: 1, z: 0.99999994}
+ - name: ball_r
+ parentName: foot_r
+ position: {x: -0.06536763, y: 0.13629231, z: -0.00043897645}
+ rotation: {x: -0.000000005529728, y: 0.000000009313226, z: 0.7071069, w: 0.70710677}
+ scale: {x: 0.9999998, y: 0.99999994, z: 1}
+ - name: ankle_bck_r
+ parentName: foot_r
+ position: {x: 0.007048862, y: -0.040763192, z: 0.0073197093}
+ rotation: {x: -0.70213395, y: -0.0055068186, z: 0.050049227, w: 0.7102625}
+ scale: {x: 1, y: 1, z: 1}
+ - name: ankle_fwd_r
+ parentName: foot_r
+ position: {x: 0.017609686, y: 0.0450347, z: -0.0034758465}
+ rotation: {x: 0.7102624, y: 0.050049186, z: 0.005506902, w: 0.702134}
+ scale: {x: 1.0000001, y: 1, z: 1.0000001}
+ - name: calf_twist_02_r
+ parentName: calf_r
+ position: {x: -0.13901822, y: -7.3274716e-17, z: 0.00049997185}
+ rotation: {x: 0.000046332727, y: 0.0024714228, z: -0.0009909213, w: 0.9999965}
+ scale: {x: 1.0000002, y: 1.0000001, z: 1}
+ - name: calf_twistCor_02_r
+ parentName: calf_twist_02_r
+ position: {x: -1.4366055e-16, y: 3.5133133e-17, z: 2.993174e-16}
+ rotation: {x: -0, y: -0, z: 4.4906294e-11, w: 1}
+ scale: {x: 1.0000002, y: 1, z: 1.0000002}
+ - name: calf_twist_01_r
+ parentName: calf_r
+ position: {x: -0.27803645, y: -5.7731595e-17, z: 0.0009999437}
+ rotation: {x: 0.000046332727, y: 0.0024714228, z: -0.0009909213, w: 0.9999965}
+ scale: {x: 1.0000002, y: 1.0000001, z: 1}
+ - name: calf_correctiveRoot_r
+ parentName: calf_r
+ position: {x: 1.4210853e-16, y: -4.7796345e-17, z: 2.500146e-17}
+ rotation: {x: 0.000048219972, y: 0.0017982362, z: -0.00000003897169, w: 0.99999845}
+ scale: {x: 1, y: 0.99999994, z: 1}
+ - name: calf_kneeBack_r
+ parentName: calf_correctiveRoot_r
+ position: {x: 0.0026281518, y: -0.052397516, z: -0.0033655467}
+ rotation: {x: -0.6933397, y: -0.012997749, z: 0.021528952, w: 0.7201719}
+ scale: {x: 1.0000001, y: 1, z: 1}
+ - name: calf_knee_r
+ parentName: calf_correctiveRoot_r
+ position: {x: 0.00048278633, y: 0.04618447, z: -0.0012729659}
+ rotation: {x: 0.72017187, y: 0.021528933, z: 0.0129976915, w: 0.69333977}
+ scale: {x: 1.0000001, y: 1, z: 1.0000001}
+ - name: thigh_twist_01_r
+ parentName: thigh_r
+ position: {x: -0.15250646, y: 5.551115e-17, z: 6.5725204e-16}
+ rotation: {x: -0.00000012962846, y: 0.002471855, z: -0.00046509248, w: 0.9999969}
+ scale: {x: 1.0000002, y: 1.0000001, z: 1.0000002}
+ - name: thigh_twistCor_01_r
+ parentName: thigh_twist_01_r
+ position: {x: -1.4218852e-16, y: -3.488578e-17, z: 2.237562e-17}
+ rotation: {x: -0, y: -0, z: 2.8060884e-13, w: 1}
+ scale: {x: 1.0000002, y: 1.0000001, z: 1.0000002}
+ - name: thigh_twist_02_r
+ parentName: thigh_r
+ position: {x: -0.3050129, y: 6.661338e-17, z: 1.3855583e-15}
+ rotation: {x: -0.00000012962846, y: 0.002471855, z: -0.00046509248, w: 0.9999969}
+ scale: {x: 1.0000002, y: 1.0000001, z: 1.0000002}
+ - name: thigh_twistCor_02_r
+ parentName: thigh_twist_02_r
+ position: {x: 5.6852105e-16, y: -2.5676307e-17, z: -1.1281973e-17}
+ rotation: {x: -0, y: -0, z: 2.8060884e-13, w: 1}
+ scale: {x: 1.0000002, y: 1.0000001, z: 1.0000002}
+ - name: thigh_correctiveRoot_r
+ parentName: thigh_r
+ position: {x: 1.421086e-16, y: -4.4401827e-18, z: -8.9123027e-17}
+ rotation: {x: -0, y: -0.0000000018626451, z: -0.0000000010477379, w: 1}
+ scale: {x: 0.99999994, y: 1.0000001, z: 1.0000001}
+ - name: thigh_fwd_r
+ parentName: thigh_correctiveRoot_r
+ position: {x: 0.06317815, y: 0.076825134, z: -0.00922383}
+ rotation: {x: 0.70710677, y: -0.00000001651029, z: -5.7622623e-10, w: 0.70710695}
+ scale: {x: 0.99999994, y: 0.9999998, z: 0.9999998}
+ - name: thigh_bck_r
+ parentName: thigh_correctiveRoot_r
+ position: {x: 0.038313203, y: -0.11172121, z: -0.02334635}
+ rotation: {x: -0.7071069, y: 0.00000002083249, z: -0.000000015637175, w: 0.70710677}
+ scale: {x: 0.99999994, y: 0.99999994, z: 0.99999994}
+ - name: thigh_out_r
+ parentName: thigh_correctiveRoot_r
+ position: {x: 0.058910087, y: -0.013259211, z: 0.048599437}
+ rotation: {x: -1, y: -0.00000001781154, z: -0.00000006332992, w: -0.000000081956365}
+ scale: {x: 1, y: 1.0000002, z: 1.0000004}
+ - name: thigh_in_r
+ parentName: thigh_correctiveRoot_r
+ position: {x: -0.10392979, y: 0.0078099295, z: -0.092181854}
+ rotation: {x: -0.000000065658234, y: 0.00000006286427, z: 0.000000032363456, w: 1}
+ scale: {x: 0.99999994, y: 1.0000001, z: 1.0000002}
+ - name: thigh_bck_lwr_r
+ parentName: thigh_correctiveRoot_r
+ position: {x: -0.06271104, y: -0.10753544, z: -0.01988474}
+ rotation: {x: -0.7071069, y: 0.00000002083249, z: -0.000000015637175, w: 0.70710677}
+ scale: {x: 0.99999994, y: 0.99999994, z: 0.99999994}
+ - name: thigh_fwd_lwr_r
+ parentName: thigh_correctiveRoot_r
+ position: {x: 0.0051321397, y: 0.07306521, z: -0.0082216365}
+ rotation: {x: 0.70710677, y: -0.00000001651029, z: -5.7622623e-10, w: 0.70710695}
+ scale: {x: 0.99999994, y: 0.9999998, z: 0.9999998}
+ - name: thigh_l
+ parentName: pelvis
+ position: {x: 0.032319915, y: -0.000680315, z: -0.11154585}
+ rotation: {x: -0.07294369, y: -0.023873683, z: -0.040190455, w: -0.99623996}
+ scale: {x: 1.0000001, y: 1.0000004, z: 1.0000004}
+ - name: calf_l
+ parentName: thigh_l
+ position: {x: 0.45752037, y: -1.3322676e-16, z: 0}
+ rotation: {x: -0.00000004470348, y: 0.00000001117587, z: 0.009541987, w: 0.9999545}
+ scale: {x: 0.99999994, y: 0.99999994, z: 0.9999998}
+ - name: foot_l
+ parentName: calf_l
+ position: {x: 0.4170542, y: -3.9968027e-17, z: 7.105427e-17}
+ rotation: {x: 0.000022589244, y: -0.022162236, z: -0.0009917999, w: 0.9997539}
+ scale: {x: 1.0000002, y: 1.0000002, z: 1.0000001}
+ - name: ball_l
+ parentName: foot_l
+ position: {x: 0.065367624, y: -0.1362923, z: 0.0004389023}
+ rotation: {x: -0.00000004998946, y: -0.00000010989605, z: 0.7071069, w: 0.70710677}
+ scale: {x: 0.9999997, y: 0.9999998, z: 0.9999998}
+ - name: ankle_bck_l
+ parentName: foot_l
+ position: {x: -0.0077887718, y: 0.03379726, z: -0.003502553}
+ rotation: {x: -0.75016344, y: -0.0019756504, z: 0.050311532, w: 0.6593328}
+ scale: {x: 1.0000004, y: 1, z: 1.0000004}
+ - name: ankle_fwd_l
+ parentName: foot_l
+ position: {x: -0.01391695, y: -0.040441476, z: -0.00232527}
+ rotation: {x: 0.65933275, y: 0.050311547, z: 0.001975655, w: 0.75016344}
+ scale: {x: 1.0000002, y: 1.0000001, z: 0.99999994}
+ - name: calf_twist_02_l
+ parentName: calf_l
+ position: {x: 0.13901806, y: -0.00000009072541, z: -0.0005004721}
+ rotation: {x: 0.000046420373, y: 0.0024714163, z: -0.0009909102, w: 0.9999965}
+ scale: {x: 1.0000002, y: 1.0000001, z: 1.0000001}
+ - name: calf_twistCor_02_l
+ parentName: calf_twist_02_l
+ position: {x: 5.681046e-16, y: -2.5574042e-17, z: 7.8570174e-17}
+ rotation: {x: -0, y: -0, z: -5.498179e-11, w: 1}
+ scale: {x: 1, y: 0.99999994, z: 1}
+ - name: calf_twist_01_l
+ parentName: calf_l
+ position: {x: 0.27803615, y: -0.00000009746015, z: -0.0010004657}
+ rotation: {x: 0.000046420373, y: 0.0024714163, z: -0.0009909102, w: 0.9999965}
+ scale: {x: 1.0000002, y: 1.0000001, z: 1.0000001}
+ - name: calf_correctiveRoot_l
+ parentName: calf_l
+ position: {x: -0.0000000014940447, y: -0.000000078276486, z: -0.00000044586872}
+ rotation: {x: 0.000048081103, y: 0.0017982639, z: -0.00000005859291, w: 0.99999845}
+ scale: {x: 1.0000004, y: 1.0000002, z: 1.0000001}
+ - name: calf_kneeBack_l
+ parentName: calf_correctiveRoot_l
+ position: {x: -0.0025936486, y: 0.052346658, z: 0.0031119525}
+ rotation: {x: -0.6933395, y: -0.012986026, z: 0.02152949, w: 0.72017235}
+ scale: {x: 1.0000002, y: 1.0000001, z: 1.0000006}
+ - name: calf_knee_l
+ parentName: calf_correctiveRoot_l
+ position: {x: -0.00045141357, y: -0.046220325, z: 0.0012432837}
+ rotation: {x: 0.72017235, y: 0.021529516, z: 0.012985959, w: 0.6933395}
+ scale: {x: 1.0000001, y: 1.0000001, z: 1}
+ - name: thigh_twist_01_l
+ parentName: thigh_l
+ position: {x: 0.15250678, y: -0.000000028001988, z: -0.00000015947238}
+ rotation: {x: -0.000000059604645, y: 0.0024719294, z: -0.00046513823, w: 0.9999969}
+ scale: {x: 0.99999994, y: 1, z: 0.9999998}
+ - name: thigh_twistCor_01_l
+ parentName: thigh_twist_01_l
+ position: {x: 2.8376967e-16, y: 3.109513e-19, z: 9.117463e-17}
+ rotation: {x: -0, y: -0, z: 1.8193223e-12, w: 1}
+ scale: {x: 1, y: 1, z: 1}
+ - name: thigh_twist_02_l
+ parentName: thigh_l
+ position: {x: 0.30501357, y: -0.000000056003977, z: -0.00000031894476}
+ rotation: {x: -0.000000059604645, y: 0.0024719294, z: -0.00046513823, w: 0.9999969}
+ scale: {x: 0.99999994, y: 1, z: 0.9999998}
+ - name: thigh_twistCor_02_l
+ parentName: thigh_twist_02_l
+ position: {x: 1.4237393e-16, y: -4.3440742e-18, z: -5.250355e-17}
+ rotation: {x: -0, y: -0, z: 1.8193223e-12, w: 1}
+ scale: {x: 1, y: 1, z: 1}
+ - name: thigh_correctiveRoot_l
+ parentName: thigh_l
+ position: {x: 8.5265126e-16, y: -8.685708e-18, z: 3.7191112e-17}
+ rotation: {x: -0, y: -0, z: 5.820765e-10, w: 1}
+ scale: {x: 1, y: 1.0000001, z: 1}
+ - name: thigh_bck_l
+ parentName: thigh_correctiveRoot_l
+ position: {x: -0.038295336, y: 0.11165069, z: 0.023064828}
+ rotation: {x: -0.7071068, y: 0.0000001252088, z: 0.000000026072891, w: 0.70710677}
+ scale: {x: 0.9999997, y: 0.9999997, z: 0.9999997}
+ - name: thigh_fwd_l
+ parentName: thigh_correctiveRoot_l
+ position: {x: -0.06322477, y: -0.07665837, z: 0.009260224}
+ rotation: {x: 0.7071068, y: -0.000000008793492, z: -0.000000045347896, w: 0.7071068}
+ scale: {x: 0.99999994, y: 0.9999996, z: 0.9999997}
+ - name: thigh_out_l
+ parentName: thigh_correctiveRoot_l
+ position: {x: -0.058887012, y: 0.013106737, z: -0.048734512}
+ rotation: {x: -1, y: 0.00000007043126, z: 0.00000009848735, w: 0.00000007293419}
+ scale: {x: 0.99999994, y: 1, z: 0.99999994}
+ - name: thigh_bck_lwr_l
+ parentName: thigh_correctiveRoot_l
+ position: {x: 0.06006294, y: 0.106731765, z: 0.015781237}
+ rotation: {x: -0.7071068, y: 0.0000001252088, z: 0.000000026072891, w: 0.70710677}
+ scale: {x: 0.9999997, y: 0.9999997, z: 0.9999997}
+ - name: thigh_in_l
+ parentName: thigh_correctiveRoot_l
+ position: {x: 0.10327433, y: -0.008447497, z: 0.09194447}
+ rotation: {x: -0, y: -0, z: -0, w: 1}
+ scale: {x: 1, y: 1.0000001, z: 1}
+ - name: thigh_fwd_lwr_l
+ parentName: thigh_correctiveRoot_l
+ position: {x: -0.0042718383, y: -0.07843591, z: 0.0073839184}
+ rotation: {x: 0.7071068, y: -0.000000008793492, z: -0.000000045347896, w: 0.7071068}
+ scale: {x: 0.99999994, y: 0.9999996, z: 0.9999997}
+ - name: ik_foot_root
+ parentName: root
+ position: {x: -0, y: 0, z: 0}
+ rotation: {x: -0, y: -0, z: -0, w: 1}
+ scale: {x: 1, y: 1.0000004, z: 1.0000004}
+ - name: ik_foot_l
+ parentName: ik_foot_root
+ position: {x: -0.14711809, y: -0.0004145237, z: 0.08143789}
+ rotation: {x: 0.035699368, y: 0.70451885, z: -0.028369874, w: 0.7082188}
+ scale: {x: 1.0000006, y: 1.0000005, z: 1.0000002}
+ - name: ik_foot_r
+ parentName: ik_foot_root
+ position: {x: 0.1471183, y: -0.00041416238, z: 0.08143787}
+ rotation: {x: 0.7082188, y: 0.028369665, z: 0.7045189, w: -0.03569925}
+ scale: {x: 1.0000006, y: 1.0000006, z: 1.0000004}
+ - name: ik_hand_root
+ parentName: root
+ position: {x: -0, y: 0, z: 0}
+ rotation: {x: -0, y: -0, z: -0, w: 1}
+ scale: {x: 1, y: 1.0000004, z: 1.0000004}
+ - name: ik_hand_gun
+ parentName: ik_hand_root
+ position: {x: 0.45554888, y: -0.1440057, z: 1.056407}
+ rotation: {x: 0.60836744, y: 0.17933683, z: -0.46125895, w: 0.62045753}
+ scale: {x: 1.0000005, y: 1.0000001, z: 1.0000001}
+ - name: ik_hand_l
+ parentName: ik_hand_gun
+ position: {x: -0.4648035, y: -0.7203053, z: 0.30857638}
+ rotation: {x: 0.5101571, y: 0.79058844, z: -0.3386882, w: 0.00000029057264}
+ scale: {x: 0.99999994, y: 1.0000004, z: 1.0000001}
+ - name: ik_hand_r
+ parentName: ik_hand_gun
+ position: {x: -0, y: 0, z: 0}
+ rotation: {x: -0, y: -0, z: 0.000000007450581, w: 1}
+ scale: {x: 1.0000002, y: 1.0000001, z: 1}
+ - name: interaction
+ parentName: root
+ position: {x: 1.7763568e-17, y: 1.8735012e-18, z: 3.5527136e-17}
+ rotation: {x: -0, y: 2.5275457e-17, z: 1.3877785e-17, w: 1}
+ scale: {x: 1, y: 1.0000004, z: 1.0000004}
+ - name: center_of_mass
+ parentName: root
+ position: {x: 1.7763568e-17, y: 1.8735012e-18, z: 3.5527136e-17}
+ rotation: {x: -0, y: 2.5275457e-17, z: 1.3877785e-17, w: 1}
+ scale: {x: 1, y: 1.0000004, z: 1.0000004}
armTwist: 0.5
foreArmTwist: 0.5
upperLegTwist: 0.5
diff --git a/Assets/5.Enemy Animation/BossAnimation'/줍는모션 3_Unreal5.6.fbx b/Assets/5.Enemy Animation/BossAnimation'/줍는모션 3_Unreal5.6.fbx
new file mode 100644
index 00000000..c3fcec1e
Binary files /dev/null and b/Assets/5.Enemy Animation/BossAnimation'/줍는모션 3_Unreal5.6.fbx differ
diff --git a/Assets/5.Enemy Animation/BossAnimation'/줍는모션 3_Unreal5.6.fbx.meta b/Assets/5.Enemy Animation/BossAnimation'/줍는모션 3_Unreal5.6.fbx.meta
new file mode 100644
index 00000000..587ea5bc
--- /dev/null
+++ b/Assets/5.Enemy Animation/BossAnimation'/줍는모션 3_Unreal5.6.fbx.meta
@@ -0,0 +1,113 @@
+fileFormatVersion: 2
+guid: db245789d27145b4abbed35593cbaad9
+ModelImporter:
+ serializedVersion: 22200
+ internalIDToNameTable: []
+ externalObjects: {}
+ materials:
+ materialImportMode: 2
+ materialName: 0
+ materialSearch: 1
+ materialLocation: 1
+ animations:
+ legacyGenerateAnimations: 4
+ bakeSimulation: 0
+ resampleCurves: 1
+ optimizeGameObjects: 0
+ removeConstantScaleCurves: 0
+ motionNodeName:
+ rigImportErrors:
+ rigImportWarnings:
+ animationImportErrors:
+ animationImportWarnings:
+ animationRetargetingWarnings:
+ animationDoRetargetingWarnings: 0
+ importAnimatedCustomProperties: 0
+ importConstraints: 0
+ animationCompression: 3
+ animationRotationError: 0.5
+ animationPositionError: 0.5
+ animationScaleError: 0.5
+ animationWrapMode: 0
+ extraExposedTransformPaths: []
+ extraUserProperties: []
+ clipAnimations: []
+ isReadable: 0
+ meshes:
+ lODScreenPercentages:
+ - 0.25
+ - 0.125
+ - 0.0625
+ - 0.01
+ globalScale: 1
+ meshCompression: 0
+ addColliders: 0
+ useSRGBMaterialColor: 1
+ sortHierarchyByName: 1
+ importPhysicalCameras: 1
+ importVisibility: 1
+ importBlendShapes: 1
+ importCameras: 1
+ importLights: 1
+ nodeNameCollisionStrategy: 1
+ fileIdsGeneration: 2
+ swapUVChannels: 0
+ generateSecondaryUV: 0
+ useFileUnits: 1
+ keepQuads: 0
+ weldVertices: 1
+ bakeAxisConversion: 0
+ preserveHierarchy: 0
+ skinWeightsMode: 0
+ maxBonesPerVertex: 4
+ minBoneWeight: 0.001
+ optimizeBones: 1
+ meshOptimizationFlags: -1
+ indexFormat: 0
+ secondaryUVAngleDistortion: 8
+ secondaryUVAreaDistortion: 15.000001
+ secondaryUVHardAngle: 88
+ secondaryUVMarginMethod: 1
+ secondaryUVMinLightmapResolution: 40
+ secondaryUVMinObjectScale: 1
+ secondaryUVPackMargin: 4
+ useFileScale: 1
+ strictVertexDataChecks: 0
+ tangentSpace:
+ normalSmoothAngle: 60
+ normalImportMode: 0
+ tangentImportMode: 3
+ normalCalculationMode: 4
+ legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0
+ blendShapeNormalImportMode: 1
+ normalSmoothingSource: 0
+ referencedClips: []
+ importAnimation: 1
+ humanDescription:
+ serializedVersion: 3
+ human: []
+ skeleton: []
+ armTwist: 0.5
+ foreArmTwist: 0.5
+ upperLegTwist: 0.5
+ legTwist: 0.5
+ armStretch: 0.05
+ legStretch: 0.05
+ feetSpacing: 0
+ globalScale: 1
+ rootMotionBoneName:
+ hasTranslationDoF: 0
+ hasExtraRoot: 0
+ skeletonHasParents: 1
+ lastHumanDescriptionAvatarSource: {instanceID: 0}
+ autoGenerateAvatarMappingIfUnspecified: 1
+ animationType: 3
+ humanoidOversampling: 1
+ avatarSetup: 1
+ addHumanoidExtraRootOnlyWhenUsingAvatar: 1
+ importBlendShapeDeformPercent: 1
+ remapMaterialsIfMaterialImportModeIsNone: 0
+ additionalBone: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Core/Utils.meta b/Assets/Scripts/Core/Utils.meta
new file mode 100644
index 00000000..b66dd210
--- /dev/null
+++ b/Assets/Scripts/Core/Utils.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 1f15c116f1cf57042a361cf23f1fb1d3
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Core/Utils/AnimationLengthCach.cs b/Assets/Scripts/Core/Utils/AnimationLengthCach.cs
new file mode 100644
index 00000000..74e0f9c5
--- /dev/null
+++ b/Assets/Scripts/Core/Utils/AnimationLengthCach.cs
@@ -0,0 +1,41 @@
+using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
+using System.Collections.Generic; // 딕셔너리를 사용할거에요 -> System.Collections.Generic을
+
+///
+/// 애니메이션 클립 길이를 런타임에 캐싱하고 조회하는 유틸리티
+///
+public class AnimationLengthCache // 클래스를 선언할거에요 -> 애니메이션 길이 캐싱을 담당하는 AnimationLengthCache를
+{
+ private readonly Animator _animator; // 변수를 선언할거에요 -> 애니메이터 참조를 저장할 _animator를
+ private readonly Dictionary _clipLengthCache = new Dictionary(); // 변수를 선언할거에요 -> 클립 이름과 길이를 저장할 캐시를
+
+ public AnimationLengthCache(Animator animator) // 생성자를 만들거에요 -> 애니메이터를 받는
+ {
+ _animator = animator; // 값을 저장할거에요 -> 전달받은 애니메이터를
+ CacheAllClipLengths(); // 함수를 실행할거에요 -> 모든 클립 길이를 미리 저장하는
+ }
+
+ private void CacheAllClipLengths() // 함수를 선언할거에요 -> 초기 캐싱 로직을
+ {
+ _clipLengthCache.Clear(); // 비울거에요 -> 기존 데이터를
+ if (_animator == null || _animator.runtimeAnimatorController == null) return; // 조건이 맞으면 중단할거에요 -> 애니메이터가 없으면
+
+ foreach (AnimationClip clip in _animator.runtimeAnimatorController.animationClips) // 반복할거에요 -> 모든 클립을
+ {
+ if (!_clipLengthCache.ContainsKey(clip.name)) // 조건이 맞으면 실행할거에요 -> 캐시에 없는 이름이라면
+ _clipLengthCache[clip.name] = clip.length; // 값을 저장할거에요 -> 길이를
+ }
+ }
+
+ public float GetClipLength(string clipName, float fallback = 1.0f) // 함수를 선언할거에요 -> 길이를 조회하는 GetClipLength를
+ {
+ if (_clipLengthCache.TryGetValue(clipName, out float length)) return length; // 조건이 맞으면 반환할거에요 -> 캐시에서 찾은 길이를
+
+ if (_animator != null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면 (비상용)
+ {
+ AnimatorStateInfo state = _animator.GetCurrentAnimatorStateInfo(0); // 정보를 가져올거에요 -> 현재 상태를
+ if (state.IsName(clipName)) return state.length; // 조건이 맞으면 반환할거에요 -> 현재 상태의 길이를
+ }
+ return fallback; // 반환할거에요 -> 기본값을 (못 찾았을 때)
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Core/Utils/AnimationLengthCach.cs.meta b/Assets/Scripts/Core/Utils/AnimationLengthCach.cs.meta
new file mode 100644
index 00000000..0ee776e7
--- /dev/null
+++ b/Assets/Scripts/Core/Utils/AnimationLengthCach.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a03df9f7a6ef3b549a573abc7664a8af
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Core/Utils/ProjectileMath.cs b/Assets/Scripts/Core/Utils/ProjectileMath.cs
new file mode 100644
index 00000000..642b82e7
--- /dev/null
+++ b/Assets/Scripts/Core/Utils/ProjectileMath.cs
@@ -0,0 +1,24 @@
+using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
+
+public static class ProjectileMath // 정적 클래스를 선언할거에요 -> 수학 계산용 ProjectileMath를
+{
+ public static Vector3 CalculateLobVelocity(Vector3 origin, Vector3 target, float angle) // 함수를 선언할거에요 -> 곡사 속도를 계산하는
+ {
+ Vector3 dir = target - origin; // 벡터를 계산할거에요 -> 방향과 거리를
+ float height = dir.y; // 값을 저장할거에요 -> 높이 차이를
+ dir.y = 0; // 값을 초기화할거에요 -> 수평 거리 계산용으로 y를 0으로
+ float dist = dir.magnitude; // 값을 계산할거에요 -> 수평 거리를
+
+ float a = angle * Mathf.Deg2Rad; // 값을 변환할거에요 -> 각도를 라디안으로
+ dir.y = dist * Mathf.Tan(a); // 값을 계산할거에요 -> 각도에 따른 높이를
+ dist += height / Mathf.Tan(a); // 값을 보정할거에요 -> 높이 차이에 따른 거리 보정을
+
+ float gravity = Physics.gravity.magnitude; // 값을 가져올거에요 -> 중력 가속도를
+ float velocitySq = (gravity * dist * dist) / (2 * Mathf.Pow(Mathf.Cos(a), 2) * (dist * Mathf.Tan(a) - height)); // 값을 계산할거에요 -> 필요한 속도의 제곱을
+
+ if (velocitySq <= 0) return Vector3.zero; // 조건이 맞으면 반환할거에요 -> 계산 불가시 0을
+
+ float velocity = Mathf.Sqrt(velocitySq); // 값을 계산할거에요 -> 제곱근을 구해 실제 속도를
+ return dir.normalized * velocity; // 값을 반환할거에요 -> 방향에 속도를 곱해서
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Core/Utils/ProjectileMath.cs.meta b/Assets/Scripts/Core/Utils/ProjectileMath.cs.meta
new file mode 100644
index 00000000..4423593f
--- /dev/null
+++ b/Assets/Scripts/Core/Utils/ProjectileMath.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b58ac9c02288aaf4698a6e64e77cdd53
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Core/Utils/StatusEffectProcessor.cs b/Assets/Scripts/Core/Utils/StatusEffectProcessor.cs
new file mode 100644
index 00000000..9dda896c
--- /dev/null
+++ b/Assets/Scripts/Core/Utils/StatusEffectProcessor.cs
@@ -0,0 +1,55 @@
+using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
+using UnityEngine.AI; // 내비게이션 기능을 불러올거에요 -> UnityEngine.AI를
+using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를
+
+///
+/// 화상, 독, 슬로우 등의 상태이상을 전담 처리하는 클래스
+///
+public class StatusEffectProcessor // 클래스를 선언할거에요 -> 상태이상 로직을 담당하는 클래스를
+{
+ private readonly MonoBehaviour _owner; // 변수를 선언할거에요 -> 코루틴 실행 주체를
+ private readonly IDamageable _damageable; // 변수를 선언할거에요 -> 데미지 인터페이스를
+ private readonly NavMeshAgent _agent; // 변수를 선언할거에요 -> 이동 에이전트를
+ private readonly Animator _animator; // 변수를 선언할거에요 -> 애니메이터를
+
+ public StatusEffectProcessor(MonoBehaviour owner, IDamageable damageable, NavMeshAgent agent, Animator animator) // 생성자를 만들거에요 -> 의존성 주입을 위한
+ {
+ _owner = owner; // 값을 저장할거에요 -> 주인을
+ _damageable = damageable; // 값을 저장할거에요 -> 데미지 대상을
+ _agent = agent; // 값을 저장할거에요 -> 에이전트를
+ _animator = animator; // 값을 저장할거에요 -> 애니메이터를
+ }
+
+ public void ApplyBurn(float damage, float duration) => _owner.StartCoroutine(BurnRoutine(damage, duration)); // 함수를 선언할거에요 -> 화상 코루틴 시작을
+ public void ApplySlow(float amount, float duration) => _owner.StartCoroutine(SlowRoutine(amount, duration)); // 함수를 선언할거에요 -> 슬로우 코루틴 시작을
+ public void ApplyShock(float damage, float duration) { _damageable.TakeDamage(damage); _owner.StartCoroutine(StunRoutine(duration)); } // 함수를 선언할거에요 -> 충격과 스턴 시작을
+
+ private IEnumerator BurnRoutine(float damage, float duration) // 코루틴 함수를 정의할거에요 -> 화상 로직을
+ {
+ float elapsed = 0f; // 변수를 초기화할거에요 -> 경과 시간을
+ while (elapsed < duration) // 반복할거에요 -> 지속 시간 동안
+ {
+ _damageable.TakeDamage(damage * 0.5f); // 실행할거에요 -> 0.5초치 데미지를
+ yield return new WaitForSeconds(0.5f); // 기다릴거에요 -> 0.5초를
+ elapsed += 0.5f; // 값을 더할거에요 -> 경과 시간에
+ }
+ }
+
+ private IEnumerator SlowRoutine(float amount, float duration) // 코루틴 함수를 정의할거에요 -> 슬로우 로직을
+ {
+ if (_agent == null) yield break; // 조건이 맞으면 종료할거에요 -> 에이전트가 없으면
+ float orgSpeed = _agent.speed; // 값을 저장할거에요 -> 원래 속도를
+ _agent.speed *= (1f - Mathf.Clamp01(amount / 100f)); // 값을 바꿀거에요 -> 속도를 줄여서
+ yield return new WaitForSeconds(duration); // 기다릴거에요 -> 지속 시간만큼
+ _agent.speed = orgSpeed; // 값을 복구할거에요 -> 원래 속도로
+ }
+
+ private IEnumerator StunRoutine(float duration) // 코루틴 함수를 정의할거에요 -> 스턴 로직을
+ {
+ if (_agent != null) _agent.isStopped = true; // 명령을 내릴거에요 -> 이동 정지를
+ if (_animator != null) _animator.speed = 0; // 값을 바꿀거에요 -> 애니 속도를 0으로
+ yield return new WaitForSeconds(duration); // 기다릴거에요 -> 스턴 시간만큼
+ if (_agent != null && _agent.isOnNavMesh) _agent.isStopped = false; // 명령을 내릴거에요 -> 이동 재개를
+ if (_animator != null) _animator.speed = 1; // 값을 바꿀거에요 -> 애니 속도 복구를
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Core/Utils/StatusEffectProcessor.cs.meta b/Assets/Scripts/Core/Utils/StatusEffectProcessor.cs.meta
new file mode 100644
index 00000000..75666c24
--- /dev/null
+++ b/Assets/Scripts/Core/Utils/StatusEffectProcessor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2e27a73f1d147004c9ec8e5309aa6f22
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Enemy/AI/ChargeMonster.cs b/Assets/Scripts/Enemy/AI/ChargeMonster.cs
index 03a484c5..ba9146f5 100644
--- a/Assets/Scripts/Enemy/AI/ChargeMonster.cs
+++ b/Assets/Scripts/Enemy/AI/ChargeMonster.cs
@@ -1,211 +1,80 @@
-using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
-using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를
-using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
-using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
+using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
+using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를
+using System.Collections.Generic; // 리스트를 사용할거에요 -> System.Collections.Generic을
-///
-/// 돌진 공격 몬스터
-///
-public class ChargeMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 ChargeMonster를
+public class ChargeMonster : MonsterClass // 클래스를 선언할거에요 -> 돌진 몬스터를
{
- [Header("=== 돌진 공격 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 돌진 공격 설정 === 을
- [SerializeField] private float chargeSpeed = 15f; // 변수를 선언할거에요 -> 돌진 속도(15.0)를 chargeSpeed에
- [SerializeField] private float chargeRange = 10f; // 변수를 선언할거에요 -> 돌진 시작 거리(10.0)를 chargeRange에
- [SerializeField] private float chargeDuration = 2f; // 변수를 선언할거에요 -> 돌진 지속 시간(2.0초)을 chargeDuration에
- [SerializeField] private float chargeDelay = 3f; // 변수를 선언할거에요 -> 돌진 쿨타임(3.0초)을 chargeDelay에
- [SerializeField] private float prepareTime = 0.5f; // 변수를 선언할거에요 -> 돌진 준비 시간(0.5초)을 prepareTime에
+ [Header("=== 돌진 설정 ===")] // 인스펙터 제목을 달거에요 -> 돌진 설정을
+ [SerializeField] private float chargeSpeed = 15f; // 변수를 선언할거에요 -> 속도를
+ [SerializeField] private float chargeRange = 10f; // 변수를 선언할거에요 -> 감지 거리를
+ [SerializeField] private float chargeDuration = 2f; // 변수를 선언할거에요 -> 지속 시간을
+ [SerializeField] private float chargeDelay = 3f; // 변수를 선언할거에요 -> 쿨타임을
+ [SerializeField] private float prepareTime = 0.5f; // 변수를 선언할거에요 -> 준비 시간을
- // 🎁 [추가] 드랍 아이템 리스트
- [Header("=== 드랍 아이템 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 드랍 아이템 설정 === 을
- [Tooltip("드랍 가능한 아이템 리스트 (랜덤 1개)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- [SerializeField] private List dropItemPrefabs; // 리스트를 선언할거에요 -> 드랍 아이템 프리팹 목록을 dropItemPrefabs에
- [Tooltip("아이템이 나올 확률 (0 ~ 100%)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- [Range(0, 100)][SerializeField] private float dropChance = 30f; // 변수를 선언할거에요 -> 드랍 확률(30%)을 dropChance에
+ [Header("애니메이션")] // 인스펙터 제목을 달거에요 -> 애니메이션을
+ [SerializeField] private string chargeAnim = "Monster_Charge"; // 변수를 선언할거에요 -> 돌진 애니를
+ [SerializeField] private string prepareAnim = "Monster_ChargePrepare"; // 변수를 선언할거에요 -> 준비 애니를
+ [SerializeField] private string walkAnim = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니를
- private float lastChargeTime; // 변수를 선언할거에요 -> 마지막 돌진 시간을 lastChargeTime에
- private bool isCharging = false; // 변수를 선언할거에요 -> 돌진 중 여부를 isCharging에
- private bool isPreparing = false; // 변수를 선언할거에요 -> 준비 중 여부를 isPreparing에
- private Vector3 chargeDirection; // 변수를 선언할거에요 -> 돌진 방향 벡터를 chargeDirection에
+ private float lastChargeTime; // 변수를 선언할거에요 -> 마지막 돌진 시간을
+ private bool isCharging; // 변수를 선언할거에요 -> 돌진 중 여부를
+ private Rigidbody _rb; // 변수를 선언할거에요 -> 리지드바디를
- [Header("공격 / 이동 애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 공격 / 이동 애니메이션 을
- [SerializeField] private string chargeAnimation = "Monster_Charge"; // 변수를 선언할거에요 -> 돌진 애니메이션 이름을 chargeAnimation에
- [SerializeField] private string prepareAnimation = "Monster_ChargePrepare"; // 변수를 선언할거에요 -> 준비 애니메이션 이름을 prepareAnimation에
- [SerializeField] private string Monster_Walk = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을 Monster_Walk에
-
- [Header("AI 상세 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 상세 설정 을
- [SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에
- [SerializeField] private float patrolInterval = 2f; // 변수를 선언할거에요 -> 순찰 간격(2.0초)을 patrolInterval에
-
- private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에
- private bool isPlayerInZone; // 변수를 선언할거에요 -> 플레이어 감지 여부를 isPlayerInZone에
- private Rigidbody _rigidbody; // 변수를 선언할거에요 -> 물리 컴포넌트를 담을 _rigidbody를
-
- protected override void Awake() // 함수를 덮어씌워 실행할거에요 -> Awake 초기화 로직을
+ protected override void Awake() // 함수를 실행할거에요 -> 초기화 Awake를
{
- base.Awake(); // 부모의 함수를 실행할거에요 -> MonsterClass의 Awake를
- _rigidbody = GetComponent(); // 컴포넌트를 가져올거에요 -> Rigidbody를
- if (_rigidbody == null) _rigidbody = gameObject.AddComponent(); // 조건이 맞으면 실행할거에요 -> 없으면 새로 추가해서 _rigidbody에
- _rigidbody.isKinematic = true; // 설정을 바꿀거에요 -> 물리 연산을 꺼두기(Kinematic)로
+ base.Awake(); // 부모 함수를 실행할거에요
+ _rb = GetComponent(); // 가져올거에요 -> 리지드바디를
+ if (_rb == null) _rb = gameObject.AddComponent(); // 추가할거에요 -> 없으면 새로
+ _rb.isKinematic = true; // 끌거에요 -> 물리 연산을
}
- protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기 설정을 Init에서
+ protected override void Init() { if (agent != null) agent.stoppingDistance = 1f; } // 초기화할거에요 -> 정지 거리를
+
+ protected override void ExecuteAILogic() // 함수를 실행할거에요 -> AI 로직을
{
- if (agent != null) agent.stoppingDistance = 1f; // 조건이 맞으면 설정할거에요 -> 에이전트 정지 거리를 1.0으로
- if (animator != null) animator.applyRootMotion = false; // 조건이 맞으면 설정할거에요 -> 애니메이션 이동을 끄기로
+ if (isHit || isCharging || isAttacking) return; // 중단할거에요 -> 행동 불가면
+ float dist = Vector3.Distance(transform.position, playerTransform.position); // 계산할거에요 -> 거리를
+ if (dist <= chargeRange && Time.time >= lastChargeTime + chargeDelay) StartCoroutine(ChargeRoutine()); // 실행할거에요 -> 돌진을
+ else ChasePlayer(); // 실행할거에요 -> 추격을
}
- protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을
+ private void ChasePlayer() // 함수를 선언할거에요 -> 추격 로직을
{
- if (isHit || isCharging || isPreparing) return; // 조건이 맞으면 중단할거에요 -> 피격, 돌진, 준비 중이라면
-
- float distance = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의 거리를
-
- if (isPlayerInZone || distance <= chargeRange * 1.5f) // 조건이 맞으면 실행할거에요 -> 감지 구역 안이거나 거리가 가까우면
- {
- HandleChargeCombat(distance); // 함수를 실행할거에요 -> 전투 로직인 HandleChargeCombat을
- }
- else // 조건이 틀리면 실행할거에요 -> 멀리 있다면
- {
- Patrol(); // 함수를 실행할거에요 -> 순찰을 도는 Patrol을
- }
-
- UpdateMovementAnimation(); // 함수를 실행할거에요 -> 이동 애니메이션을 갱신하는 UpdateMovementAnimation을
+ if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(playerTransform.position); } // 이동할거에요 -> 플레이어에게
+ animator.Play(walkAnim); // 재생할거에요 -> 걷기 애니를
}
- void HandleChargeCombat(float distance) // 함수를 선언할거에요 -> 거리 기반 전투 판단 로직 HandleChargeCombat을
+ private IEnumerator ChargeRoutine() // 코루틴 함수를 정의할거에요 -> 돌진 과정을
{
- if (distance <= chargeRange && Time.time >= lastChargeTime + chargeDelay) // 조건이 맞으면 실행할거에요 -> 사거리 안이고 쿨타임이 지났다면
- {
- StartCoroutine(PrepareCharge()); // 코루틴을 실행할거에요 -> 돌진 준비인 PrepareCharge를
- }
- else if (!isCharging && !isPreparing) // 조건이 맞으면 실행할거에요 -> 돌진이나 준비 중이 아니라면 (추격)
- {
- if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면
- {
- if (agent.isStopped) agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀라고
- agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로
- }
- }
- }
+ isAttacking = true; agent.isStopped = true; // 설정할거에요 -> 공격 상태와 정지를
+ animator.Play(prepareAnim); // 재생할거에요 -> 준비 애니를
- IEnumerator PrepareCharge() // 코루틴 함수를 선언할거에요 -> 돌진 준비 과정인 PrepareCharge를
- {
- isPreparing = true; // 상태를 바꿀거에요 -> 준비 중 상태를 참(true)으로
- if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면
- {
- agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고
- agent.ResetPath(); // 명령을 내릴거에요 -> 경로를 초기화하라고
- agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로
- }
-
- chargeDirection = (playerTransform.position - transform.position).normalized; // 벡터를 계산할거에요 -> 플레이어를 향하는 방향을
- chargeDirection.y = 0; // 값을 바꿀거에요 -> 높이 차이를 무시하게 y를 0으로
- if (chargeDirection != Vector3.zero) transform.rotation = Quaternion.LookRotation(chargeDirection); // 회전시킬거에요 -> 돌진 방향을 바라보게
-
- if (!string.IsNullOrEmpty(prepareAnimation)) animator.Play(prepareAnimation, 0, 0f); // 재생할거에요 -> 준비 애니메이션을
-
- Debug.Log("[ChargeMonster] 돌진 준비..."); // 로그를 출력할거에요 -> 준비 메시지를
+ Vector3 dir = (playerTransform.position - transform.position).normalized; dir.y = 0; // 계산할거에요 -> 방향을
+ transform.rotation = Quaternion.LookRotation(dir); // 회전할거에요 -> 방향대로
yield return new WaitForSeconds(prepareTime); // 기다릴거에요 -> 준비 시간만큼
- StartCoroutine(Charge()); // 코루틴을 실행할거에요 -> 실제 돌진인 Charge를
- }
+ isCharging = true; lastChargeTime = Time.time; // 설정할거에요 -> 돌진 상태와 시간을
+ agent.enabled = false; _rb.isKinematic = false; // 전환할거에요 -> 물리 이동으로
+ animator.Play(chargeAnim); // 재생할거에요 -> 돌진 애니를
- IEnumerator Charge() // 코루틴 함수를 선언할거에요 -> 실제 돌진 동작인 Charge를
- {
- isPreparing = false; // 상태를 바꿀거에요 -> 준비 상태를 거짓(false)으로
- isCharging = true; // 상태를 바꿀거에요 -> 돌진 중 상태를 참(true)으로
- lastChargeTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 돌진 시간으로
-
- if (agent != null) agent.enabled = false; // 기능을 끌거에요 -> 길찾기 에이전트를 (물리 이동을 위해)
- if (_rigidbody != null) _rigidbody.isKinematic = false; // 기능을 켤거에요 -> 물리 연산을 (충돌 감지를 위해)
-
- animator.Play(chargeAnimation, 0, 0f); // 재생할거에요 -> 돌진 애니메이션을
- Debug.Log("[ChargeMonster] 돌진 시작!"); // 로그를 출력할거에요 -> 돌진 시작 메시지를
-
- float chargeStartTime = Time.time; // 값을 저장할거에요 -> 돌진 시작 시간을
-
- while (Time.time < chargeStartTime + chargeDuration) // 반복할거에요 -> 지속 시간이 끝날 때까지
+ float elapsed = 0f; // 초기화할거에요 -> 경과 시간을
+ while (elapsed < chargeDuration) // 반복할거에요 -> 지속 시간 동안
{
- if (_rigidbody != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면
- _rigidbody.velocity = chargeDirection * chargeSpeed; // 값을 넣을거에요 -> 속도를 돌진 방향과 속도로
- else // 조건이 틀리면 실행할거에요 -> 리지드바디가 없다면 (비상용)
- transform.position += chargeDirection * chargeSpeed * Time.deltaTime; // 이동시킬거에요 -> 위치를 직접 수정해서
+ _rb.velocity = transform.forward * chargeSpeed; // 적용할거에요 -> 속도를
+ elapsed += Time.deltaTime; // 더할거에요 -> 시간을
yield return null; // 대기할거에요 -> 다음 프레임까지
}
- if (_rigidbody != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면
- {
- _rigidbody.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로
- _rigidbody.isKinematic = true; // 기능을 끌거에요 -> 물리 연산을 (다시 NavMesh 이동을 위해)
- }
-
- if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면
- {
- agent.enabled = true; // 기능을 켤거에요 -> 길찾기 에이전트를
- agent.ResetPath(); // 초기화할거에요 -> 경로를
- agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 내부 속도를 0으로
- }
-
- isCharging = false; // 상태를 바꿀거에요 -> 돌진 중 상태를 거짓(false)으로
- Debug.Log("[ChargeMonster] 돌진 종료, 쿨타임 시작"); // 로그를 출력할거에요 -> 종료 메시지를
+ _rb.velocity = Vector3.zero; _rb.isKinematic = true; agent.enabled = true; // 복구할거에요 -> 상태를
+ isCharging = false; OnAttackEnd(); // 종료할거에요 -> 돌진과 공격을
}
- void UpdateMovementAnimation() // 함수를 선언할거에요 -> 애니메이션 갱신 함수 UpdateMovementAnimation을
+ private void OnCollisionEnter(Collision col) // 함수를 실행할거에요 -> 충돌 시
{
- if (isCharging || isPreparing || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면
- if (agent.enabled && agent.velocity.magnitude > 0.1f) // 조건이 맞으면 실행할거에요 -> 움직이고 있다면
- animator.Play(Monster_Walk); // 재생할거에요 -> 걷기 애니메이션을
- else // 조건이 틀리면 실행할거에요 -> 멈춰 있다면
- animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니메이션을
- }
-
- void Patrol() // 함수를 선언할거에요 -> 순찰 로직 Patrol을
- {
- if (Time.time < nextPatrolTime) return; // 조건이 맞으면 중단할거에요 -> 대기 시간이라면
- Vector3 randomPoint = transform.position + new Vector3( // 벡터를 계산할거에요 -> 랜덤 순찰 지점을
- Random.Range(-patrolRadius, patrolRadius),
- 0,
- Random.Range(-patrolRadius, patrolRadius)
- );
-
- if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) // 조건이 맞으면 실행할거에요 -> 유효한 NavMesh 위치라면
- if (agent.isOnNavMesh) agent.SetDestination(hit.position); // 명령을 내릴거에요 -> 그곳으로 이동하라고
-
- nextPatrolTime = Time.time + patrolInterval; // 값을 갱신할거에요 -> 다음 순찰 시간을
- }
-
- private void OnCollisionEnter(Collision collision) // 함수를 실행할거에요 -> 물리 충돌이 발생했을 때 OnCollisionEnter를
- {
- if (!isCharging) return; // 조건이 맞으면 중단할거에요 -> 돌진 중이 아니라면
- if (collision.gameObject.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 충돌한 대상이 플레이어라면
+ if (isCharging && col.gameObject.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 돌진 중 플레이어 충돌이면
{
- if (collision.gameObject.TryGetComponent(out var playerHealth)) // 컴포넌트를 가져올거에요 -> 플레이어 체력 스크립트를
- {
- if (!playerHealth.isInvincible) // 조건이 맞으면 실행할거에요 -> 무적 상태가 아니라면
- playerHealth.TakeDamage(attackDamage); // 함수를 실행할거에요 -> 데미지를 입히는 TakeDamage를
- }
+ var t = col.gameObject.GetComponent(); // 가져올거에요 -> 인터페이스를
+ if (t != null) t.TakeDamage(attackDamage); // 입힐거에요 -> 데미지를
}
}
-
- // ☠️ [추가] 죽을 때 아이템 드랍
- protected override void OnDie() // 함수를 덮어씌워 실행할거에요 -> 사망 시 아이템 드랍 로직인 OnDie를
- {
- if (_rigidbody != null) _rigidbody.velocity = Vector3.zero; // 값을 넣을거에요 -> 죽을 때 미끄러지지 않게 속도를 0으로
-
- if (dropItemPrefabs != null && dropItemPrefabs.Count > 0) // 조건이 맞으면 실행할거에요 -> 아이템 리스트가 있다면
- {
- float randomValue = Random.Range(0f, 100f); // 값을 뽑을거에요 -> 0~100 사이 랜덤값을
- if (randomValue <= dropChance) // 조건이 맞으면 실행할거에요 -> 확률에 당첨되었다면
- {
- int randomIndex = Random.Range(0, dropItemPrefabs.Count); // 값을 뽑을거에요 -> 랜덤 인덱스를
- if (dropItemPrefabs[randomIndex] != null) // 조건이 맞으면 실행할거에요 -> 아이템이 유효하다면
- {
- Instantiate(dropItemPrefabs[randomIndex], transform.position + Vector3.up * 0.5f, Quaternion.identity); // 생성할거에요 -> 아이템을 몬스터 위치에
- }
- }
- }
- }
-
- private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; } // 상태를 바꿀거에요 -> 플레이어 감지 시 참(true)으로
- private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; } // 상태를 바꿀거에요 -> 플레이어가 나가면 거짓(false)으로
}
\ No newline at end of file
diff --git a/Assets/Scripts/Enemy/AI/ExplodeMonster.cs b/Assets/Scripts/Enemy/AI/ExplodeMonster.cs
index 7a2bbdd2..0b38fc1b 100644
--- a/Assets/Scripts/Enemy/AI/ExplodeMonster.cs
+++ b/Assets/Scripts/Enemy/AI/ExplodeMonster.cs
@@ -1,240 +1,95 @@
-using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
-using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를
-using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
+using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
+using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를
-///
-/// 자폭 몬스터 (Kamikaze)
-/// - 플레이어에게 전력 질주
-/// - 범위 내 진입 시 제자리에서 카운트다운 후 폭발
-///
-public class ExplodeMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 ExplodeMonster를
+public class ExplodeMonster : MonsterClass // 클래스를 선언할거에요 -> 자폭 몬스터를
{
- [Header("=== 자폭 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 자폭 설정 === 을
- [SerializeField] private float explodeRange = 4f; // 변수를 선언할거에요 -> 폭발 데미지 반경(4.0)을 explodeRange에
- [SerializeField] private float triggerRange = 2.5f; // 변수를 선언할거에요 -> 자폭 시퀀스 시작 거리(2.5)를 triggerRange에
- [SerializeField] private float fuseTime = 1.5f; // 변수를 선언할거에요 -> 폭발 지연 시간(1.5초)을 fuseTime에
- [SerializeField] private float explosionDamage = 50f; // 변수를 선언할거에요 -> 폭발 데미지(50.0)를 explosionDamage에
+ [Header("=== 자폭 설정 ===")] // 인스펙터 제목을 달거에요 -> 자폭 설정을
+ [SerializeField] private float explodeRange = 4f; // 변수를 선언할거에요 -> 폭발 범위를
+ [SerializeField] private float triggerRange = 2.5f; // 변수를 선언할거에요 -> 감지 범위를
+ [SerializeField] private float fuseTime = 1.5f; // 변수를 선언할거에요 -> 지연 시간을
+ [SerializeField] private float explosionDamage = 50f; // 변수를 선언할거에요 -> 폭발 데미지를
- [Header("폭발 효과")] // 인스펙터 창에 제목을 표시할거에요 -> 폭발 효과 를
- [SerializeField] private GameObject explosionEffectPrefab; // 변수를 선언할거에요 -> 폭발 이펙트 프리팹을 explosionEffectPrefab에
- [SerializeField] private ParticleSystem fuseEffect; // 변수를 선언할거에요 -> 도화선(준비) 이펙트를 fuseEffect에
- [SerializeField] private AudioClip fuseSound; // 변수를 선언할거에요 -> 도화선 소리를 fuseSound에
- [SerializeField] private AudioClip explosionSound; // 변수를 선언할거에요 -> 폭발 소리를 explosionSound에
+ [Header("이펙트")] // 인스펙터 제목을 달거에요 -> 이펙트를
+ [SerializeField] private GameObject explosionEffectPrefab; // 변수를 선언할거에요 -> 폭발 이펙트를
+ [SerializeField] private ParticleSystem fuseEffect; // 변수를 선언할거에요 -> 도화선 이펙트를
+ [SerializeField] private AudioClip fuseSound; // 변수를 선언할거에요 -> 도화선 소리를
+ [SerializeField] private AudioClip explosionSound; // 변수를 선언할거에요 -> 폭발 소리를
- [Header("애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 애니메이션 을
- [SerializeField] private string runAnimation = "Monster_Run"; // 변수를 선언할거에요 -> 달리기 애니메이션 이름을 runAnimation에
- [SerializeField] private string fuseAnimation = "Monster_Fuse"; // 변수를 선언할거에요 -> 자폭 준비 애니메이션 이름을 fuseAnimation에
+ [Header("애니메이션")] // 인스펙터 제목을 달거에요 -> 애니메이션을
+ [SerializeField] private string runAnim = "Monster_Run"; // 변수를 선언할거에요 -> 달리기 애니 이름을
+ [SerializeField] private string fuseAnim = "Monster_Fuse"; // 변수를 선언할거에요 -> 점화 애니 이름을
- [Header("AI 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 설정 을
- [SerializeField] private float chaseSpeed = 6f; // 변수를 선언할거에요 -> 추격 속도(6.0)를 chaseSpeed에
- [SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에
- [SerializeField] private float patrolInterval = 2f; // 변수를 선언할거에요 -> 순찰 간격(2.0초)을 patrolInterval에
+ private bool hasExploded = false; // 변수를 선언할거에요 -> 폭발 여부를
- private bool isExploding = false; // 변수를 초기화할거에요 -> 폭발 진행 중 여부를 거짓(false)으로
- private bool hasExploded = false; // 변수를 초기화할거에요 -> 이미 터졌는지 여부를 거짓(false)으로
- private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에
- private bool isPlayerInZone; // 변수를 선언할거에요 -> 플레이어 감지 여부를 isPlayerInZone에
-
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- // 초기화
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-
- protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기화 로직인 Init을
+ protected override void Init() // 함수를 실행할거에요 -> 초기화 Init을
{
- if (agent != null)
- {
- agent.speed = chaseSpeed; // 값을 설정할거에요 -> 이동 속도를 추격 속도로
- agent.stoppingDistance = 0.5f; // 값을 설정할거에요 -> 정지 거리를 아주 짧게(0.5)
- }
- if (animator != null) animator.applyRootMotion = false; // 설정을 바꿀거에요 -> 애니메이션 이동을 끄기로
-
- if (fuseEffect != null) fuseEffect.Stop(); // 기능을 실행할거에요 -> 도화선 이펙트가 켜져있다면 끄기를
+ if (agent != null) agent.stoppingDistance = 0.5f; // 설정을 바꿀거에요 -> 정지 거리를 짧게
}
- protected override void OnResetStats() // 함수를 덮어씌워 실행할거에요 -> 스탯 초기화 로직인 OnResetStats를
+ protected override void ExecuteAILogic() // 함수를 실행할거에요 -> AI 로직을
{
- isExploding = false; // 상태를 바꿀거에요 -> 폭발 진행 상태를 거짓(false)으로
- hasExploded = false; // 상태를 바꿀거에요 -> 폭발 완료 상태를 거짓(false)으로
- if (fuseEffect != null) fuseEffect.Stop(); // 기능을 실행할거에요 -> 도화선 이펙트를 끄기를
- }
+ if (isHit || isAttacking || hasExploded) return; // 조건이 맞으면 중단할거에요 -> 행동 불가면
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- // AI 로직
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+ float dist = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의
- protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을
- {
- if (isHit || isExploding || hasExploded) return; // 조건이 맞으면 중단할거에요 -> 피격, 자폭 중, 이미 폭발 상태라면
-
- float distance = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의 거리를
-
- // 플레이어가 감지 범위(15m) 안에 있거나 트리거에 닿았으면 추격
- if (isPlayerInZone || distance <= 15f) // 조건이 맞으면 실행할거에요 -> 추격 조건이 만족되면
+ if (dist <= triggerRange) // 조건이 맞으면 실행할거에요 -> 감지 거리 안이면
{
- ChasePlayer(distance); // 함수를 실행할거에요 -> 추격 및 자폭 처리를 하는 ChasePlayer를
+ StartCoroutine(ExplodeRoutine()); // 코루틴을 실행할거에요 -> 자폭 시퀀스를
}
- else // 조건이 틀리면 실행할거에요 -> 멀리 있다면
+ else // 조건이 틀리면 실행할거에요 -> 멀면
{
- Patrol(); // 함수를 실행할거에요 -> 순찰을 도는 Patrol을
- UpdateMovementAnimation(); // 함수를 실행할거에요 -> 애니메이션을 갱신하는 UpdateMovementAnimation을
+ ChasePlayer(); // 함수를 실행할거에요 -> 추격을
}
}
- void ChasePlayer(float distance) // 함수를 선언할거에요 -> 거리별 추격 행동을 정하는 ChasePlayer를
+ private void ChasePlayer() // 함수를 선언할거에요 -> 추격 로직을
{
- // 1. 폭발 시작 거리 안으로 들어왔다? -> 점화!
- if (distance <= triggerRange) // 조건이 맞으면 실행할거에요 -> 자폭 거리 이내라면
+ if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 내비게이션 위라면
{
- StartCoroutine(ExplodeRoutine()); // 코루틴을 실행할거에요 -> 자폭 시퀀스인 ExplodeRoutine을
- return; // 중단할거에요 -> 더 이상 이동하지 않도록
- }
-
- // 2. 아직 멀었다? -> 전력 질주
- if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면
- {
- agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀라고
- agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로
-
- // 달리기 애니메이션
- animator.Play(runAnimation); // 재생할거에요 -> 달리기 애니메이션을
+ agent.isStopped = false; // 명령을 내릴거에요 -> 이동 재개
+ agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 플레이어 위치로
}
+ animator.Play(runAnim); // 재생할거에요 -> 달리기 애니를
}
- void UpdateMovementAnimation() // 함수를 선언할거에요 -> 애니메이션 갱신 함수 UpdateMovementAnimation을
+ private IEnumerator ExplodeRoutine() // 코루틴 함수를 정의할거에요 -> 자폭 과정을
{
- if (isExploding || isHit) return; // 조건이 맞으면 중단할거에요 -> 폭발 중이거나 맞고 있다면
+ isAttacking = true; // 상태를 바꿀거에요 -> 공격 중으로 (이동 불가)
+ hasExploded = true; // 상태를 바꿀거에요 -> 폭발 됨으로 (중복 방지)
- if (agent.velocity.magnitude > 0.1f) // 조건이 맞으면 실행할거에요 -> 움직이고 있다면
- animator.Play(runAnimation); // 재생할거에요 -> 달리기 애니메이션을
- else // 조건이 틀리면 실행할거에요 -> 멈춰 있다면
- animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니메이션을
+ if (agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 명령을 내릴거에요 -> 정지
+
+ // 1. 점화
+ animator.Play(fuseAnim); // 재생할거에요 -> 점화 애니를
+ if (fuseEffect != null) fuseEffect.Play(); // 실행할거에요 -> 도화선 효과를
+ if (fuseSound != null) audioSource.PlayOneShot(fuseSound); // 재생할거에요 -> 도화선 소리를
+
+ yield return new WaitForSeconds(fuseTime); // 기다릴거에요 -> 지연 시간만큼
+
+ // 2. 폭발
+ Explode(); // 함수를 실행할거에요 -> 폭발 처리를
}
- void Patrol() // 함수를 선언할거에요 -> 순찰 로직 Patrol을
+ private void Explode() // 함수를 선언할거에요 -> 실제 폭발 로직을
{
- if (Time.time < nextPatrolTime) return; // 조건이 맞으면 중단할거에요 -> 대기 시간이라면
+ if (explosionEffectPrefab != null) Instantiate(explosionEffectPrefab, transform.position, Quaternion.identity); // 생성할거에요 -> 폭발 이펙트를
+ if (explosionSound != null) AudioSource.PlayClipAtPoint(explosionSound, transform.position); // 재생할거에요 -> 폭발 소리를
- Vector3 randomPoint = transform.position + new Vector3( // 벡터를 계산할거에요 -> 랜덤 순찰 지점을
- Random.Range(-patrolRadius, patrolRadius),
- 0,
- Random.Range(-patrolRadius, patrolRadius)
- );
-
- if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) // 조건이 맞으면 실행할거에요 -> 유효한 위치라면
- if (agent.isOnNavMesh) agent.SetDestination(hit.position); // 명령을 내릴거에요 -> 이동하라고
-
- nextPatrolTime = Time.time + patrolInterval; // 값을 갱신할거에요 -> 다음 순찰 시간을
- }
-
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- // 💣 폭발 시퀀스 (점화 -> 대기 -> 쾅!)
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-
- IEnumerator ExplodeRoutine() // 코루틴 함수를 선언할거에요 -> 자폭 진행 과정인 ExplodeRoutine을
- {
- if (hasExploded) yield break; // 조건이 맞으면 종료할거에요 -> 이미 터졌다면
-
- isExploding = true; // 상태를 바꿀거에요 -> 폭발 진행 상태를 참(true)으로
- hasExploded = true; // 상태를 바꿀거에요 -> 폭발 완료 상태를 참(true)으로 (중복 방지)
- isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로 (부모 간섭 방지)
-
- // ⭐ [핵심] 급브레이크! (문워크 방지)
- if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 있다면
+ // 범위 데미지 처리
+ Collider[] hits = Physics.OverlapSphere(transform.position, explodeRange); // 배열에 담을거에요 -> 범위 내 충돌체들을
+ foreach (var hit in hits) // 반복할거에요 -> 충돌체마다
{
- agent.isStopped = true; // 명령을 내릴거에요 -> 멈추라고
- agent.ResetPath(); // 명령을 내릴거에요 -> 경로를 지우라고
- agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로
- }
-
- Debug.Log($"💣 자폭 카운트다운! ({fuseTime}초 뒤 폭발)"); // 로그를 출력할거에요 -> 자폭 예고 메시지를
-
- // 1. 부들부들 떨기 (폭발 준비 모션)
- if (!string.IsNullOrEmpty(fuseAnimation)) animator.Play(fuseAnimation); // 재생할거에요 -> 준비 애니메이션을
-
- // 2. 치익~ 소리 & 이펙트
- if (fuseEffect != null) fuseEffect.Play(); // 실행할거에요 -> 도화선 이펙트를
- if (fuseSound != null && audioSource != null) audioSource.PlayOneShot(fuseSound); // 재생할거에요 -> 도화선 소리를
-
- // 3. 도망갈 시간 주기
- yield return new WaitForSeconds(fuseTime); // 기다릴거에요 -> 폭발 지연 시간만큼
-
- // 4. 쾅!
- PerformExplosion(); // 함수를 실행할거에요 -> 실제 폭발 처리를
- }
-
- void PerformExplosion() // 함수를 선언할거에요 -> 폭발 데미지와 이펙트를 처리하는 PerformExplosion을
- {
- Debug.Log("💥💥💥 쾅!!!"); // 로그를 출력할거에요 -> 폭발 메시지를
-
- // 폭발 이펙트 생성 (Particle System)
- if (explosionEffectPrefab != null) // 조건이 맞으면 실행할거에요 -> 폭발 프리팹이 있다면
- {
- Instantiate(explosionEffectPrefab, transform.position, Quaternion.identity); // 생성할거에요 -> 폭발 이펙트를 현재 위치에
- }
-
- // 폭발음
- if (explosionSound != null) // 조건이 맞으면 실행할거에요 -> 폭발음이 있다면
- {
- AudioSource.PlayClipAtPoint(explosionSound, transform.position, 1f); // 재생할거에요 -> 폭발 소리를 해당 위치에서
- }
-
- // 주변 데미지 처리
- DamageNearbyTargets(); // 함수를 실행할거에요 -> 주변 대상에게 데미지를 주는 DamageNearbyTargets를
-
- // 몬스터 사망 처리 (MonsterClass 기능 사용)
- Die(); // 함수를 실행할거에요 -> 몬스터를 죽게 만드는 Die를
- }
-
- void DamageNearbyTargets() // 함수를 선언할거에요 -> 폭발 범위 내 데미지를 입히는 DamageNearbyTargets를
- {
- // 폭발 범위(Sphere) 안에 있는 모든 물체 검사
- Collider[] hitColliders = Physics.OverlapSphere(transform.position, explodeRange); // 배열에 담을거에요 -> 폭발 범위 내의 충돌체들을
-
- foreach (Collider hit in hitColliders) // 반복할거에요 -> 감지된 모든 충돌체에 대해
- {
- // 플레이어 데미지
- if (hit.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 대상이 플레이어라면
+ if (hit.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 플레이어라면
{
- if (hit.TryGetComponent(out var playerHealth)) // 컴포넌트를 가져올거에요 -> 플레이어 체력 스크립트를
- {
- if (!playerHealth.isInvincible) // 조건이 맞으면 실행할거에요 -> 무적 상태가 아니라면
- {
- playerHealth.TakeDamage(explosionDamage); // 함수를 실행할거에요 -> 폭발 데미지를 입히는 TakeDamage를
- Debug.Log($"💥 플레이어 폭사! 데미지: {explosionDamage}"); // 로그를 출력할거에요 -> 피격 메시지를
- }
- }
- }
- // (선택사항) 주변 몬스터도 팀킬?
- else if (hit.CompareTag("Enemy") && hit.gameObject != gameObject) // 조건이 맞으면 실행할거에요 -> 대상이 다른 몬스터라면
- {
- // 팀킬 로직이 필요하면 여기에 추가
+ var hp = hit.GetComponent(); // 가져올거에요 -> 체력 인터페이스를
+ if (hp != null) hp.TakeDamage(explosionDamage); // 실행할거에요 -> 폭발 데미지 입히기를
}
}
+
+ // 자폭이므로 몬스터 사망
+ currentHP = 0; // 값을 설정할거에요 -> 체력을 0으로
+ Die(); // 실행할거에요 -> 사망 함수를
}
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- // 유틸리티
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-
- // 자폭병은 때려도 폭발 안 멈춤 (취향에 따라 수정 가능)
- protected override void OnStartHit() { } // 함수를 비워둘거에요 -> 피격 시 아무 행동도 안 하게 (폭발 캔슬 방지)
-
- // 죽을 때 퓨즈 끄기
- protected override void OnDie() // 함수를 덮어씌워 실행할거에요 -> 사망 시 정리를 위해
- {
- if (fuseEffect != null && fuseEffect.isPlaying) fuseEffect.Stop(); // 기능을 실행할거에요 -> 도화선 이펙트가 켜져있다면 끄기를
- }
-
- private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; } // 상태를 바꿀거에요 -> 플레이어 진입 시 감지 상태를 참으로
- private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; } // 상태를 바꿀거에요 -> 플레이어 이탈 시 감지 상태를 거짓으로
-
- // 에디터에서 범위 보여주기
- private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 에디터에서 범위를 그리는 OnDrawGizmosSelected를
- {
- Gizmos.color = Color.red; // 색상을 설정할거에요 -> 감지 범위 색을 빨간색으로
- Gizmos.DrawWireSphere(transform.position, triggerRange); // 그림을 그릴거에요 -> 자폭 감지 범위를
-
- Gizmos.color = new Color(1f, 0.5f, 0f, 0.3f); // 색상을 설정할거에요 -> 폭발 범위 색을 주황색 반투명으로
- Gizmos.DrawSphere(transform.position, explodeRange); // 그림을 그릴거에요 -> 폭발 데미지 범위를
- }
+ protected override void OnStartHit() { } // 가상 함수를 비울거에요 -> 피격 되어도 멈추지 않게 (슈퍼아머)
}
\ No newline at end of file
diff --git a/Assets/Scripts/Enemy/AI/Monster Weapon.cs b/Assets/Scripts/Enemy/AI/Monster Weapon.cs
index 511234dc..9e97d455 100644
--- a/Assets/Scripts/Enemy/AI/Monster Weapon.cs
+++ b/Assets/Scripts/Enemy/AI/Monster Weapon.cs
@@ -1,80 +1,28 @@
-using System.Collections.Generic; // 제네릭 컬렉션을 쓰기 위해 불러올거에요 -> System.Collections.Generic을
-using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
+using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
-public class MonsterWeapon : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 MonsterWeapon을
-{ // 코드 블록을 시작할거에요 -> MonsterWeapon 범위를
+public class MonsterWeapon : MonoBehaviour // 클래스를 선언할거에요 -> 일반 몬스터 무기를
+{
+ [SerializeField] private float baseDamage = 10f; // 변수를 선언할거에요 -> 기본 데미지를
+ private float finalDamage; // 변수를 선언할거에요 -> 최종 데미지를
+ private BoxCollider col; // 변수를 선언할거에요 -> 콜라이더를
- [Header("무기 설정")] // 인스펙터에 제목을 표시할거에요 -> 무기 설정을
- [Tooltip("이 무기 고유의 공격력 (예: 10)")] // 인스펙터에 툴팁을 달거에요 -> weaponBaseDamage 설명을
- [SerializeField] private float weaponBaseDamage = 10f; // 변수를 선언할거에요 -> 무기 기본 공격력을 10으로
+ private void Awake() // 함수를 실행할거에요 -> 초기화 Awake를
+ {
+ col = GetComponent(); // 가져올거에요 -> 박스 콜라이더를
+ finalDamage = baseDamage; // 설정할거에요 -> 초기 데미지를
+ EnableHitBox(); // 실행할거에요 -> 일단 켜두기를
+ }
- private float _finalDamage; // 변수를 선언할거에요 -> 최종 데미지를 저장할 _finalDamage를
- [SerializeField] private BoxCollider _weaponCollider; // 변수를 선언할거에요 -> 무기 콜라이더를 저장할 _weaponCollider를
+ public void SetDamage(float amount) => finalDamage = amount; // 함수를 선언할거에요 -> 데미지 설정을
+ public void EnableHitBox() { if (col != null) col.enabled = true; } // 함수를 선언할거에요 -> 판정 켜기를
+ public void DisableHitBox() { if (col != null) col.enabled = false; } // 함수를 선언할거에요 -> 판정 끄기를
- private void Awake() // 함수를 선언할거에요 -> 시작 시 1번 실행되는 Awake를
- { // 코드 블록을 시작할거에요 -> Awake 범위를
- _weaponCollider = GetComponent(); // 컴포넌트를 가져올거에요 -> 내 오브젝트의 BoxCollider를 찾아 _weaponCollider에
- _finalDamage = weaponBaseDamage; // 값을 넣을거에요 -> 최종 데미지를 기본 데미지로 초기화
- //DisableHitBox(); // 주석 처리할거에요 -> 시작 시 판정을 끄는 코드(현재는 안 씀)
- EnableHitBox(); // 함수를 실행할거에요 -> 시작하자마자 판정을 켜기(현재 설정)
- } // 코드 블록을 끝낼거에요 -> Awake를
-
- public void SetDamage(float monsterStrength) // 함수를 선언할거에요 -> 몬스터 스탯을 받아 최종 데미지를 세팅하는 SetDamage를
- { // 코드 블록을 시작할거에요 -> SetDamage 범위를
- _finalDamage = weaponBaseDamage + monsterStrength; // 값을 계산할거에요 -> 기본 데미지 + 몬스터 힘으로 최종 데미지 설정
- Debug.Log($"✅ [무기] 데미지 설정됨: 총 {_finalDamage}"); // 로그를 찍을거에요 -> 최종 데미지가 얼마인지
- } // 코드 블록을 끝낼거에요 -> SetDamage를
-
- public void EnableHitBox() // 함수를 선언할거에요 -> 공격 판정을 켜는 EnableHitBox를
- { // 코드 블록을 시작할거에요 -> EnableHitBox 범위를
- Debug.Log("enabletest"); // 로그를 찍을거에요 -> 함수가 호출됐는지 확인용
-
- if (_weaponCollider != null) // 조건을 검사할거에요 -> 콜라이더가 정상적으로 잡혔는지
- { // 코드 블록을 시작할거에요 -> 콜라이더가 있을 때 처리
- Debug.Log("setcollider"); // 로그를 찍을거에요 -> 콜라이더 enable 직전 확인용
- _weaponCollider.enabled = true; // 값을 바꿀거에요 -> 트리거 판정을 켜기
- Debug.Log("🔴 [무기] 공격 판정 ON! (휘두르기 시작)"); // 로그를 찍을거에요 -> 판정이 켜졌음을
- } // 코드 블록을 끝낼거에요 -> 콜라이더 처리
- } // 코드 블록을 끝낼거에요 -> EnableHitBox를
-
- public void DisableHitBox() // 함수를 선언할거에요 -> 공격 판정을 끄는 DisableHitBox를
- { // 코드 블록을 시작할거에요 -> DisableHitBox 범위를
- if (_weaponCollider != null) // 조건을 검사할거에요 -> 콜라이더가 있는지
- { // 코드 블록을 시작할거에요 -> 콜라이더가 있을 때 처리
- _weaponCollider.enabled = false; // 값을 바꿀거에요 -> 트리거 판정을 끄기
- Debug.Log("⚪ [무기] 공격 판정 OFF (휘두르기 끝)"); // 로그를 찍을거에요 -> 판정이 꺼졌음을
- } // 코드 블록을 끝낼거에요 -> 콜라이더 처리
- } // 코드 블록을 끝낼거에요 -> DisableHitBox를
-
- // ⭐ 여기가 핵심! 닿는 모든 것을 기록함 // 설명을 적을거에요 -> 트리거 충돌 감지 핵심 구간임을
- private void OnTriggerEnter(Collider other) // 함수를 선언할거에요 -> 트리거에 다른 콜라이더가 들어오면 호출되는 OnTriggerEnter를
- { // 코드 블록을 시작할거에요 -> OnTriggerEnter 범위를
- // 1. 무엇이든 닿으면 일단 로그를 찍음 (범인 색출) // 설명을 적을거에요 -> 뭐랑 부딪히는지 디버깅용
- Debug.Log($"💥 [충돌 감지] 무기가 닿은 것: {other.name} / 태그: {other.tag}"); // 로그를 찍을거에요 -> 충돌한 오브젝트 이름/태그를
-
- if (other.CompareTag("Player")) // 조건을 검사할거에요 -> 닿은 대상이 Player 태그인지
- { // 코드 블록을 시작할거에요 -> 플레이어일 때 처리
- PlayerHealth playerHealth = other.GetComponent(); // 컴포넌트를 가져올거에요 -> 플레이어의 PlayerHealth를 찾아 playerHealth에
-
- if (playerHealth != null) // 조건을 검사할거에요 -> PlayerHealth가 실제로 붙어있는지
- { // 코드 블록을 시작할거에요 -> PlayerHealth가 있을 때 처리
- if (!playerHealth.isInvincible) // 조건을 검사할거에요 -> 플레이어가 무적인지 아닌지
- { // 코드 블록을 시작할거에요 -> 무적이 아닐 때 처리
- playerHealth.TakeDamage(_finalDamage); // 함수를 실행할거에요 -> 플레이어에게 최종 데미지 적용
- Debug.Log($"⚔️ [적중] 플레이어 체력 깎임! (-{_finalDamage})"); // 로그를 찍을거에요 -> 데미지가 들어갔음을
- } // 코드 블록을 끝낼거에요 -> 무적 아닐 때 처리
- else // 조건이 아니면 실행할거에요 -> 무적이면
- { // 코드 블록을 시작할거에요 -> 무적일 때 처리
- Debug.Log("🛡️ [방어] 플레이어가 무적 상태입니다."); // 로그를 찍을거에요 -> 무적이라 데미지 안 들어감을
- } // 코드 블록을 끝낼거에요 -> 무적 처리
-
- DisableHitBox(); // 함수를 실행할거에요 -> 한 번 맞추면 바로 판정 끄기(중복 타격 방지)
- } // 코드 블록을 끝낼거에요 -> PlayerHealth 있을 때 처리
- else // 조건이 아니면 실행할거에요 -> PlayerHealth가 없으면
- { // 코드 블록을 시작할거에요 -> 오류 처리
- Debug.LogError("⚠️ [오류] 플레이어 태그는 있는데 'PlayerHealth' 스크립트가 없습니다!"); // 에러 로그를 찍을거에요 -> 컴포넌트 누락 경고
- } // 코드 블록을 끝낼거에요 -> 오류 처리
- } // 코드 블록을 끝낼거에요 -> 플레이어 처리
- } // 코드 블록을 끝낼거에요 -> OnTriggerEnter를
-
-} // 코드 블록을 끝낼거에요 -> MonsterWeapon을
\ No newline at end of file
+ private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 충돌 감지를
+ {
+ if (other.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 플레이어라면
+ {
+ var hp = other.GetComponent(); // 가져올거에요 -> 체력 인터페이스를
+ if (hp != null) { hp.TakeDamage(finalDamage); DisableHitBox(); } // 실행할거에요 -> 데미지를 주고 판정을 끄기를
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Enemy/AI/MonsterClass.cs b/Assets/Scripts/Enemy/AI/MonsterClass.cs
index 94b99598..37c26d94 100644
--- a/Assets/Scripts/Enemy/AI/MonsterClass.cs
+++ b/Assets/Scripts/Enemy/AI/MonsterClass.cs
@@ -1,359 +1,193 @@
-using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
-using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를
-using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
-using System; // 기본 시스템 기능(Action 등)을 사용할거에요 -> System을
+using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
+using UnityEngine.AI; // 길찾기 기능을 불러올거에요 -> UnityEngine.AI를
+using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를
+using System; // 시스템 기능을 사용할거에요 -> System을
-///
-/// 몬스터 기본 클래스 (공통 기능만)
-/// - 공격 방식은 자식 클래스에서 구현
-///
-public abstract class MonsterClass : MonoBehaviour, IDamageable // 추상 클래스를 선언할거에요 -> MonoBehaviour와 IDamageable을 상속받는 MonsterClass를
+public abstract class MonsterClass : MonoBehaviour, IDamageable // 추상 클래스를 선언할거에요 -> MonsterClass를
{
- [Header("--- 최적화 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 최적화 설정 --- 을
- protected Renderer mobRenderer; // 변수를 선언할거에요 -> 몬스터의 렌더러(외형) 컴포넌트를 담을 mobRenderer를
- protected Transform playerTransform; // 변수를 선언할거에요 -> 플레이어의 위치 정보를 담을 playerTransform을
- [SerializeField] protected float optimizationDistance = 40f; // 변수를 선언할거에요 -> 몬스터 최적화(AI 정지) 거리(40.0)를 optimizationDistance에
+ [Header("--- 최적화 ---")] // 인스펙터 제목을 달거에요 -> 최적화 설정을
+ protected Renderer mobRenderer; // 변수를 선언할거에요 -> 렌더러를
+ protected Transform playerTransform; // 변수를 선언할거에요 -> 플레이어 위치를
+ [SerializeField] protected float optimizationDistance = 40f; // 변수를 선언할거에요 -> 최적화 거리를
- [Header("몬스터 기본 스탯")] // 인스펙터 창에 제목을 표시할거에요 -> 몬스터 기본 스탯 을
- [SerializeField] protected float maxHP = 100f; // 변수를 선언할거에요 -> 최대 체력(100.0)을 maxHP에
- [SerializeField] protected float attackDamage = 10f; // 변수를 선언할거에요 -> 공격력(10.0)을 attackDamage에
- [SerializeField] protected int expReward = 10; // 변수를 선언할거에요 -> 처치 시 경험치 보상(10)을 expReward에
- [SerializeField] protected float moveSpeed = 3.5f; // 변수를 선언할거에요 -> 이동 속도(3.5)를 moveSpeed에
+ [Header("스탯")] // 인스펙터 제목을 달거에요 -> 스탯 설정을
+ [SerializeField] protected float maxHP = 100f; // 변수를 선언할거에요 -> 최대 체력을
+ [SerializeField] protected float attackDamage = 10f; // 변수를 선언할거에요 -> 공격력을
+ [SerializeField] protected int expReward = 10; // 변수를 선언할거에요 -> 경험치 보상을
+ [SerializeField] protected float moveSpeed = 3.5f; // 변수를 선언할거에요 -> 이동 속도를
- protected float currentHP; // 변수를 선언할거에요 -> 현재 체력을 저장할 currentHP를
- public event Action OnHealthChanged; // 이벤트를 선언할거에요 -> 체력이 변할 때 알릴 OnHealthChanged를
+ protected float currentHP; // 변수를 선언할거에요 -> 현재 체력을
+ public event Action OnHealthChanged; // 이벤트를 선언할거에요 -> 체력 변경 알림을
- [Header("전투 / 무기 (선택사항)")] // 인스펙터 창에 제목을 표시할거에요 -> 전투 / 무기 (선택사항) 을
- // ⭐ 근접 몬스터가 사용할 무기 (원거리 몬스터는 비워둬도 됨)
- [SerializeField] protected MonsterWeapon myWeapon; // 변수를 선언할거에요 -> 몬스터가 장착한 무기 스크립트를 myWeapon에
+ [Header("전투")] // 인스펙터 제목을 달거에요 -> 전투 설정을
+ [SerializeField] protected MonsterWeapon myWeapon; // 변수를 선언할거에요 -> 무기를
- [Header("피격 / 사망 / 대기 애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 피격 / 사망 / 대기 애니메이션 을
- [SerializeField] protected string Monster_Idle = "Monster_Idle"; // 변수를 선언할거에요 -> 대기 애니메이션 이름("Monster_Idle")을 Monster_Idle에
- [SerializeField] protected string Monster_GetDamage = "Monster_GetDamage"; // 변수를 선언할거에요 -> 피격 애니메이션 이름("Monster_GetDamage")을 Monster_GetDamage에
- [SerializeField] protected string Monster_Die = "Monster_Die"; // 변수를 선언할거에요 -> 사망 애니메이션 이름("Monster_Die")을 Monster_Die에
+ [Header("애니메이션")] // 인스펙터 제목을 달거에요 -> 애니메이션을
+ [SerializeField] protected string Monster_Idle = "Monster_Idle"; // 변수를 선언할거에요 -> 대기 애니 이름을
+ [SerializeField] protected string Monster_GetDamage = "Monster_GetDamage"; // 변수를 선언할거에요 -> 피격 애니 이름을
+ [SerializeField] protected string Monster_Die = "Monster_Die"; // 변수를 선언할거에요 -> 사망 애니 이름을
- protected Animator animator; // 변수를 선언할거에요 -> 애니메이션 제어 컴포넌트를 담을 animator를
- protected NavMeshAgent agent; // 변수를 선언할거에요 -> 길찾기 에이전트 컴포넌트를 담을 agent를
- protected AudioSource audioSource; // 변수를 선언할거에요 -> 소리 재생 컴포넌트를 담을 audioSource를
+ protected Animator animator; // 변수를 선언할거에요 -> 애니메이터를
+ protected NavMeshAgent agent; // 변수를 선언할거에요 -> 에이전트를
+ protected AudioSource audioSource; // 변수를 선언할거에요 -> 오디오 소스를
+ protected StatusEffectProcessor statusProcessor; // 변수를 선언할거에요 -> 상태이상 처리기를
- // ⭐ [핵심] 자식 클래스에서 공유할 상태 변수들 (중복 선언 방지)
- protected bool isHit;
- protected bool isDead;
- protected bool isAttacking;
+ protected bool isHit, isDead, isAttacking; // 상태 변수들을 선언할거에요
+ public bool IsAggroed { get; protected set; } // 프로퍼티를 선언할거에요 -> 어그로 여부를
- public bool IsAggroed { get; protected set; } // 프로퍼티를 선언할거에요 -> 어그로(전투) 상태 여부를 외부에서 읽기만 가능하게 IsAggroed에
+ [Header("AI")] // 인스펙터 제목을 달거에요 -> AI 설정을
+ [SerializeField] protected float attackRestDuration = 1.5f; // 변수를 선언할거에요 -> 공격 후 휴식 시간을
+ protected bool isResting; // 변수를 선언할거에요 -> 휴식 여부를
- [Header("AI 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 설정 을
- [SerializeField] protected float attackRestDuration = 1.5f; // 변수를 선언할거에요 -> 공격 후 대기 시간(1.5초)을 attackRestDuration에
- protected bool isResting; // 변수를 선언할거에요 -> 휴식 중인지 여부를 저장할 isResting을
+ public static Action OnMonsterKilled; // 이벤트를 선언할거에요 -> 사망 전역 알림을
- public static System.Action OnMonsterKilled; // 정적 이벤트를 선언할거에요 -> 몬스터 처치 시 경험치를 전달할 OnMonsterKilled를
+ [Header("사운드/이펙트")] // 인스펙터 제목을 달거에요 -> 효과 설정을
+ [SerializeField] protected AudioClip hitSound, deathSound; // 변수를 선언할거에요 -> 효과음들을
+ [SerializeField] protected GameObject deathEffectPrefab; // 변수를 선언할거에요 -> 사망 이펙트를
+ [SerializeField] protected ParticleSystem hitEffect; // 변수를 선언할거에요 -> 피격 이펙트를
+ [SerializeField] protected Transform impactSpawnPoint; // 변수를 선언할거에요 -> 이펙트 위치를
- [Header("공통 사운드/이펙트")] // 인스펙터 창에 제목을 표시할거에요 -> 공통 사운드/이펙트 를
- [SerializeField] protected AudioClip hitSound, deathSound; // 변수를 선언할거에요 -> 피격음과 사망음 오디오 클립을
- [SerializeField] protected GameObject deathEffectPrefab; // 변수를 선언할거에요 -> 사망 시 재생할 이펙트 프리팹을
- [SerializeField] protected ParticleSystem hitEffect; // 변수를 선언할거에요 -> 피격 시 재생할 파티클 시스템을
- [SerializeField] protected Transform impactSpawnPoint; // 변수를 선언할거에요 -> 이펙트가 생성될 위치를
-
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- // 생명주기
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-
- protected virtual void Awake() // 함수를 실행할거에요 -> 스크립트가 켜질 때 호출되는 Awake를
+ protected virtual void Awake() // 함수를 실행할거에요 -> 초기화 Awake를
{
- mobRenderer = GetComponentInChildren(); // 컴포넌트를 가져올거에요 -> 자식 오브젝트의 렌더러를 mobRenderer에
- animator = GetComponent(); // 컴포넌트를 가져올거에요 -> 내 몸의 애니메이터를 animator에
- agent = GetComponent(); // 컴포넌트를 가져올거에요 -> 내 몸의 길찾기 에이전트를 agent에
- audioSource = GetComponent(); // 컴포넌트를 가져올거에요 -> 내 몸의 오디오 소스를 audioSource에
- if (agent != null) agent.speed = moveSpeed; // 조건이 맞으면 설정할거에요 -> 에이전트가 있다면 이동 속도를 moveSpeed로
+ mobRenderer = GetComponentInChildren(); // 가져올거에요 -> 렌더러를
+ animator = GetComponent(); // 가져올거에요 -> 애니메이터를
+ agent = GetComponent(); // 가져올거에요 -> 에이전트를
+ audioSource = GetComponent(); // 가져올거에요 -> 오디오 소스를
+ if (agent != null) agent.speed = moveSpeed; // 설정할거에요 -> 이동 속도를
+ statusProcessor = new StatusEffectProcessor(this, this, agent, animator); // 생성할거에요 -> 상태이상 처리기를
}
- protected virtual void OnEnable() // 함수를 실행할거에요 -> 오브젝트가 활성화될 때 호출되는 OnEnable을
+ protected virtual void OnEnable() // 함수를 실행할거에요 -> 활성화 시
{
- playerTransform = GameObject.FindGameObjectWithTag("Player")?.transform; // 오브젝트를 찾을거에요 -> "Player" 태그를 가진 오브젝트의 위치를 playerTransform에
- if (mobRenderer != null) mobRenderer.enabled = true; // 조건이 맞으면 실행할거에요 -> 렌더러가 있다면 보이게 켜기(true)를
- Init(); // 함수를 실행할거에요 -> 자식 클래스에서 정의할 초기화 함수 Init을
- if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.RegisterMob(this); // 조건이 맞으면 실행할거에요 -> 매니저가 있다면 나(this)를 관리 목록에 등록하기를
+ playerTransform = GameObject.FindGameObjectWithTag("Player")?.transform; // 찾을거에요 -> 플레이어를
+ if (mobRenderer != null) mobRenderer.enabled = true; // 켤거에요 -> 렌더러를
+ Init(); // 실행할거에요 -> 자식 초기화를
+ if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.RegisterMob(this); // 등록할거에요 -> 매니저에
}
- protected virtual void OnDisable() // 함수를 실행할거에요 -> 오브젝트가 비활성화될 때 호출되는 OnDisable을
+ protected virtual void OnDisable() // 함수를 실행할거에요 -> 비활성화 시
{
- if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.UnregisterMob(this); // 조건이 맞으면 실행할거에요 -> 매니저가 있다면 나(this)를 관리 목록에서 제외하기를
+ if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.UnregisterMob(this); // 해제할거에요 -> 매니저에서
}
- // ⭐ [수정] Update를 virtual로 선언하여 자식이 재정의 가능하게 함
- protected virtual void Update()
+ protected virtual void Update() => OnManagedUpdate(); // 실행할거에요 -> 관리형 업데이트를
+
+ public void ResetStats() // 함수를 선언할거에요 -> 스탯 초기화를
{
- OnManagedUpdate(); // 기본적으로 매니저 업데이트 로직을 수행 (일반 몬스터용)
+ isDead = false; IsAggroed = false; currentHP = maxHP; // 초기화할거에요 -> 상태와 체력을
+ OnHealthChanged?.Invoke(currentHP, maxHP); // 알릴거에요 -> 체력 변경을
+ if (GetComponent() != null) GetComponent().enabled = true; // 켤거에요 -> 콜라이더를
+ if (agent != null) agent.speed = moveSpeed; // 복구할거에요 -> 속도를
+ OnResetStats(); // 실행할거에요 -> 추가 리셋을
}
- public void ResetStats() // 함수를 선언할거에요 -> 몬스터 상태를 초기화하는 ResetStats를
+ // ⭐ [복구] 스포너에서 호출하는 재활성화 함수
+ public virtual void Reactivate() // 함수를 선언할거에요 -> 재활성화를
{
- isDead = false; // 상태를 바꿀거에요 -> 사망 상태를 거짓(false)으로
- IsAggroed = false; // 상태를 바꿀거에요 -> 어그로 상태를 거짓(false)으로
- currentHP = maxHP; // 값을 넣을거에요 -> 현재 체력을 최대 체력으로
- OnHealthChanged?.Invoke(currentHP, maxHP); // 이벤트를 알릴거에요 -> 체력이 변경되었음을 UI 등에
-
- Collider col = GetComponent(); // 컴포넌트를 가져올거에요 -> 내 몸의 콜라이더를 col에
- if (col != null) col.enabled = true; // 조건이 맞으면 실행할거에요 -> 콜라이더가 있다면 다시 켜기(true)를
-
- if (agent != null) agent.speed = moveSpeed; // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면 속도를 초기화하기를
-
- OnResetStats(); // 함수를 실행할거에요 -> 자식 클래스의 추가 초기화 함수 OnResetStats를
- }
-
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- // 추상 메서드 (자식이 반드시 구현)
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-
- protected virtual void Init() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 초기화 함수 Init을
- protected abstract void ExecuteAILogic(); // 추상 함수를 선언할거에요 -> 자식에서 반드시 구현해야 할 AI 로직 ExecuteAILogic을
- protected virtual void OnResetStats() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 스탯 초기화 함수 OnResetStats를
-
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- // 공통 기능
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-
- public virtual void Reactivate() // 함수를 선언할거에요 -> 몬스터를 재활성화하는 Reactivate를
- {
- if (agent != null && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 있고 바닥에 있다면
+ if (agent != null && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 정상이면
{
- agent.isStopped = false; // 명령을 내릴거에요 -> 이동 멈춤을 풀라(false)고
- if (IsAggroed && playerTransform != null) // 조건이 맞으면 실행할거에요 -> 어그로가 끌렸고 플레이어가 있다면
- {
- agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로
- }
+ agent.isStopped = false; // 명령을 내릴거에요 -> 이동 재개
+ if (IsAggroed && playerTransform != null) agent.SetDestination(playerTransform.position); // 이동할거에요 -> 플레이어에게
}
- if (mobRenderer != null) mobRenderer.enabled = true; // 조건이 맞으면 실행할거에요 -> 렌더러가 있다면 다시 켜기(true)를
+ if (mobRenderer != null) mobRenderer.enabled = true; // 켤거에요 -> 렌더러를
}
- public virtual void OnManagedUpdate() // 함수를 선언할거에요 -> 매니저가 호출해줄 업데이트 함수 OnManagedUpdate를
+ protected virtual void Init() { }
+ protected abstract void ExecuteAILogic();
+ protected virtual void OnResetStats() { }
+
+ public virtual void OnManagedUpdate() // 함수를 선언할거에요 -> 최적화 업데이트를
{
- if (isDead || playerTransform == null || !gameObject.activeInHierarchy) return; // 조건이 맞으면 중단할거에요 -> 죽었거나, 플레이어가 없거나, 꺼져 있다면
+ if (isDead || playerTransform == null || !gameObject.activeInHierarchy) return; // 중단할거에요 -> 작동 불가면
- float distance = Vector3.Distance(transform.position, playerTransform.position); // 값을 계산할거에요 -> 나와 플레이어 사이의 거리를 distance에
-
- if (distance > optimizationDistance && !IsAggroed) // 조건이 맞으면 실행할거에요 -> 거리가 멀고 어그로 상태가 아니라면
+ float dist = Vector3.Distance(transform.position, playerTransform.position); // 계산할거에요 -> 거리를
+ if (dist > optimizationDistance && !IsAggroed) // 조건이 맞으면 실행할거에요 -> 최적화 대상이면
{
- StopMovement(); // 함수를 실행할거에요 -> 움직임을 멈추는 StopMovement를
- if (mobRenderer != null && mobRenderer.enabled) mobRenderer.enabled = false; // 조건이 맞으면 실행할거에요 -> 렌더러가 켜져 있다면 끄기(false)를 (최적화)
- return; // 중단할거에요 -> AI 로직을 실행하지 않도록 함수를
+ StopMovement(); // 멈출거에요 -> 이동을
+ if (mobRenderer != null) mobRenderer.enabled = false; // 끌거에요 -> 렌더러를
+ return; // 중단할거에요 -> 로직을
}
- if (mobRenderer != null && !mobRenderer.enabled) mobRenderer.enabled = true; // 조건이 맞으면 실행할거에요 -> 렌더러가 꺼져 있다면 다시 켜기(true)를
- if (mobRenderer != null && !mobRenderer.isVisible) { StopMovement(); return; } // 조건이 맞으면 실행할거에요 -> 렌더러가 화면에 보이지 않는다면 멈추고 중단하기를
- if (agent != null && agent.isOnNavMesh && agent.isStopped) agent.isStopped = false; // 조건이 맞으면 실행할거에요 -> 에이전트가 멈춰있다면 다시 움직이게(false) 하기를
-
- ExecuteAILogic(); // 함수를 실행할거에요 -> 실제 몬스터의 AI 로직을
+ if (mobRenderer != null) mobRenderer.enabled = true; // 켤거에요 -> 렌더러를
+ if (agent != null && agent.isOnNavMesh && agent.isStopped) agent.isStopped = false; // 켤거에요 -> 이동을
+ ExecuteAILogic(); // 실행할거에요 -> AI 로직을
}
- protected void StopMovement() // 함수를 선언할거에요 -> 이동을 멈추는 StopMovement를
+ protected void StopMovement() // 함수를 선언할거에요 -> 정지 함수를
{
- if (agent != null && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 정상 작동 중이라면
- {
- agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고
- agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 이동 속도를 0으로
- }
- if (animator != null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면
- {
- animator.SetFloat("Speed", 0f); // 값을 전달할거에요 -> 속도 파라미터를 0으로
- animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니메이션(Monster_Idle)을
- }
+ if (agent != null && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 멈출거에요 -> 에이전트를
+ if (animator != null) { animator.SetFloat("Speed", 0f); animator.Play(Monster_Idle); } // 재생할거에요 -> 대기 모션을
}
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- // 피격 / 사망
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-
- public virtual void TakeDamage(float amount) // 함수를 선언할거에요 -> 외부에서 데미지를 줄 때 호출할 TakeDamage를
+ public virtual void TakeDamage(float amount) // 함수를 선언할거에요 -> 피격 처리를
{
- OnDamaged(amount); // 함수를 실행할거에요 -> 실제 피격 처리를 담당하는 OnDamaged를
+ if (isDead) return; // 중단할거에요 -> 죽었으면
+ IsAggroed = true; currentHP -= amount; // 적용할거에요 -> 어그로와 데미지를
+ OnHealthChanged?.Invoke(currentHP, maxHP); // 알릴거에요 -> UI에
+ if (currentHP <= 0) Die(); else if (!isHit) StartHit(); // 분기할거에요 -> 사망 또는 피격으로
}
- public virtual void OnDamaged(float damage) // 함수를 선언할거에요 -> 데미지 계산 로직인 OnDamaged를
+ protected virtual void StartHit() // 함수를 선언할거에요 -> 피격 시작을
{
- if (isDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽었다면
- IsAggroed = true; // 상태를 바꿀거에요 -> 공격받았으니 어그로 상태를 참(true)으로
- currentHP -= damage; // 값을 뺄거에요 -> 체력에서 데미지(damage)만큼을
- OnHealthChanged?.Invoke(currentHP, maxHP); // 이벤트를 알릴거에요 -> 체력이 깎였음을
- if (currentHP <= 0) { Die(); return; } // 조건이 맞으면 실행할거에요 -> 체력이 0 이하라면 사망 함수(Die)를
- if (!isHit) StartHit(); // 조건이 맞으면 실행할거에요 -> 피격 상태가 아니라면 피격 연출(StartHit)을
+ isHit = true; isAttacking = false; isResting = false; StopAllCoroutines(); // 초기화할거에요 -> 상태를
+ OnStartHit(); // 실행할거에요 -> 추가 처리를
+ if (agent && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 멈출거에요 -> 이동을
+ animator.Play(Monster_GetDamage, 0, 0f); // 재생할거에요 -> 피격 애니를
+ if (hitEffect) hitEffect.Play(); // 재생할거에요 -> 이펙트를
+ if (hitSound) audioSource.PlayOneShot(hitSound); // 재생할거에요 -> 소리를
}
- protected virtual void StartHit() // 함수를 선언할거에요 -> 피격 연출을 시작하는 StartHit을
+ protected virtual void OnStartHit() { if (myWeapon != null) myWeapon.DisableHitBox(); } // 가상 함수를 선언할거에요 -> 무기 끄기
+
+ public virtual void OnHitEnd() // 함수를 선언할거에요 -> 피격 종료를
{
- isHit = true; // 상태를 바꿀거에요 -> 피격 중 상태를 참(true)으로
- isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓(false)으로
- isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로
- StopAllCoroutines(); // 중단할거에요 -> 실행 중인 모든 코루틴을
-
- OnStartHit(); // 함수를 실행할거에요 -> 자식 클래스의 추가 피격 처리 OnStartHit을
-
- if (agent && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 작동 중이라면
- {
- agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고
- agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로
- }
-
- animator.Play(Monster_GetDamage, 0, 0f); // 재생할거에요 -> 피격 애니메이션(Monster_GetDamage)을
- if (hitEffect) hitEffect.Play(); // 조건이 맞으면 실행할거에요 -> 피격 이펙트가 있다면 재생하기를
- if (hitSound) audioSource.PlayOneShot(hitSound); // 조건이 맞으면 실행할거에요 -> 피격 사운드가 있다면 재생하기를
+ isHit = false; // 해제할거에요 -> 피격 상태를
+ if (agent && agent.isOnNavMesh) agent.isStopped = false; // 재개할거에요 -> 이동을
}
- protected virtual void OnStartHit() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 피격 시작 함수 OnStartHit을
-
- public virtual void OnHitEnd() // 함수를 선언할거에요 -> 피격 연출이 끝났을 때 호출할 OnHitEnd를
+ protected virtual void Die() // 함수를 선언할거에요 -> 사망 처리를
{
- isHit = false; // 상태를 바꿀거에요 -> 피격 중 상태를 거짓(false)으로
- if (agent && agent.isOnNavMesh) agent.isStopped = false; // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면 다시 움직이게(false) 하기를
+ isDead = true; IsAggroed = false; OnDie(); // 설정할거에요 -> 사망 상태를
+ OnMonsterKilled?.Invoke(expReward); // 알릴거에요 -> 경험치 지급을
+ if (GetComponent() != null) GetComponent().enabled = false; // 끌거에요 -> 충돌을
+ if (agent && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 멈출거에요 -> 이동을
+ animator.Play(Monster_Die, 0, 0f); // 재생할거에요 -> 사망 애니를
+ if (deathSound) audioSource.PlayOneShot(deathSound); // 재생할거에요 -> 사망 소리를
+ Invoke(nameof(ReturnToPool), 1.5f); // 예약할거에요 -> 풀 반환을
}
- protected virtual void Die() // 함수를 선언할거에요 -> 몬스터 사망을 처리하는 Die를
+ protected virtual void OnDie() { if (myWeapon != null) myWeapon.DisableHitBox(); } // 가상 함수를 선언할거에요 -> 사망 시 처리를
+ protected void ReturnToPool() => gameObject.SetActive(false); // 함수를 선언할거에요 -> 비활성화를
+
+ public virtual void OnAttackStart() // 함수를 선언할거에요 -> 공격 시작을
{
- if (isDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽은 상태라면
- isDead = true; // 상태를 바꿀거에요 -> 사망 상태를 참(true)으로
- IsAggroed = false; // 상태를 바꿀거에요 -> 어그로 상태를 거짓(false)으로
-
- OnDie(); // 함수를 실행할거에요 -> 자식 클래스의 추가 사망 처리 OnDie를
-
- OnMonsterKilled?.Invoke(expReward); // 이벤트를 알릴거에요 -> 몬스터 처치와 경험치 보상(expReward)을
- Collider col = GetComponent(); // 컴포넌트를 가져올거에요 -> 내 몸의 콜라이더를 col에
- if (col != null) col.enabled = false; // 조건이 맞으면 실행할거에요 -> 콜라이더가 있다면 끄기(false)를 (시체 밟기 방지)
-
- if (agent && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 작동 중이라면
- {
- agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고
- agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로
- }
-
- animator.Play(Monster_Die, 0, 0f); // 재생할거에요 -> 사망 애니메이션(Monster_Die)을
- if (deathSound) audioSource.PlayOneShot(deathSound); // 조건이 맞으면 실행할거에요 -> 사망 사운드가 있다면 재생하기를
- Invoke("ReturnToPool", 1.5f); // 예약을 걸거에요 -> 1.5초 뒤에 풀로 반환하는 ReturnToPool 함수를
+ isAttacking = true; isResting = false; // 설정할거에요 -> 공격 중으로
+ if (myWeapon != null) myWeapon.EnableHitBox(); // 켤거에요 -> 무기 판정을
}
- protected virtual void OnDie() { } // 가상 함수를 선언할거에요 -> 자식에서 덮어쓸 사망 처리 함수 OnDie를
-
- public bool IsDead => isDead; // 프로퍼티를 선언할거에요 -> 외부에서 사망 여부를 확인할 수 있는 IsDead를
- protected void ReturnToPool() => gameObject.SetActive(false); // 함수를 선언할거에요 -> 오브젝트를 비활성화해서 풀로 돌려보내는 ReturnToPool을
-
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- // ⭐ 공격 이벤트
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-
- // ⭐ [핵심 수정] 자식에서 재정의할 수 있도록 public virtual로 선언
- public virtual void OnAttackStart() // 함수를 선언할거에요 -> 공격 시작 시 호출되는 OnAttackStart를
+ public virtual void OnAttackEnd() // 함수를 선언할거에요 -> 공격 종료를
{
- isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로
- isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로
-
- if (myWeapon != null) // 조건이 맞으면 실행할거에요 -> 무기가 있다면
- {
- myWeapon.EnableHitBox(); // 함수를 실행할거에요 -> 무기의 공격 판정을 켜는 EnableHitBox를
- }
+ if (myWeapon != null) myWeapon.DisableHitBox(); // 끌거에요 -> 무기 판정을
+ isAttacking = false; // 해제할거에요 -> 공격 상태를
+ if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); // 시작할거에요 -> 휴식을
}
- // ⭐ [핵심 수정] 자식에서 재정의할 수 있도록 public virtual로 선언
- public virtual void OnAttackEnd() // 함수를 선언할거에요 -> 공격 종료 시 호출되는 OnAttackEnd를
+ protected virtual IEnumerator RestAfterAttack() // 코루틴을 선언할거에요 -> 휴식 로직을
{
- if (myWeapon != null) // 조건이 맞으면 실행할거에요 -> 무기가 있다면
- {
- myWeapon.DisableHitBox(); // 함수를 실행할거에요 -> 무기의 공격 판정을 끄는 DisableHitBox를
- }
-
- isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓(false)으로
- if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); // 조건이 맞으면 실행할거에요 -> 살아있고 안 맞았다면 휴식 코루틴(RestAfterAttack)을
+ isResting = true; // 설정할거에요 -> 휴식 중으로
+ yield return new WaitForSeconds(attackRestDuration); // 기다릴거에요 -> 휴식 시간만큼
+ isResting = false; // 해제할거에요 -> 휴식 상태를
}
- protected virtual IEnumerator RestAfterAttack() // 코루틴 함수를 선언할거에요 -> 공격 후 잠시 쉬는 RestAfterAttack을
+ public void ApplyStatusEffect(StatusEffectType type, float dmg, float dur) // 함수를 선언할거에요 -> 상태이상 적용을
{
- isResting = true; // 상태를 바꿀거에요 -> 휴식 중 상태를 참(true)으로
- yield return new WaitForSeconds(attackRestDuration); // 기다릴거에요 -> 휴식 시간(attackRestDuration)만큼
- isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로
- }
-
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- // ⭐ 상태 이상 시스템
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-
- public void ApplyStatusEffect(StatusEffectType type, float damage, float duration) // 함수를 선언할거에요 -> 상태이상을 적용하는 ApplyStatusEffect를
- {
- if (isDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽었다면
-
- switch (type) // 분기할거에요 -> 상태이상 종류(type)에 따라
- {
- case StatusEffectType.Burn: // 조건이 맞으면 실행할거에요 -> 화상(Burn)이라면
- StartCoroutine(BurnCoroutine(damage, duration)); // 코루틴을 실행할거에요 -> 화상 효과를 주는 BurnCoroutine을
- break;
- case StatusEffectType.Slow: // 조건이 맞으면 실행할거에요 -> 슬로우(Slow)라면
- StartCoroutine(SlowCoroutine(damage, duration)); // 코루틴을 실행할거에요 -> 느리게 만드는 SlowCoroutine을
- break;
- case StatusEffectType.Poison: // 조건이 맞으면 실행할거에요 -> 독(Poison)이라면
- StartCoroutine(PoisonCoroutine(damage, duration)); // 코루틴을 실행할거에요 -> 독 효과를 주는 PoisonCoroutine을
- break;
- case StatusEffectType.Shock: // 조건이 맞으면 실행할거에요 -> 충격(Shock)이라면
- TakeDamage(damage); // 함수를 실행할거에요 -> 충격 데미지를 즉시 적용하기를
- StartCoroutine(StunCoroutine(0.5f)); // 코루틴을 실행할거에요 -> 0.5초간 기절시키는 StunCoroutine을
- break;
- }
- }
-
- private IEnumerator BurnCoroutine(float tickDamage, float duration) // 코루틴 함수를 선언할거에요 -> 화상 효과를 처리할 BurnCoroutine을
- {
- float elapsed = 0f; // 변수를 초기화할거에요 -> 경과 시간을 0으로
- float tickInterval = 0.5f; // 변수를 초기화할거에요 -> 데미지 간격을 0.5초로
- while (elapsed < duration) // 반복할거에요 -> 경과 시간이 지속 시간보다 작을 동안
- {
- TakeDamage(tickDamage * tickInterval); // 함수를 실행할거에요 -> 틱 데미지를 입히는 TakeDamage를
- yield return new WaitForSeconds(tickInterval); // 기다릴거에요 -> 0.5초의 시간을
- elapsed += tickInterval; // 값을 더할거에요 -> 경과 시간에 0.5초를
- }
- }
-
- private IEnumerator SlowCoroutine(float amount, float duration) // 코루틴 함수를 선언할거에요 -> 슬로우 효과를 처리할 SlowCoroutine을
- {
- if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면
- {
- float originalSpeed = agent.speed; // 값을 저장할거에요 -> 원래 속도를 originalSpeed에
- agent.speed *= (1f - Mathf.Clamp01(amount / 100f)); // 값을 변경할거에요 -> 현재 속도를 비율(amount)만큼 줄여서
- yield return new WaitForSeconds(duration); // 기다릴거에요 -> 지속 시간(duration)만큼
- agent.speed = originalSpeed; // 값을 복구할거에요 -> 속도를 원래대로
- }
- else // 조건이 틀리면 실행할거에요 -> 에이전트가 없다면
- {
- yield return new WaitForSeconds(duration); // 기다릴거에요 -> 시간만 때우기를
- }
- }
-
- private IEnumerator PoisonCoroutine(float tickDamage, float duration) // 코루틴 함수를 선언할거에요 -> 독 효과를 처리할 PoisonCoroutine을
- {
- float elapsed = 0f; // 변수를 초기화할거에요 -> 경과 시간을 0으로
- float tickInterval = 1f; // 변수를 초기화할거에요 -> 데미지 간격을 1초로
- while (elapsed < duration) // 반복할거에요 -> 경과 시간이 지속 시간보다 작을 동안
- {
- TakeDamage(tickDamage * tickInterval); // 함수를 실행할거에요 -> 틱 데미지를 입히는 TakeDamage를
- yield return new WaitForSeconds(tickInterval); // 기다릴거에요 -> 1초의 시간을
- elapsed += tickInterval; // 값을 더할거에요 -> 경과 시간에 1초를
- }
- }
-
- private IEnumerator StunCoroutine(float duration) // 코루틴 함수를 선언할거에요 -> 기절 효과를 처리할 StunCoroutine을
- {
- if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면
- {
- agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라(true)고
- if (animator != null) animator.speed = 0; // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면 재생 속도를 0으로 (얼음 땡)
-
- yield return new WaitForSeconds(duration); // 기다릴거에요 -> 지속 시간(duration)만큼
-
- if (!isDead) // 조건이 맞으면 실행할거에요 -> 아직 살아있다면
- {
- agent.isStopped = false; // 명령을 내릴거에요 -> 이동 멈춤을 풀라(false)고
- if (animator != null) animator.speed = 1; // 조건이 맞으면 실행할거에요 -> 애니메이션 속도를 정상(1)으로
- }
- }
- else // 조건이 틀리면 실행할거에요 -> 에이전트가 없다면
- {
- yield return new WaitForSeconds(duration); // 기다릴거에요 -> 시간만 때우기를
+ if (isDead) return; // 중단할거에요 -> 죽었으면
+ switch (type)
+ { // 분기할거에요 -> 타입에 따라
+ case StatusEffectType.Burn: statusProcessor.ApplyBurn(dmg, dur); break; // 적용할거에요 -> 화상을
+ case StatusEffectType.Slow: statusProcessor.ApplySlow(dmg, dur); break; // 적용할거에요 -> 슬로우를
+ case StatusEffectType.Poison: statusProcessor.ApplyBurn(dmg, dur); break; // 적용할거에요 -> 독을
+ case StatusEffectType.Shock: statusProcessor.ApplyShock(dmg, dur); break; // 적용할거에요 -> 충격을
}
}
}
\ No newline at end of file
diff --git a/Assets/Scripts/Enemy/AI/NormalMonster.cs b/Assets/Scripts/Enemy/AI/NormalMonster.cs
index 1ff83c12..8c10cdac 100644
--- a/Assets/Scripts/Enemy/AI/NormalMonster.cs
+++ b/Assets/Scripts/Enemy/AI/NormalMonster.cs
@@ -1,182 +1,69 @@
-using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
-using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를
-using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
-using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
+using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
+using System.Collections.Generic; // 리스트를 사용할거에요 -> 제네릭을
-public class MeleeMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 MeleeMonster를
+public class MeleeMonster : MonsterClass // 클래스를 선언할거에요 -> 근접 몬스터를
{
- [Header("=== 근접 공격 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 근접 공격 설정 === 을
- [SerializeField] private float attackRange = 2f; // 변수를 선언할거에요 -> 공격 사거리(2.0)를 attackRange에
- [SerializeField] private float attackDelay = 1.5f; // 변수를 선언할거에요 -> 공격 딜레이(1.5초)를 attackDelay에
+ [Header("=== 공격 설정 ===")] // 인스펙터 제목을 달거에요 -> 공격 설정을
+ [SerializeField] private float attackRange = 2f; // 변수를 선언할거에요 -> 사거리를
+ [SerializeField] private float attackDelay = 1.5f; // 변수를 선언할거에요 -> 딜레이를
- [Header("=== 드랍 아이템 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 드랍 아이템 설정 === 을
- [Tooltip("드랍 가능한 아이템들을 여기에 여러 개 등록하세요. (랜덤 1개 드랍)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- [SerializeField] private List dropItemPrefabs; // 리스트를 선언할거에요 -> 드랍할 아이템 프리팹 목록을 dropItemPrefabs에
+ [Header("=== 드랍 ===")] // 인스펙터 제목을 달거에요 -> 드랍 설정을
+ [SerializeField] private List dropItemPrefabs; // 변수를 선언할거에요 -> 아이템 목록을
+ [Range(0, 100)][SerializeField] private float dropChance = 30f; // 변수를 선언할거에요 -> 확률을
- [Tooltip("아이템이 나올 확률 (0 ~ 100%)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- [Range(0, 100)][SerializeField] private float dropChance = 30f; // 변수를 선언할거에요 -> 드랍 확률(30%)을 dropChance에
+ [Header("애니메이션")] // 인스펙터 제목을 달거에요 -> 애니메이션을
+ [SerializeField] private string[] attackAnimations = { "Monster_Attack_1" }; // 변수를 선언할거에요 -> 공격 애니들을
+ [SerializeField] private string Monster_Walk = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니를
- private float lastAttackTime; // 변수를 선언할거에요 -> 마지막 공격 시간을 lastAttackTime에
+ private float lastAttackTime; // 변수를 선언할거에요 -> 마지막 공격 시간을
+ private int attackIndex; // 변수를 선언할거에요 -> 공격 인덱스를
- [Header("공격 / 이동 애니메이션")] // 인스펙터 창에 제목을 표시할거에요 -> 공격 / 이동 애니메이션 을
- [SerializeField] private string[] attackAnimations = { "Monster_Attack_1" }; // 배열을 선언할거에요 -> 공격 애니메이션 이름들을 attackAnimations에
- [SerializeField] private string Monster_Walk = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을 Monster_Walk에
-
- [Header("AI 상세 설정")] // 인스펙터 창에 제목을 표시할거에요 -> AI 상세 설정 을
- [SerializeField] private float stopBuffer = 0.3f; // 변수를 선언할거에요 -> 정지 여유 거리(0.3)를 stopBuffer에
- [SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에
- [SerializeField] private float patrolInterval = 2f; // 변수를 선언할거에요 -> 순찰 대기 시간(2.0)을 patrolInterval에
-
- private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에
- private float repathInterval = 0.3f; // 변수를 선언할거에요 -> 경로 갱신 주기(0.3초)를 repathInterval에
- private float nextRepathTime; // 변수를 선언할거에요 -> 다음 경로 갱신 시간을 nextRepathTime에
- private int attackIndex; // 변수를 선언할거에요 -> 현재 공격 애니메이션 인덱스를 attackIndex에
- private bool isPlayerInZone; // 변수를 선언할거에요 -> 플레이어가 감지 구역에 있는지 여부를 isPlayerInZone에
-
- protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기화 로직인 Init을
+ protected override void Init() // 함수를 실행할거에요 -> 초기화를
{
- if (agent != null) agent.stoppingDistance = attackRange - 0.4f; // 조건이 맞으면 설정할거에요 -> 에이전트 정지 거리를 사거리보다 약간 짧게
- if (animator != null) animator.applyRootMotion = false; // 조건이 맞으면 설정할거에요 -> 애니메이션 이동(Root Motion)을 끄기로
+ if (agent != null) agent.stoppingDistance = attackRange - 0.4f; // 설정할거에요 -> 정지 거리를
+ if (animator != null) animator.applyRootMotion = false; // 끌거에요 -> 루트 모션을
}
- protected override void OnResetStats() // 함수를 덮어씌워 실행할거에요 -> 스탯 초기화 로직인 OnResetStats를
+ protected override void OnResetStats() => myWeapon?.SetDamage(attackDamage); // 함수를 실행할거에요 -> 무기 데미지 설정을
+
+ protected override void ExecuteAILogic() // 함수를 실행할거에요 -> AI 로직을
{
- if (myWeapon != null) // 조건이 맞으면 실행할거에요 -> 무기가 있다면
+ if (isHit || isAttacking || isResting) return; // 중단할거에요 -> 행동 불가면
+
+ float dist = Vector3.Distance(transform.position, playerTransform.position); // 계산할거에요 -> 거리를
+ if (dist <= attackRange) TryAttack(); // 분기할거에요 -> 가까우면 공격을
+ else MoveToPlayer(); // 분기할거에요 -> 멀면 이동을
+ }
+
+ private void MoveToPlayer() // 함수를 선언할거에요 -> 이동 로직을
+ {
+ if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(playerTransform.position); } // 이동할거에요 -> 플레이어에게
+ if (agent.velocity.magnitude > 0.1f) animator.Play(Monster_Walk); // 재생할거에요 -> 걷기 애니를
+ else animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니를
+ }
+
+ private void TryAttack() // 함수를 선언할거에요 -> 공격 시도를
+ {
+ if (Time.time < lastAttackTime + attackDelay) return; // 중단할거에요 -> 쿨타임이면
+ lastAttackTime = Time.time; // 갱신할거에요 -> 공격 시간을
+ isAttacking = true; // 설정할거에요 -> 공격 중으로
+ if (agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 멈출거에요 -> 이동을
+
+ string animName = attackAnimations[attackIndex]; // 가져올거에요 -> 애니 이름을
+ attackIndex = (attackIndex + 1) % attackAnimations.Length; // 갱신할거에요 -> 인덱스를
+ animator.Play(animName, 0, 0f); // 재생할거에요 -> 공격 애니를
+ }
+
+ protected override void OnDie() // 함수를 실행할거에요 -> 사망 시
+ {
+ base.OnDie(); // 실행할거에요 -> 부모 사망 로직을
+ if (dropItemPrefabs != null && dropItemPrefabs.Count > 0) // 조건이 맞으면 실행할거에요 -> 아이템이 있으면
{
- myWeapon.SetDamage(attackDamage); // 함수를 실행할거에요 -> 무기 공격력을 몬스터 공격력으로 설정을
- }
- }
-
- protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을
- {
- if (isHit || isAttacking || isResting) return; // 조건이 맞으면 중단할거에요 -> 피격, 공격, 휴식 중이라면
-
- float distance = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 나와 플레이어 사이의 거리를 distance에
-
- if (isPlayerInZone || distance <= attackRange * 2f) // 조건이 맞으면 실행할거에요 -> 감지 구역 안이거나 거리가 가까우면
- HandlePlayerTarget(); // 함수를 실행할거에요 -> 추격 및 공격 처리를 하는 HandlePlayerTarget을
- else // 조건이 틀리면 실행할거에요 -> 멀리 있다면
- Patrol(); // 함수를 실행할거에요 -> 주변을 배회하는 Patrol을
-
- UpdateMovementAnimation(); // 함수를 실행할거에요 -> 애니메이션 상태를 갱신하는 UpdateMovementAnimation을
- }
-
- void HandlePlayerTarget() // 함수를 선언할거에요 -> 타겟 처리 로직인 HandlePlayerTarget을
- {
- float distance = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의 거리를
-
- if (distance <= attackRange - stopBuffer) // 조건이 맞으면 실행할거에요 -> 사거리 안쪽으로 충분히 들어왔다면
- TryAttack(); // 함수를 실행할거에요 -> 공격을 시도하는 TryAttack을
- else if (Time.time >= nextRepathTime) // 조건이 맞으면 실행할거에요 -> 경로 갱신 시간이 되었다면
- {
- if (agent.isOnNavMesh) agent.SetDestination(playerTransform.position); // 명령을 내릴거에요 -> 목적지를 플레이어 위치로
- nextRepathTime = Time.time + repathInterval; // 값을 갱신할거에요 -> 다음 경로 갱신 시간을
- }
- }
-
- void TryAttack() // 함수를 선언할거에요 -> 공격을 수행하는 TryAttack을
- {
- if (Time.time < lastAttackTime + attackDelay) return; // 조건이 맞으면 중단할거에요 -> 아직 쿨타임이 안 지났다면
- lastAttackTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 공격 시간으로
-
- string attackName = attackAnimations[attackIndex]; // 값을 가져올거에요 -> 현재 인덱스의 공격 애니메이션 이름을
- attackIndex = (attackIndex + 1) % attackAnimations.Length; // 값을 갱신할거에요 -> 다음 공격 인덱스로 (순환)
-
- isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로
-
- if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 바닥에 있다면
- {
- agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고
- agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 속도를 0으로
- }
-
- animator.Play(attackName, 0, 0f); // 재생할거에요 -> 공격 애니메이션을 처음부터
- }
-
- void UpdateMovementAnimation() // 함수를 선언할거에요 -> 이동 애니메이션을 제어하는 UpdateMovementAnimation을
- {
- if (isAttacking || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면
-
- if (agent.velocity.magnitude < 0.1f) // 조건이 맞으면 실행할거에요 -> 거의 멈춰 있다면
- animator.Play(Monster_Idle); // 재생할거에요 -> 대기 애니메이션을
- else // 조건이 틀리면 실행할거에요 -> 움직이고 있다면
- animator.Play(Monster_Walk); // 재생할거에요 -> 걷기 애니메이션을
- }
-
- void Patrol() // 함수를 선언할거에요 -> 순찰 로직인 Patrol을
- {
- if (Time.time < nextPatrolTime) return; // 조건이 맞으면 중단할거에요 -> 아직 대기 시간이라면
-
- Vector3 randomPoint = transform.position + new Vector3( // 벡터를 계산할거에요 -> 현재 위치 주변의 랜덤한 지점을
- Random.Range(-patrolRadius, patrolRadius),
- 0,
- Random.Range(-patrolRadius, patrolRadius)
- );
-
- if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) // 조건이 맞으면 실행할거에요 -> 랜덤 지점이 NavMesh 위라면
- if (agent.isOnNavMesh) agent.SetDestination(hit.position); // 명령을 내릴거에요 -> 그 지점으로 이동하라고
-
- nextPatrolTime = Time.time + patrolInterval; // 값을 갱신할거에요 -> 다음 순찰 시간을
- }
-
- public override void OnAttackStart() // 함수를 덮어씌워 실행할거에요 -> 공격 시작 시 호출되는 OnAttackStart를
- {
- isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로
- isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로
- if (myWeapon != null) myWeapon.EnableHitBox(); // 조건이 맞으면 실행할거에요 -> 무기 공격 판정을 켜기를
- }
-
- public override void OnAttackEnd() // 함수를 덮어씌워 실행할거에요 -> 공격 종료 시 호출되는 OnAttackEnd를
- {
- if (myWeapon != null) myWeapon.DisableHitBox(); // 조건이 맞으면 실행할거에요 -> 무기 공격 판정을 끄기를
- isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓(false)으로
- if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); // 조건이 맞으면 실행할거에요 -> 살아있고 안 맞았다면 휴식 코루틴을
- }
-
- protected override IEnumerator RestAfterAttack() // 코루틴 함수를 덮어씌워 실행할거에요 -> 공격 후 휴식하는 RestAfterAttack을
- {
- isResting = true; // 상태를 바꿀거에요 -> 휴식 중 상태를 참(true)으로
- yield return new WaitForSeconds(attackRestDuration); // 기다릴거에요 -> 휴식 시간만큼
- isResting = false; // 상태를 바꿀거에요 -> 휴식 중 상태를 거짓(false)으로
- }
-
- protected override void OnStartHit() // 함수를 덮어씌워 실행할거에요 -> 피격 시작 시 호출되는 OnStartHit을
- {
- if (myWeapon != null) myWeapon.DisableHitBox(); // 조건이 맞으면 실행할거에요 -> 피격 당했으니 공격 판정을 끄기를
- }
-
- // 🎲 [핵심 수정] 리스트에서 랜덤 뽑기 + 확률 체크
- protected override void OnDie() // 함수를 덮어씌워 실행할거에요 -> 사망 시 호출되는 OnDie를
- {
- if (myWeapon != null) myWeapon.DisableHitBox(); // 조건이 맞으면 실행할거에요 -> 공격 판정을 끄기를
-
- // 1. 리스트에 아이템이 하나라도 있는지 확인
- if (dropItemPrefabs != null && dropItemPrefabs.Count > 0) // 조건이 맞으면 실행할거에요 -> 드랍 테이블이 비어있지 않다면
- {
- // 2. 확률 체크 (0 ~ 100)
- float randomValue = Random.Range(0f, 100f); // 값을 뽑을거에요 -> 0부터 100 사이의 랜덤값을
-
- if (randomValue <= dropChance) // 당첨! // 조건이 맞으면 실행할거에요 -> 랜덤값이 드랍 확률 이하라면
+ if (Random.Range(0f, 100f) <= dropChance) // 조건이 맞으면 실행할거에요 -> 확률 당첨되면
{
- // 3. 리스트에서 랜덤하게 하나 뽑기 (0번 ~ 마지막 번호 중 하나)
- int randomIndex = Random.Range(0, dropItemPrefabs.Count); // 값을 뽑을거에요 -> 아이템 리스트 인덱스를 랜덤으로
- GameObject selectedItem = dropItemPrefabs[randomIndex]; // 오브젝트를 가져올거에요 -> 선택된 아이템 프리팹을
-
- if (selectedItem != null) // 조건이 맞으면 실행할거에요 -> 아이템이 유효하다면
- {
- Instantiate(selectedItem, transform.position + Vector3.up * 0.5f, Quaternion.identity); // 생성할거에요 -> 아이템을 몬스터 위치에
- }
+ int idx = Random.Range(0, dropItemPrefabs.Count); // 뽑을거에요 -> 랜덤 인덱스를
+ if (dropItemPrefabs[idx] != null) Instantiate(dropItemPrefabs[idx], transform.position + Vector3.up * 0.5f, Quaternion.identity); // 생성할거에요 -> 아이템을
}
}
}
-
- private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 트리거에 들어왔을 때 OnTriggerEnter를
- {
- if (other.CompareTag("Player")) isPlayerInZone = true; // 조건이 맞으면 실행할거에요 -> 플레이어라면 감지 구역 진입 상태를 참(true)으로
- }
-
- private void OnTriggerExit(Collider other) // 함수를 실행할거에요 -> 트리거에서 나갔을 때 OnTriggerExit을
- {
- if (other.CompareTag("Player")) isPlayerInZone = false; // 조건이 맞으면 실행할거에요 -> 플레이어라면 감지 구역 진입 상태를 거짓(false)으로
- }
}
\ No newline at end of file
diff --git a/Assets/Scripts/Enemy/AI/Range Monster.cs b/Assets/Scripts/Enemy/AI/Range Monster.cs
index 66a3d0d3..2bbeb5af 100644
--- a/Assets/Scripts/Enemy/AI/Range Monster.cs
+++ b/Assets/Scripts/Enemy/AI/Range Monster.cs
@@ -1,208 +1,75 @@
-using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
-using UnityEngine.AI; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를
-using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
+using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
-///
-/// 통합 원거리 몬스터 (키 작은 플레이어 머리 조준 기능 추가)
-/// 파일 이름을 반드시 [ UniversalRangedMonster.cs ] 로 맞춰주세요!
-///
-public class UniversalRangedMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 UniversalRangedMonster를
+public class UniversalRangedMonster : MonsterClass // 클래스를 선언할거에요 -> 원거리 몬스터를
{
- public enum AttackStyle { Straight, Lob } // 열거형을 정의할거에요 -> 공격 방식(직선, 곡사)을 정의하는 AttackStyle을
+ public enum AttackStyle { Straight, Lob } // 열거형을 정의할거에요 -> 공격 타입을
+ [Header("=== 원거리 설정 ===")] // 인스펙터 제목을 달거에요 -> 원거리 설정을
+ [SerializeField] private AttackStyle attackStyle = AttackStyle.Straight; // 변수를 선언할거에요 -> 스타일을
+ [SerializeField] private GameObject projectilePrefab; // 변수를 선언할거에요 -> 투사체를
+ [SerializeField] private Transform firePoint; // 변수를 선언할거에요 -> 발사 위치를
+ [SerializeField] private float attackRange = 10f; // 변수를 선언할거에요 -> 사거리를
+ [SerializeField] private float attackDelay = 2f; // 변수를 선언할거에요 -> 딜레이를
+ [SerializeField] private float projectileSpeed = 20f; // 변수를 선언할거에요 -> 속도를
- [Header("=== 🏹 공격 스타일 선택 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 🏹 공격 스타일 선택 === 을
- [SerializeField] private AttackStyle attackStyle = AttackStyle.Straight; // 변수를 선언할거에요 -> 공격 스타일(기본 직선)을 attackStyle에
+ [Header("곡사 설정")] // 인스펙터 제목을 달거에요 -> 곡사 설정을
+ [SerializeField] private float launchAngle = 45f; // 변수를 선언할거에요 -> 각도를
+ [SerializeField] private float aimHeightOffset = 1.2f; // 변수를 선언할거에요 -> 높이 보정을
- [Header("공통 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 공통 설정 을
- [SerializeField] private GameObject projectilePrefab; // 변수를 선언할거에요 -> 발사체 프리팹을 projectilePrefab에
- [SerializeField] private Transform firePoint; // 변수를 선언할거에요 -> 발사 위치를 firePoint에
- [SerializeField] private float attackRange = 10f; // 변수를 선언할거에요 -> 공격 사거리(10.0)를 attackRange에
- [SerializeField] private float attackDelay = 2f; // 변수를 선언할거에요 -> 공격 딜레이(2.0초)를 attackDelay에
- [SerializeField] private float detectRange = 15f; // 변수를 선언할거에요 -> 인식 거리(15.0)를 detectRange에
+ [Header("애니메이션")] // 인스펙터 제목을 달거에요 -> 애니메이션을
+ [SerializeField] private string attackAnim = "Monster_Attack"; // 변수를 선언할거에요 -> 공격 애니를
+ [SerializeField] private string walkAnim = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니를
- [Header("🔹 직선 발사 설정 (활/총)")] // 인스펙터 창에 제목을 표시할거에요 -> 🔹 직선 발사 설정 (활/총) 을
- [SerializeField] private float projectileSpeed = 20f; // 변수를 선언할거에요 -> 투사체 속도(20.0)를 projectileSpeed에
- [SerializeField] private float minDistance = 5f; // 변수를 선언할거에요 -> 최소 거리(도망가는 거리)를 minDistance에
+ private float lastAttackTime; // 변수를 선언할거에요 -> 마지막 공격 시간을
- [Header("🔸 곡사 투척 설정 (돌/폭탄)")] // 인스펙터 창에 제목을 표시할거에요 -> 🔸 곡사 투척 설정 (돌/폭탄) 을
- [Tooltip("체크하면 거리/높이를 계산해서 정확히 맞춥니다.")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- [SerializeField] private bool usePreciseLob = true; // 변수를 선언할거에요 -> 정밀 곡사 사용 여부를 usePreciseLob에
+ protected override void Init() { if (agent != null) agent.stoppingDistance = attackRange * 0.8f; } // 초기화할거에요 -> 정지 거리를
- [Tooltip("던지는 각도 (45도가 최대 사거리)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- [Range(15f, 75f)][SerializeField] private float launchAngle = 45f; // 변수를 선언할거에요 -> 발사 각도(45도)를 launchAngle에
-
- // ⭐ [추가됨] 조준 높이 보정 (키 작은 플레이어 해결용)
- [Tooltip("0이면 발가락, 1.0이면 명치, 1.5면 머리를 노립니다.")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- [SerializeField] private float aimHeight = 1.2f; // 변수를 선언할거에요 -> 조준 높이 오프셋(1.2)을 aimHeight에
-
- [Header("🏃♂️ 도망 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 🏃♂️ 도망 설정 을
- [SerializeField] private float fleeDistance = 5f; // 변수를 선언할거에요 -> 도망가는 거리(5.0)를 fleeDistance에
-
- [Header("애니메이션 & 기타")] // 인스펙터 창에 제목을 표시할거에요 -> 애니메이션 & 기타 를
- [SerializeField] private float throwForce = 15f; // 변수를 선언할거에요 -> 기본 투척 힘을 throwForce에
- [SerializeField] private float throwUpward = 5f; // 변수를 선언할거에요 -> 기본 상향 힘을 throwUpward에
- [SerializeField] private float reloadTime = 2.0f; // 변수를 선언할거에요 -> 장전 시간(2.0초)을 reloadTime에
- [SerializeField] private GameObject handModel; // 변수를 선언할거에요 -> 손에 든 무기 모델을 handModel에
- [SerializeField] private bool aimAtPlayer = true; // 변수를 선언할거에요 -> 플레이어 조준 여부를 aimAtPlayer에
-
- [SerializeField] private string attackAnim = "Monster_Attack"; // 변수를 선언할거에요 -> 공격 애니메이션 이름을 attackAnim에
- [SerializeField] private string walkAnim = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을 walkAnim에
- [SerializeField] private float patrolRadius = 5f; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에
- [SerializeField] private float patrolInterval = 3f; // 변수를 선언할거에요 -> 순찰 간격(3.0초)을 patrolInterval에
-
- private float lastAttackTime; // 변수를 선언할거에요 -> 마지막 공격 시간을 lastAttackTime에
- private float nextPatrolTime; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에
- private bool isReloading = false; // 변수를 선언할거에요 -> 장전 중 여부를 isReloading에
-
- protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 초기화 로직인 Init을
+ protected override void ExecuteAILogic() // 함수를 실행할거에요 -> AI 로직을
{
- if (agent != null) { agent.stoppingDistance = attackRange * 0.8f; agent.speed = 3.5f; } // 조건이 맞으면 설정할거에요 -> 정지 거리를 사거리의 80%로, 속도를 3.5로
- if (animator != null) animator.applyRootMotion = false; // 조건이 맞으면 설정할거에요 -> 애니메이션 이동을 끄기로
+ if (isHit || isAttacking || isResting) return; // 중단할거에요 -> 행동 불가면
+ float dist = Vector3.Distance(transform.position, playerTransform.position); // 계산할거에요 -> 거리를
+ if (dist <= attackRange) TryAttack(); else ChasePlayer(); // 분기할거에요 -> 공격 또는 추격으로
}
- protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을
+ private void ChasePlayer() // 함수를 선언할거에요 -> 추격 로직을
{
- if (isHit || isAttacking || isResting || isReloading) return; // 조건이 맞으면 중단할거에요 -> 피격, 공격, 휴식, 장전 중이라면
- if (isAttacking && animator.GetCurrentAnimatorStateInfo(0).IsName(Monster_Idle)) OnAttackEnd(); // 조건이 맞으면 실행할거에요 -> 공격 중인데 모션이 대기라면 강제로 공격 종료를
+ if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(playerTransform.position); } // 이동할거에요 -> 플레이어에게
+ animator.Play(walkAnim); // 재생할거에요 -> 걷기 애니를
+ }
- float dist = Vector3.Distance(transform.position, playerTransform.position); // 거리를 계산할거에요 -> 플레이어와의 거리를
+ private void TryAttack() // 함수를 선언할거에요 -> 공격 시도를
+ {
+ if (Time.time < lastAttackTime + attackDelay) return; // 중단할거에요 -> 쿨타임 중이면
+ lastAttackTime = Time.time; isAttacking = true; // 갱신할거에요 -> 시간과 상태를
+ if (agent.isOnNavMesh) agent.isStopped = true; // 멈출거에요 -> 이동을
- if (dist <= detectRange) HandleCombat(dist); // 조건이 맞으면 실행할거에요 -> 감지 거리 이내라면 전투 처리(HandleCombat)를
- else // 조건이 틀리면 실행할거에요 -> 멀리 있다면
+ Vector3 dir = (playerTransform.position - transform.position).normalized; dir.y = 0; // 계산할거에요 -> 방향을
+ transform.rotation = Quaternion.LookRotation(dir); // 회전할거에요 -> 방향대로
+ animator.Play(attackAnim); // 재생할거에요 -> 공격 애니를
+ }
+
+ public override void OnAttackStart() // 함수를 실행할거에요 -> 공격 이벤트 발생 시
+ {
+ if (projectilePrefab == null || firePoint == null) return; // 중단할거에요 -> 설정 없으면
+ GameObject obj = Instantiate(projectilePrefab, firePoint.position, transform.rotation); // 생성할거에요 -> 투사체를
+ Projectile proj = obj.GetComponent(); // 가져올거에요 -> 스크립트를
+
+ if (attackStyle == AttackStyle.Straight) // 조건이 맞으면 실행할거에요 -> 직선형이면
{
- if (agent.hasPath && agent.destination == playerTransform.position) { agent.ResetPath(); nextPatrolTime = 0; } // 조건이 맞으면 실행할거에요 -> 추격 중이었다면 경로를 취소하고 순찰을 준비하기를
- Patrol(); // 함수를 실행할거에요 -> 순찰을 도는 Patrol을
- UpdateMovementAnimation(); // 함수를 실행할거에요 -> 애니메이션 갱신을
+ if (proj != null) proj.Initialize(transform.forward, projectileSpeed, attackDamage); // 초기화할거에요 -> 직선 발사로
}
- }
-
- void HandleCombat(float dist) // 함수를 선언할거에요 -> 거리별 전투 행동을 정하는 HandleCombat을
- {
- if (attackStyle == AttackStyle.Straight && dist < minDistance) RetreatFromPlayer(); // 조건이 맞으면 실행할거에요 -> 직선 공격 타입이고 너무 가까우면 도망가기를
- else if (dist <= attackRange) TryAttack(); // 조건이 맞으면 실행할거에요 -> 사거리 안이라면 공격 시도를
- else { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(playerTransform.position); UpdateMovementAnimation(); } } // 그 외엔 실행할거에요 -> 플레이어를 향해 추격하기를
- }
-
- void TryAttack() // 함수를 선언할거에요 -> 공격을 수행하는 TryAttack을
- {
- if (Time.time < lastAttackTime + attackDelay) return; // 조건이 맞으면 중단할거에요 -> 쿨타임이 안 지났다면
- lastAttackTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 공격 시간으로
- isAttacking = true; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로
-
- if (agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 조건이 맞으면 실행할거에요 -> 이동을 멈추고 제자리에 서기를
-
- Vector3 lookDir = (playerTransform.position - transform.position).normalized; // 벡터를 계산할거에요 -> 플레이어를 바라보는 방향을
- lookDir.y = 0; // 값을 바꿀거에요 -> 높이 차이를 무시하게
- if (lookDir != Vector3.zero) transform.rotation = Quaternion.LookRotation(lookDir); // 회전시킬거에요 -> 플레이어 쪽을 보도록
-
- animator.Play(attackAnim); // 재생할거에요 -> 공격 애니메이션을
- }
-
- public override void OnAttackStart() // 함수를 덮어씌워 실행할거에요 -> 공격 타이밍(애니메이션 이벤트)에 호출될 OnAttackStart를
- {
- if (!projectilePrefab || !firePoint) return; // 조건이 맞으면 중단할거에요 -> 프리팹이나 발사 위치가 없다면
-
- GameObject obj = Instantiate(projectilePrefab, firePoint.position, transform.rotation); // 생성할거에요 -> 투사체를 발사 위치에
-
- if (obj.TryGetComponent(out var proj)) // 조건이 맞으면 실행할거에요 -> 투사체 스크립트가 있다면
+ else // 조건이 틀리면 실행할거에요 -> 곡사형이면
{
- float speed = (attackStyle == AttackStyle.Straight) ? projectileSpeed : 0f; // 값을 결정할거에요 -> 직선이면 속도를, 곡사면 0(물리)을
- proj.Initialize(transform.forward, speed, attackDamage); // 초기화할거에요 -> 방향, 속도, 데미지 정보를 전달해서
- }
-
- if (attackStyle == AttackStyle.Lob) // 조건이 맞으면 실행할거에요 -> 곡사 공격 타입이라면
- {
- Rigidbody rb = obj.GetComponent(); // 컴포넌트를 가져올거에요 -> 투사체의 리지드바디를
- if (rb) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면
+ Rigidbody rb = obj.GetComponent(); // 가져올거에요 -> 리지드바디를
+ if (rb != null)
{
- rb.useGravity = true; // 기능을 켤거에요 -> 중력 영향을 받도록
+ if (proj != null) proj.Initialize(Vector3.zero, 0, attackDamage); // 초기화할거에요 -> 물리 이동으로
+ rb.useGravity = true; // 켤거에요 -> 중력을
+ Vector3 targetPos = playerTransform.position + Vector3.up * aimHeightOffset; // 계산할거에요 -> 타겟 위치를
+ Vector3 velocity = ProjectileMath.CalculateLobVelocity(firePoint.position, targetPos, launchAngle); // 계산할거에요 -> 헬퍼로 속도를
- // ⭐ 목표 지점 설정 (플레이어 위치 + 높이 보정)
- Vector3 targetPos = playerTransform.position + Vector3.up * aimHeight; // 위치를 계산할거에요 -> 플레이어 위치에 조준 높이를 더해서
-
- if (usePreciseLob) // 조건이 맞으면 실행할거에요 -> 정밀 곡사를 사용한다면
- {
- // 보정된 위치(targetPos)를 기준으로 탄도 계산
- Vector3 velocity = CalculateLobVelocity(firePoint.position, targetPos, launchAngle); // 계산할거에요 -> 목표에 도달하기 위한 물리 속도를
-
- if (!float.IsNaN(velocity.x)) // 조건이 맞으면 실행할거에요 -> 계산 결과가 유효하다면
- {
- rb.velocity = velocity; // 값을 넣을거에요 -> 계산된 속도를 리지드바디에
- }
- else // 조건이 틀리면 실행할거에요 -> 계산 실패(도달 불가)라면
- {
- // 계산 실패 시 백업
- rb.AddForce(transform.forward * throwForce + Vector3.up * throwUpward, ForceMode.Impulse); // 힘을 가할거에요 -> 기본 힘으로 던지기를
- }
- }
- else // 조건이 틀리면 실행할거에요 -> 정밀 곡사가 아니라면
- {
- Vector3 dir = aimAtPlayer ? (targetPos - firePoint.position).normalized : transform.forward; // 방향을 결정할거에요 -> 타겟 방향 혹은 정면으로
- rb.AddForce(dir * throwForce + Vector3.up * throwUpward, ForceMode.Impulse); // 힘을 가할거에요 -> 방향과 힘을 적용해서 던지기를
- }
+ if (velocity != Vector3.zero) rb.velocity = velocity; // 적용할거에요 -> 속도를
+ else rb.AddForce(transform.forward * 15f + Vector3.up * 5f, ForceMode.Impulse); // 실패 시 기본 힘을
}
-
- if (handModel != null) { handModel.SetActive(false); StartCoroutine(ReloadRoutine()); } // 조건이 맞으면 실행할거에요 -> 손에 있는 무기를 숨기고 장전 코루틴을 시작하기를
}
}
-
- Vector3 CalculateLobVelocity(Vector3 origin, Vector3 target, float angle) // 함수를 선언할거에요 -> 곡사 탄도 속도를 계산하는 CalculateLobVelocity를
- {
- Vector3 dir = target - origin; // 벡터를 계산할거에요 -> 목표까지의 거리 벡터를
- float height = dir.y; // 값을 저장할거에요 -> 높이 차이를
- dir.y = 0; // 값을 바꿀거에요 -> 수평 거리 계산을 위해 y를 0으로
- float dist = dir.magnitude; // 값을 저장할거에요 -> 수평 거리를
-
- float a = angle * Mathf.Deg2Rad; // 값을 변환할거에요 -> 각도를 라디안으로
-
- dir.y = dist * Mathf.Tan(a); // 값을 계산할거에요 -> 탄젠트를 이용해 높이를
- dist += height / Mathf.Tan(a); // 값을 보정할거에요 -> 높이 차이에 따른 거리 보정을
-
- float gravity = Physics.gravity.magnitude; // 값을 가져올거에요 -> 중력 가속도를
- // 물리 공식: v^2 = (g * x^2) / (2 * cos^2(a) * (x * tan(a) - y))
- float velocitySq = (gravity * dist * dist) / (2 * Mathf.Pow(Mathf.Cos(a), 2) * (dist * Mathf.Tan(a) - height)); // 값을 계산할거에요 -> 필요한 속도의 제곱을
-
- if (velocitySq <= 0) return Vector3.zero; // 조건이 맞으면 반환할거에요 -> 계산 불가라면 0 벡터를
-
- float velocity = Mathf.Sqrt(velocitySq); // 값을 계산할거에요 -> 제곱근을 구해서 실제 속도를
- return dir.normalized * velocity; // 반환할거에요 -> 방향 벡터에 속도를 곱해서
- }
-
- void RetreatFromPlayer() // 함수를 선언할거에요 -> 플레이어로부터 도망가는 RetreatFromPlayer를
- {
- if (agent.pathPending || (agent.remainingDistance > 0.5f && agent.velocity.magnitude > 0.1f)) { UpdateMovementAnimation(); return; } // 조건이 맞으면 중단할거에요 -> 이미 이동 중이라면
- Vector3 dir = (transform.position - playerTransform.position).normalized; // 벡터를 계산할거에요 -> 플레이어 반대 방향을
- Vector3 pos = transform.position + dir * fleeDistance; // 위치를 계산할거에요 -> 도망갈 목표 지점을
- if (NavMesh.SamplePosition(pos, out NavMeshHit hit, 5f, NavMesh.AllAreas)) { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(hit.position); } } // 조건이 맞으면 실행할거에요 -> 유효한 위치라면 그곳으로 이동하기를
- UpdateMovementAnimation(); // 함수를 실행할거에요 -> 이동 애니메이션 갱신을
- }
-
- IEnumerator ReloadRoutine() // 코루틴 함수를 선언할거에요 -> 무기 장전(재생성)을 처리하는 ReloadRoutine을
- {
- isReloading = true; // 상태를 바꿀거에요 -> 장전 중 상태를 참으로
- yield return new WaitForSeconds(reloadTime); // 기다릴거에요 -> 장전 시간만큼
- if (handModel != null) handModel.SetActive(true); // 조건이 맞으면 실행할거에요 -> 손에 있는 무기를 다시 보이게
- isReloading = false; // 상태를 바꿀거에요 -> 장전 중 상태를 거짓으로
- }
-
- public override void OnAttackEnd() // 함수를 덮어씌워 실행할거에요 -> 공격 종료 처리를 하는 OnAttackEnd를
- {
- isAttacking = false; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓으로
- if (animator != null) animator.Play(Monster_Idle); // 조건이 맞으면 실행할거에요 -> 대기 애니메이션으로 복귀를
- if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); // 조건이 맞으면 실행할거에요 -> 살아있다면 휴식 코루틴 시작을
- }
-
- void Patrol() // 함수를 선언할거에요 -> 순찰 로직 Patrol을
- {
- if (Time.time < nextPatrolTime) { if (agent.velocity.magnitude < 0.1f) animator.Play(Monster_Idle); return; } // 조건이 맞으면 중단할거에요 -> 대기 시간이라면 대기 모션 재생 후 리턴
- Vector3 randomPoint = transform.position + new Vector3(Random.Range(-patrolRadius, patrolRadius), 0, Random.Range(-patrolRadius, patrolRadius)); // 벡터를 계산할거에요 -> 랜덤 순찰 지점을
- if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas)) { if (agent.isOnNavMesh) { agent.isStopped = false; agent.SetDestination(hit.position); } } // 조건이 맞으면 실행할거에요 -> 유효한 위치라면 이동하기를
- nextPatrolTime = Time.time + patrolInterval; // 값을 갱신할거에요 -> 다음 순찰 시간을
- }
-
- void UpdateMovementAnimation() // 함수를 선언할거에요 -> 이동 애니메이션 갱신 함수를
- {
- if (isAttacking || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면
- if (agent.velocity.magnitude > 0.1f) animator.Play(walkAnim); else animator.Play(Monster_Idle); // 조건에 따라 재생할거에요 -> 걷기 또는 대기 애니메이션을
- }
}
\ No newline at end of file
diff --git a/Assets/Scripts/Enemy/BossAI/Animatordebugdump.cs b/Assets/Scripts/Enemy/BossAI/Animatordebugdump.cs
new file mode 100644
index 00000000..8438c13f
--- /dev/null
+++ b/Assets/Scripts/Enemy/BossAI/Animatordebugdump.cs
@@ -0,0 +1,66 @@
+using UnityEngine;
+
+///
+/// [임시 디버그용] 보스 오브젝트에 붙이고 Play하면
+/// 콘솔에 모든 AnimationClip 이름을 출력합니다.
+/// 확인 후 이 스크립트는 삭제하세요.
+///
+public class AnimatorDebugDump : MonoBehaviour
+{
+ private void Start()
+ {
+ Animator animator = GetComponent();
+ if (animator == null)
+ {
+ Debug.LogError("[AnimatorDebug] Animator 컴포넌트가 없습니다!");
+ return;
+ }
+
+ // ═══════════════════════════════════════
+ // 1) RuntimeAnimatorController의 모든 AnimationClip 이름
+ // ═══════════════════════════════════════
+ RuntimeAnimatorController rac = animator.runtimeAnimatorController;
+ if (rac != null)
+ {
+ AnimationClip[] clips = rac.animationClips;
+ Debug.Log($"══════ [AnimatorDebug] 총 클립 수: {clips.Length} ══════");
+ for (int i = 0; i < clips.Length; i++)
+ {
+ Debug.Log($" 클립[{i}]: \"{clips[i].name}\" (길이: {clips[i].length:F2}초)");
+ }
+ }
+ else
+ {
+ Debug.LogWarning("[AnimatorDebug] RuntimeAnimatorController가 null입니다!");
+ }
+
+ // ═══════════════════════════════════════
+ // 2) 현재 레이어 0의 State 이름 확인 (Play 직후)
+ // ═══════════════════════════════════════
+ AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0);
+ Debug.Log($"[AnimatorDebug] 현재 State nameHash: {stateInfo.shortNameHash}, 길이: {stateInfo.length:F2}초");
+
+ // ═══════════════════════════════════════
+ // 3) 코드에서 사용 중인 애니메이션 이름들 존재 여부 테스트
+ // ═══════════════════════════════════════
+ string[] testNames = new string[]
+ {
+ "Idle", "Roar", "Walk", "Run",
+ "Attack_Swing", "Attack_Slam", "Attack_Throw",
+ "Skill_DashReady", "Skill_Dash", "Skill_JumpUp", "Skill_JumpDown",
+ "Skill_Pickup", "GetDamage", "Die",
+ // FBX 프리픽스 버전도 테스트
+ "Armature|Idle", "Armature|Roar", "Armature|Walk",
+ "Armature|Attack_Throw", "Armature|Skill_Pickup"
+ };
+
+ Debug.Log("══════ [AnimatorDebug] State 존재 여부 테스트 ══════");
+ foreach (string name in testNames)
+ {
+ int hash = Animator.StringToHash(name);
+ bool exists = animator.HasState(0, hash);
+ string status = exists ? "✅ 존재" : "❌ 없음";
+ Debug.Log($" \"{name}\" → {status}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Enemy/BossAI/Animatordebugdump.cs.meta b/Assets/Scripts/Enemy/BossAI/Animatordebugdump.cs.meta
new file mode 100644
index 00000000..de1a2776
--- /dev/null
+++ b/Assets/Scripts/Enemy/BossAI/Animatordebugdump.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3ff6696e8923ba24eb91fcac18f67aa5
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/BossCounterConfig.asset b/Assets/Scripts/Enemy/BossAI/BossCounterConfig.asset
similarity index 59%
rename from Assets/BossCounterConfig.asset
rename to Assets/Scripts/Enemy/BossAI/BossCounterConfig.asset
index 1b3fd727..a814665a 100644
--- a/Assets/BossCounterConfig.asset
+++ b/Assets/Scripts/Enemy/BossAI/BossCounterConfig.asset
@@ -12,14 +12,16 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 127d57ae132759d4f8233580c8075e39, type: 3}
m_Name: BossCounterConfig
m_EditorClassIdentifier:
- dodgeThreshold: 5
- aimThreshold: 4
- pierceThreshold: 0.6
- minShotsForPierceCheck: 3
- unlockedThresholdMultiplier: 0.8
- counterWeightBonus: 3
- counterSubWeightBonus: 2
- counterDecayTime: 8
- maxCounterFrequency: 0.25
- counterCooldown: 5
- habitChangeRewardRatio: 0.15
+ patterns:
+ - patternName:
+ targetCounter: 0
+ baseWeight: 0
+ weightMultiplier: 0
+ - patternName:
+ targetCounter: 0
+ baseWeight: 0
+ weightMultiplier: 0
+ - patternName:
+ targetCounter: 0
+ baseWeight: 0
+ weightMultiplier: 0
diff --git a/Assets/BossCounterConfig.asset.meta b/Assets/Scripts/Enemy/BossAI/BossCounterConfig.asset.meta
similarity index 100%
rename from Assets/BossCounterConfig.asset.meta
rename to Assets/Scripts/Enemy/BossAI/BossCounterConfig.asset.meta
diff --git a/Assets/Scripts/Enemy/BossAI/BossCounterConfig.cs b/Assets/Scripts/Enemy/BossAI/BossCounterConfig.cs
index b951280d..706ba029 100644
--- a/Assets/Scripts/Enemy/BossAI/BossCounterConfig.cs
+++ b/Assets/Scripts/Enemy/BossAI/BossCounterConfig.cs
@@ -1,75 +1,17 @@
-using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
+using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
+using System.Collections.Generic; // 리스트를 사용할거에요 -> 제네릭을
-///
-/// 보스 카운터 시스템의 임계치, 가중치, 감소(Decay) 설정을 담는 ScriptableObject.
-/// Inspector에서 밸런싱을 위해 쉽게 조정할 수 있습니다.
-///
-[CreateAssetMenu(fileName = "BossCounterConfig", menuName = "Boss/Counter Config")] // 메뉴를 만들거에요 -> 에셋 생성 메뉴에 "Boss/Counter Config"를
-public class BossCounterConfig : ScriptableObject // 클래스를 선언할거에요 -> ScriptableObject를 상속받는 BossCounterConfig를
+[CreateAssetMenu(fileName = "BossCounterConfig", menuName = "Boss/CounterConfig")] // 메뉴를 만들거에요 -> 에셋 생성 메뉴를
+public class BossCounterConfig : ScriptableObject // 클래스를 선언할거에요 -> 데이터 저장용 SO를
{
- [Header("══ 기본 임계치 (첫 런 / 잠금 해제 전) ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 기본 임계치 (첫 런 / 잠금 해제 전) ══ 을
- [Tooltip("회피 카운터 발동 조건: 10초 내 회피 횟수")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- public int dodgeThreshold = 5; // 변수를 선언할거에요 -> 회피 횟수 임계값(5)을 dodgeThreshold에
-
- [Tooltip("조준 카운터 발동 조건: 10초 내 조준 유지 시간(초)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- public float aimThreshold = 4.0f; // 변수를 선언할거에요 -> 조준 시간 임계값(4.0초)을 aimThreshold에
-
- [Tooltip("관통 카운터 발동 조건: 10초 내 관통 비율")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- [Range(0f, 1f)] // 슬라이더를 표시할거에요 -> 0부터 1 사이로
- public float pierceThreshold = 0.6f; // 변수를 선언할거에요 -> 관통 비율 임계값(60%)을 pierceThreshold에
-
- [Tooltip("관통 비율 판단을 위한 최소 발사 횟수")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- public int minShotsForPierceCheck = 3; // 변수를 선언할거에요 -> 최소 발사 횟수(3)를 minShotsForPierceCheck에
-
- [Header("══ 잠금 해제 후 임계치 감소 ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 잠금 해제 후 임계치 감소 ══ 를
- [Tooltip("잠금 해제된 카운터의 임계치 감소 비율 (0.8 = 20% 낮아짐)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- [Range(0.5f, 1.0f)] // 슬라이더를 표시할거에요 -> 0.5부터 1.0 사이로
- public float unlockedThresholdMultiplier = 0.8f; // 변수를 선언할거에요 -> 잠금 해제 시 감소 배율(0.8)을 unlockedThresholdMultiplier에
-
- [Header("══ 가중치 설정 (확률 가중치 방식) ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 가중치 설정 (확률 가중치 방식) ══ 을
- [Tooltip("카운터 발동 시 해당 카운터 패턴에 추가되는 가중치")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- public float counterWeightBonus = 3f; // 변수를 선언할거에요 -> 카운터 패턴 보너스 가중치(3.0)를 counterWeightBonus에
-
- [Tooltip("카운터 발동 시 보조 패턴에 추가되는 가중치")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- public float counterSubWeightBonus = 2f; // 변수를 선언할거에요 -> 보조 패턴 보너스 가중치(2.0)를 counterSubWeightBonus에
-
- [Header("══ Decay(감소) 설정 ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ Decay(감소) 설정 ══ 을
- [Tooltip("카운터 모드 유지 시간(초). 이 시간 동안 조건 미충족 시 비활성화")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- public float counterDecayTime = 8f; // 변수를 선언할거에요 -> 카운터 유지 시간(8.0초)을 counterDecayTime에
-
- [Header("══ 빈도 제한 ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 빈도 제한 ══ 을
- [Tooltip("카운터 패턴 선택 확률 상한 (0.25 = 최대 25%)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- [Range(0.1f, 0.5f)] // 슬라이더를 표시할거에요 -> 0.1부터 0.5 사이로
- public float maxCounterFrequency = 0.25f; // 변수를 선언할거에요 -> 카운터 최대 빈도(25%)를 maxCounterFrequency에
-
- [Tooltip("카운터 패턴 발동 후 쿨타임(초)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- public float counterCooldown = 5f; // 변수를 선언할거에요 -> 카운터 재사용 대기시간(5.0초)을 counterCooldown에
-
- [Header("══ 습관 변경 보상 ══")] // 인스펙터 창에 제목을 표시할거에요 -> ══ 습관 변경 보상 ══ 을
- [Tooltip("플레이어가 이전 런에서 발동된 카운터 습관을 이번 런에서 안 보이면, 보스 일반 패턴 난이도 감소 비율")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- [Range(0f, 0.3f)] // 슬라이더를 표시할거에요 -> 0부터 0.3 사이로
- public float habitChangeRewardRatio = 0.15f; // 변수를 선언할거에요 -> 습관 변경 보상 비율(15%)을 habitChangeRewardRatio에
-
- // ═══════════════════════════════════════════
- // 임계치 조회 (잠금 해제 여부 반영)
- // ═══════════════════════════════════════════
-
- /// 현재 유효 임계치 반환 (잠금 해제 시 낮아짐)
- public int GetEffectiveDodgeThreshold(bool isUnlocked) // 함수를 선언할거에요 -> 잠금 해제 여부에 따른 회피 임계값을 반환하는 GetEffectiveDodgeThreshold를
+ [System.Serializable] // 직렬화할거에요 -> 인스펙터에 보이게
+ public struct PatternWeight // 구조체를 정의할거에요 -> 패턴 가중치 정보를 담을
{
- if (!isUnlocked) return dodgeThreshold; // 조건이 맞으면 반환할거에요 -> 잠금 상태라면 기본 임계값을
- return Mathf.Max(2, Mathf.RoundToInt(dodgeThreshold * unlockedThresholdMultiplier)); // 값을 계산해서 반환할거에요 -> 기본값에 배율을 곱하고 최소 2 이상이 되도록
+ public string patternName; // 변수를 선언할거에요 -> 패턴 이름을
+ public CounterType targetCounter; // 변수를 선언할거에요 -> 반응할 카운터 타입을
+ public float baseWeight; // 변수를 선언할거에요 -> 기본 확률을
+ public float weightMultiplier; // 변수를 선언할거에요 -> 카운터당 증가할 확률을
}
- public float GetEffectiveAimThreshold(bool isUnlocked) // 함수를 선언할거에요 -> 잠금 해제 여부에 따른 조준 임계값을 반환하는 GetEffectiveAimThreshold를
- {
- if (!isUnlocked) return aimThreshold; // 조건이 맞으면 반환할거에요 -> 잠금 상태라면 기본 임계값을
- return aimThreshold * unlockedThresholdMultiplier; // 값을 계산해서 반환할거에요 -> 기본값에 배율을 곱한 값을
- }
-
- public float GetEffectivePierceThreshold(bool isUnlocked) // 함수를 선언할거에요 -> 잠금 해제 여부에 따른 관통 임계값을 반환하는 GetEffectivePierceThreshold를
- {
- if (!isUnlocked) return pierceThreshold; // 조건이 맞으면 반환할거에요 -> 잠금 상태라면 기본 임계값을
- return pierceThreshold * unlockedThresholdMultiplier; // 값을 계산해서 반환할거에요 -> 기본값에 배율을 곱한 값을
- }
+ public List patterns; // 리스트를 선언할거에요 -> 패턴 설정 목록을
}
\ No newline at end of file
diff --git a/Assets/Scripts/Enemy/BossAI/BossCounterDebugPanel.cs b/Assets/Scripts/Enemy/BossAI/BossCounterDebugPanel.cs
index 60ae8c9d..4b38ab49 100644
--- a/Assets/Scripts/Enemy/BossAI/BossCounterDebugPanel.cs
+++ b/Assets/Scripts/Enemy/BossAI/BossCounterDebugPanel.cs
@@ -1,93 +1,32 @@
-using UnityEngine;
+using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
+using UnityEngine.UI; // UI 기능을 불러올거에요 -> UnityEngine.UI를
+using System.Text; // 문자열 빌더를 사용할거에요 -> System.Text를
-///
-/// 개발 중 보스 카운터 시스템을 테스트하기 위한 디버그 패널.
-/// 빌드 시 자동으로 비활성화됩니다.
-///
-public class BossCounterDebugPanel : MonoBehaviour
+public class BossCounterDebugPanel : MonoBehaviour // 클래스를 선언할거에요 -> 디버그 패널을
{
- [SerializeField] private BossCounterSystem counterSystem;
- [SerializeField] private bool showDebugGUI = true;
+ [SerializeField] private BossCounterSystem system; // 변수를 선언할거에요 -> 시스템 참조를
+ [SerializeField] private Text debugText; // 변수를 선언할거에요 -> 텍스트 UI를
- #if UNITY_EDITOR || DEVELOPMENT_BUILD
-
- private void OnGUI()
+ private void Update() // 함수를 실행할거에요 -> 매 프레임
{
- if (!showDebugGUI) return;
+ if (!system || !debugText) return; // 중단할거에요 -> 참조가 없으면
- GUILayout.BeginArea(new Rect(10, 10, 400, 500));
- GUILayout.BeginVertical("box");
+ StringBuilder sb = new StringBuilder(); // 생성할거에요 -> 빌더 객체를
+ sb.AppendLine("=== Boss Counter Debug ==="); // 추가할거에요 -> 제목 줄을
- GUILayout.Label("═══ 보스 카운터 디버그 ═══", GUI.skin.box);
+ var counters = system.GetActiveCounters(); // 가져올거에요 -> 활성 카운터 목록을
+ foreach (var pair in counters) // 반복할거에요 -> 각 카운터마다
+ sb.AppendLine($"{pair.Key}: {pair.Value}"); // 추가할거에요 -> 키와 값을
- // ── 플레이어 행동 데이터 ──
- var tracker = PlayerBehaviorTracker.Instance;
- if (tracker != null)
- {
- GUILayout.Label("── 플레이어 행동 (10초 윈도우) ──");
- GUILayout.Label($" 회피 횟수: {tracker.DodgeCount}");
- GUILayout.Label($" 조준 시간: {tracker.AimHoldTime:F1}s");
- GUILayout.Label($" 관통 비율: {tracker.PierceRatio:P0} ({tracker.TotalShotsInWindow}발)");
- }
+ // ⭐ [수정] 괄호 () 추가하여 올바르게 메서드를 호출합니다.
+ sb.AppendLine($"Reward: {system.IsHabitChangeRewarded()}"); // 추가할거에요 -> 보상 상태를
- GUILayout.Space(5);
-
- // ── 카운터 상태 ──
- if (counterSystem != null)
- {
- GUILayout.Label("── 카운터 모드 ──");
- var actives = counterSystem.GetActiveCounters();
- GUILayout.Label($" 활성 카운터: {(actives.Count > 0 ? string.Join(", ", actives) : "없음")}");
- GUILayout.Label($" 습관 변경 보상: {(counterSystem.IsHabitChangeRewarded ? "ON" : "OFF")}");
- }
-
- GUILayout.Space(5);
-
- // ── 영구 데이터 ──
- var persistence = BossCounterPersistence.Instance;
- if (persistence != null)
- {
- GUILayout.Label("── 영구 잠금 해제 ──");
- GUILayout.Label($" 회피: {(persistence.IsUnlocked(CounterType.Dodge) ? "✓" : "✗")} (발동 {persistence.Data.dodgeCounterActivations}회)");
- GUILayout.Label($" 조준: {(persistence.IsUnlocked(CounterType.Aim) ? "✓" : "✗")} (발동 {persistence.Data.aimCounterActivations}회)");
- GUILayout.Label($" 관통: {(persistence.IsUnlocked(CounterType.Pierce) ? "✓" : "✗")} (발동 {persistence.Data.pierceCounterActivations}회)");
- }
-
- GUILayout.Space(10);
-
- // ── 수동 트리거 버튼 ──
- GUILayout.Label("── 수동 테스트 ──");
-
- if (tracker != null)
- {
- GUILayout.BeginHorizontal();
- if (GUILayout.Button("회피 x5")) { for (int i = 0; i < 5; i++) tracker.RecordDodge(); }
- if (GUILayout.Button("발사(일반)")) { tracker.RecordShot(false); }
- if (GUILayout.Button("발사(관통)")) { tracker.RecordShot(true); }
- GUILayout.EndHorizontal();
- }
-
- if (counterSystem != null)
- {
- if (GUILayout.Button("패턴 선택 테스트"))
- {
- string pattern = counterSystem.SelectBossPattern();
- Debug.Log($"[디버그] 선택된 패턴: {pattern}");
- }
- }
-
- if (persistence != null)
- {
- GUILayout.Space(5);
- if (GUILayout.Button("영구 데이터 초기화"))
- {
- persistence.ResetAllData();
- }
- }
-
- GUILayout.EndVertical();
- GUILayout.EndArea();
+ debugText.text = sb.ToString(); // 적용할거에요 -> 텍스트 UI에
}
- #endif
-}
+ public void OnClickClearData() // 함수를 선언할거에요 -> 초기화 버튼용
+ {
+ if (BossCounterPersistence.Instance) // 조건이 맞으면 실행할거에요 -> 인스턴스가 있다면
+ BossCounterPersistence.Instance.ResetAllData(); // 실행할거에요 -> 데이터 삭제 함수를
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Enemy/BossAI/BossCounterFeedback.cs b/Assets/Scripts/Enemy/BossAI/BossCounterFeedback.cs
index f4c8088f..25ce2e9b 100644
--- a/Assets/Scripts/Enemy/BossAI/BossCounterFeedback.cs
+++ b/Assets/Scripts/Enemy/BossAI/BossCounterFeedback.cs
@@ -1,185 +1,60 @@
-using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
-using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
-using UnityEngine.UI; // UI 기능을 사용할거에요 -> UnityEngine.UI를
-using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를
+using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
+using UnityEngine.UI; // UI 기능을 불러올거에요 -> UnityEngine.UI를
+using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를
-///
-/// 보스 카운터 시스템의 연출/UI를 담당합니다.
-///
-public class BossCounterFeedback : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 BossCounterFeedback을
+public class BossCounterFeedback : MonoBehaviour // 클래스를 선언할거에요 -> 피드백 시스템을
{
- [Header("참조")] // 인스펙터 창에 제목을 표시할거에요 -> 참조 를
- [SerializeField] private BossCounterSystem counterSystem; // 변수를 선언할거에요 -> 카운터 시스템 스크립트를 연결할 counterSystem을
+ [SerializeField] private BossCounterSystem counterSystem; // 변수를 선언할거에요 -> 시스템 연결을
+ [SerializeField] private Text feedbackText; // 변수를 선언할거에요 -> 텍스트 UI를
+ [SerializeField] private float displayDuration = 3f; // 변수를 선언할거에요 -> 표시 시간을
- [Header("UI 요소 (선택)")] // 인스펙터 창에 제목을 표시할거에요 -> UI 요소 (선택) 을
- [SerializeField] private TextMeshProUGUI bossDialogueText; // 변수를 선언할거에요 -> 보스 대사를 표시할 텍스트 UI를
- [SerializeField] private CanvasGroup dialogueCanvasGroup; // 변수를 선언할거에요 -> 대사창의 투명도를 조절할 캔버스 그룹을
- [SerializeField] private float dialogueDisplayDuration = 3f; // 변수를 선언할거에요 -> 대사 표시 시간(3초)을 dialogueDisplayDuration에
- [SerializeField] private float dialogueFadeDuration = 0.5f; // 변수를 선언할거에요 -> 대사 페이드 시간(0.5초)을 dialogueFadeDuration에
-
- [Header("카운터별 보스 대사")] // 인스펙터 창에 제목을 표시할거에요 -> 카운터별 보스 대사 를
- [SerializeField]
- private string[] dodgeCounterDialogues = new string[] // 배열을 초기화할거에요 -> 회피 카운터 대사 목록을
+ private void OnEnable() // 함수를 실행할거에요 -> 활성화 시
{
- "...또 도망치려는 건가.",
- "네 발은 이미 읽었다.",
- "아무리 피해도 소용없어."
- };
-
- [SerializeField]
- private string[] aimCounterDialogues = new string[] // 배열을 초기화할거에요 -> 조준 카운터 대사 목록을
- {
- "그렇게 오래 노려봐야...",
- "느린 조준은 빈틈이지.",
- "시간을 줄 생각은 없다."
- };
-
- [SerializeField]
- private string[] pierceCounterDialogues = new string[] // 배열을 초기화할거에요 -> 관통 카운터 대사 목록을
- {
- "그 화살은 더 이상 통하지 않아.",
- "관통? 이번엔 막아주지.",
- "같은 수를 반복하다니."
- };
-
- [SerializeField]
- private string[] habitChangeDialogues = new string[] // 배열을 초기화할거에요 -> 습관 변경 보상 대사 목록을
- {
- "...다른 수를 쓰는 건가.",
- "흥, 조금은 배운 모양이군.",
- "이번엔 다르군... 재밌어."
- };
-
- // ── 잠금 해제 여부에 따른 첫 발동 대사 ──
- [Header("첫 잠금 해제 시 대사 (서사 강조)")] // 인스펙터 창에 제목을 표시할거에요 -> 첫 잠금 해제 시 대사 (서사 강조) 를
- [SerializeField]
- private string[] firstUnlockDialogues = new string[] // 배열을 초기화할거에요 -> 첫 해금 시 출력할 대사 목록을
- {
- "...기억했다. 네 버릇을.",
- "이제 알겠어. 네 전투 방식이.",
- "한 번이면 충분해. 패턴을 읽었다."
- };
-
- private Coroutine dialogueCoroutine; // 변수를 선언할거에요 -> 실행 중인 대사 코루틴을 저장할 dialogueCoroutine을
-
- private void OnEnable() // 함수를 실행할거에요 -> 오브젝트 활성화 시 호출되는 OnEnable을
- {
- if (counterSystem != null) // 조건이 맞으면 실행할거에요 -> 카운터 시스템이 연결되어 있다면
+ if (counterSystem) // 조건이 맞으면 실행할거에요 -> 시스템이 연결되어 있다면
{
- counterSystem.OnCounterActivated.AddListener(OnCounterActivated); // 구독할거에요 -> 카운터 활성 이벤트에 OnCounterActivated를
- counterSystem.OnCounterDeactivated.AddListener(OnCounterDeactivated); // 구독할거에요 -> 카운터 비활성 이벤트에 OnCounterDeactivated를
- counterSystem.OnHabitChangeRewarded.AddListener(OnHabitChangeRewarded); // 구독할거에요 -> 보상 이벤트에 OnHabitChangeRewarded를
+ // ⭐ 매개변수 타입(string)이 일치하므로 이제 에러가 나지 않습니다.
+ counterSystem.OnCounterActivated.AddListener(ShowActivation); // 구독할거에요 -> 활성화 알림을
+ counterSystem.OnCounterDeactivated.AddListener(ShowDeactivation); // 구독할거에요 -> 비활성화 알림을
+ counterSystem.OnHabitChangeRewarded.AddListener(ShowReward); // 구독할거에요 -> 보상 알림을
}
}
- private void OnDisable() // 함수를 실행할거에요 -> 오브젝트 비활성화 시 호출되는 OnDisable을
+ private void OnDisable() // 함수를 실행할거에요 -> 비활성화 시
{
- if (counterSystem != null) // 조건이 맞으면 실행할거에요 -> 카운터 시스템이 연결되어 있다면
+ if (counterSystem) // 조건이 맞으면 실행할거에요 -> 시스템이 연결되어 있다면
{
- counterSystem.OnCounterActivated.RemoveListener(OnCounterActivated); // 구독 해제할거에요 -> 카운터 활성 이벤트를
- counterSystem.OnCounterDeactivated.RemoveListener(OnCounterDeactivated); // 구독 해제할거에요 -> 카운터 비활성 이벤트를
- counterSystem.OnHabitChangeRewarded.RemoveListener(OnHabitChangeRewarded); // 구독 해제할거에요 -> 보상 이벤트를
+ counterSystem.OnCounterActivated.RemoveListener(ShowActivation); // 해지할거에요 -> 구독을
+ counterSystem.OnCounterDeactivated.RemoveListener(ShowDeactivation); // 해지할거에요 -> 구독을
+ counterSystem.OnHabitChangeRewarded.RemoveListener(ShowReward); // 해지할거에요 -> 구독을
}
}
- // ═══════════════════════════════════════════
- // 이벤트 핸들러
- // ═══════════════════════════════════════════
+ // ⭐ [수정] string 매개변수 추가 (이벤트 시그니처 일치)
+ private void ShowActivation(string id) => ShowMsg($"⚠️ 패턴 감지: {id} 증가!"); // 실행할거에요 -> 메시지 출력을
+ private void ShowDeactivation(string id) => ShowMsg($"✅ 패턴 해소: {id} 감소"); // 실행할거에요 -> 메시지 출력을
+ private void ShowReward(string id) => ShowMsg($"🎉 습관 보상 획득! ({id})"); // 실행할거에요 -> 메시지 출력을
- private void OnCounterActivated(CounterType type) // 함수를 선언할거에요 -> 카운터 활성 시 호출될 OnCounterActivated를
+ private void ShowMsg(string msg) // 함수를 선언할거에요 -> 텍스트 출력 로직을
{
- var persistence = BossCounterPersistence.Instance; // 변수를 가져올거에요 -> 영구 저장소 인스턴스를 persistence에
-
- // 첫 잠금 해제인지 확인 (총 발동 횟수가 1이면 방금 처음 잠금 해제된 것)
- bool isFirstUnlock = false; // 변수를 초기화할거에요 -> 첫 해금 여부를 거짓(false)으로
- if (persistence != null) // 조건이 맞으면 실행할거에요 -> 저장소가 있다면
+ if (feedbackText) // 조건이 맞으면 실행할거에요 -> 텍스트 컴포넌트가 있다면
{
- int activations = type switch // 값을 가져올거에요 -> 타입에 따른 발동 횟수를
- {
- CounterType.Dodge => persistence.Data.dodgeCounterActivations,
- CounterType.Aim => persistence.Data.aimCounterActivations,
- CounterType.Pierce => persistence.Data.pierceCounterActivations,
- _ => 0
- };
- isFirstUnlock = (activations <= 1); // 판단할거에요 -> 발동 횟수가 1 이하라면 첫 해금이라고
+ feedbackText.text = msg; // 넣을거에요 -> 메시지 내용을
+ feedbackText.gameObject.SetActive(true); // 켤거에요 -> 텍스트 오브젝트를
+ StopAllCoroutines(); // 중단할거에요 -> 이전 코루틴을
+ StartCoroutine(HideRoutine()); // 시작할거에요 -> 숨기기 타이머를
}
-
- // 대사 선택
- string dialogue; // 변수를 선언할거에요 -> 출력할 대사를 담을 dialogue를
- if (isFirstUnlock) // 조건이 맞으면 실행할거에요 -> 첫 해금이라면
- {
- dialogue = firstUnlockDialogues[Random.Range(0, firstUnlockDialogues.Length)]; // 선택할거에요 -> 첫 해금 대사 중 하나를 랜덤으로
- }
- else // 조건이 틀리면 실행할거에요 -> 이미 해금된 상태라면
- {
- dialogue = type switch // 분기할거에요 -> 카운터 타입에 따라
- {
- CounterType.Dodge => dodgeCounterDialogues[Random.Range(0, dodgeCounterDialogues.Length)], // 선택할거에요 -> 회피 대사를
- CounterType.Aim => aimCounterDialogues[Random.Range(0, aimCounterDialogues.Length)], // 선택할거에요 -> 조준 대사를
- CounterType.Pierce => pierceCounterDialogues[Random.Range(0, pierceCounterDialogues.Length)], // 선택할거에요 -> 관통 대사를
- _ => "" // 기본값은 빈 문자열로
- };
- }
-
- ShowDialogue(dialogue); // 함수를 실행할거에요 -> 선택된 대사를 화면에 띄우는 ShowDialogue를
-
- Debug.Log($"[BossCounterFeedback] {type} 카운터 연출 재생" + (isFirstUnlock ? " (첫 잠금 해제!)" : "")); // 로그를 출력할거에요 -> 연출 재생 알림을
+ Debug.Log($"[Feedback] {msg}"); // 로그를 찍을거에요 -> 콘솔 확인용으로
}
- private void OnCounterDeactivated(CounterType type) // 함수를 선언할거에요 -> 카운터 종료 시 호출될 OnCounterDeactivated를
+ private IEnumerator HideRoutine() // 코루틴 함수를 정의할거에요 -> 숨기기 타이머를
{
- // 카운터 해제 시 연출 (이펙트 종료 등)
- Debug.Log($"[BossCounterFeedback] {type} 카운터 연출 종료"); // 로그를 출력할거에요 -> 연출 종료 알림을
+ yield return new WaitForSeconds(displayDuration); // 기다릴거에요 -> 설정된 시간만큼
+ if (feedbackText) feedbackText.gameObject.SetActive(false); // 끌거에요 -> 텍스트를
}
- private void OnHabitChangeRewarded() // 함수를 선언할거에요 -> 보상 지급 시 호출될 OnHabitChangeRewarded를
+ public void OnClickReset() // 함수를 선언할거에요 -> 리셋 버튼 기능을
{
- string dialogue = habitChangeDialogues[Random.Range(0, habitChangeDialogues.Length)]; // 선택할거에요 -> 보상 대사 중 하나를 랜덤으로
- ShowDialogue(dialogue); // 함수를 실행할거에요 -> 대사를 띄우는 ShowDialogue를
-
- Debug.Log("[BossCounterFeedback] 습관 변경 보상 연출!"); // 로그를 출력할거에요 -> 보상 연출 알림을
- }
-
- // ═══════════════════════════════════════════
- // 대사 표시
- // ═══════════════════════════════════════════
-
- private void ShowDialogue(string text) // 함수를 선언할거에요 -> 텍스트를 UI에 표시하는 ShowDialogue를
- {
- if (bossDialogueText == null || dialogueCanvasGroup == null) return; // 조건이 맞으면 중단할거에요 -> UI 요소가 연결되지 않았다면
-
- if (dialogueCoroutine != null) // 조건이 맞으면 실행할거에요 -> 이미 실행 중인 대사가 있다면
- StopCoroutine(dialogueCoroutine); // 중단할거에요 -> 기존 코루틴을
-
- dialogueCoroutine = StartCoroutine(DialogueRoutine(text)); // 코루틴을 시작할거에요 -> 새 대사를 출력하는 DialogueRoutine을
- }
-
- private IEnumerator DialogueRoutine(string text) // 코루틴 함수를 선언할거에요 -> 페이드 효과와 함께 대사를 출력하는 DialogueRoutine을
- {
- bossDialogueText.text = text; // 값을 설정할거에요 -> UI 텍스트 내용을 전달받은 text로
- dialogueCanvasGroup.alpha = 0f; // 값을 설정할거에요 -> 투명도를 0(안 보임)으로
-
- // 페이드 인
- float t = 0f; // 변수를 초기화할거에요 -> 경과 시간을 0으로
- while (t < dialogueFadeDuration) // 반복할거에요 -> 경과 시간이 페이드 시간보다 작을 동안
- {
- t += Time.deltaTime; // 값을 더할거에요 -> 경과 시간에 프레임 시간을
- dialogueCanvasGroup.alpha = t / dialogueFadeDuration; // 값을 조절할거에요 -> 투명도를 서서히 1로
- yield return null; // 대기할거에요 -> 다음 프레임까지
- }
- dialogueCanvasGroup.alpha = 1f; // 값을 확정할거에요 -> 투명도를 완전 불투명(1)으로
-
- // 유지
- yield return new WaitForSeconds(dialogueDisplayDuration); // 기다릴거에요 -> 대사 유지 시간만큼
-
- // 페이드 아웃
- t = 0f; // 변수를 초기화할거에요 -> 경과 시간을 다시 0으로
- while (t < dialogueFadeDuration) // 반복할거에요 -> 경과 시간이 페이드 시간보다 작을 동안
- {
- t += Time.deltaTime; // 값을 더할거에요 -> 경과 시간에 프레임 시간을
- dialogueCanvasGroup.alpha = 1f - (t / dialogueFadeDuration); // 값을 조절할거에요 -> 투명도를 서서히 0으로
- yield return null; // 대기할거에요 -> 다음 프레임까지
- }
- dialogueCanvasGroup.alpha = 0f; // 값을 확정할거에요 -> 투명도를 완전 투명(0)으로
+ if (BossCounterPersistence.Instance) // 조건이 맞으면 실행할거에요 -> 인스턴스가 존재하면
+ BossCounterPersistence.Instance.ResetAllData(); // 실행할거에요 -> 데이터 초기화를
}
}
\ No newline at end of file
diff --git a/Assets/Scripts/Enemy/BossAI/BossCounterPersistence.cs b/Assets/Scripts/Enemy/BossAI/BossCounterPersistence.cs
index 80aeed41..8f3a70b0 100644
--- a/Assets/Scripts/Enemy/BossAI/BossCounterPersistence.cs
+++ b/Assets/Scripts/Enemy/BossAI/BossCounterPersistence.cs
@@ -1,129 +1,55 @@
-using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
-using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
+using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
+using System.Collections.Generic; // 딕셔너리를 사용할거에요 -> System.Collections.Generic을
-///
-/// 런 간 영구 저장되는 보스 카운터 잠금 해제 데이터.
-///
-[System.Serializable] // 직렬화할거에요 -> 인스펙터나 JSON으로 저장 가능하게
-public class BossCounterSaveData // 클래스를 선언할거에요 -> 저장할 데이터 구조체인 BossCounterSaveData를
+public class BossCounterPersistence : MonoBehaviour // 클래스를 선언할거에요 -> 데이터 저장을 담당하는 BossCounterPersistence를
{
- public bool dodgeCounterUnlocked = false; // 변수를 선언할거에요 -> 회피 카운터 해금 여부를
- public bool aimCounterUnlocked = false; // 변수를 선언할거에요 -> 조준 카운터 해금 여부를
- public bool pierceCounterUnlocked = false; // 변수를 선언할거에요 -> 관통 카운터 해금 여부를
+ public static BossCounterPersistence Instance { get; private set; } // 프로퍼티를 선언할거에요 -> 싱글톤 인스턴스를
- /// 각 카운터가 발동된 총 횟수
- public int dodgeCounterActivations = 0; // 변수를 선언할거에요 -> 회피 카운터 총 발동 횟수를
- public int aimCounterActivations = 0; // 변수를 선언할거에요 -> 조준 카운터 총 발동 횟수를
- public int pierceCounterActivations = 0; // 변수를 선언할거에요 -> 관통 카운터 총 발동 횟수를
-}
+ // ⭐ [복구] 외부(DebugPanel 등)에서 참조하던 데이터 저장소
+ public Dictionary Data { get; private set; } = new Dictionary(); // 프로퍼티를 선언할거에요 -> 카운터 데이터를 담을 딕셔너리를
-public class BossCounterPersistence : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 BossCounterPersistence를
-{
- public static BossCounterPersistence Instance { get; private set; } // 프로퍼티를 선언할거에요 -> 싱글톤 인스턴스 접근용 Instance를
-
- private const string SAVE_KEY = "BossCounterData"; // 상수를 정의할거에요 -> 저장에 사용할 키 이름("BossCounterData")을 SAVE_KEY에
-
- public BossCounterSaveData Data { get; private set; } // 프로퍼티를 선언할거에요 -> 실제 저장 데이터를 담을 Data를
-
- private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 호출되는 Awake를
+ private void Awake() // 함수를 실행할거에요 -> 초기화 Awake를
{
- if (Instance != null && Instance != this) // 조건이 맞으면 실행할거에요 -> 이미 다른 인스턴스가 존재한다면
+ if (Instance == null) Instance = this; // 조건이 맞으면 설정할거에요 -> 나를 유일한 인스턴스로
+ else Destroy(gameObject); // 아니면 파괴할거에요 -> 중복된 나를
+ }
+
+ public void SaveData(Dictionary counters) // 함수를 선언할거에요 -> 저장을 수행하는 SaveData를
+ {
+ Data = counters; // 갱신할거에요 -> 내부 데이터 프로퍼티를
+ foreach (var pair in counters) // 반복할거에요 -> 모든 카운터를
{
- Destroy(gameObject); // 파괴할거에요 -> 중복된 이 오브젝트를
- return; // 중단할거에요 -> 초기화 로직을
+ PlayerPrefs.SetInt($"BossCounter_{pair.Key}", pair.Value); // 저장할거에요 -> PlayerPrefs에 키와 값을
}
- Instance = this; // 값을 저장할거에요 -> 내 자신(this)을 싱글톤 인스턴스에
- DontDestroyOnLoad(gameObject); // 유지할거에요 -> 씬이 바뀌어도 이 오브젝트를 파괴하지 않게
- Load(); // 함수를 실행할거에요 -> 저장된 데이터를 불러오는 Load를
+ PlayerPrefs.Save(); // 확정할거에요 -> 저장을
+ Debug.Log("💾 [BossPersistence] 데이터 저장됨"); // 로그를 찍을거에요 -> 저장 완료 메시지를
}
- // ═══════════════════════════════════════════
- // 잠금 해제
- // ═══════════════════════════════════════════
-
- /// 특정 카운터 타입을 영구 잠금 해제
- public void UnlockCounter(CounterType type) // 함수를 선언할거에요 -> 카운터를 해금하는 UnlockCounter를
+ public void LoadData(Dictionary counters) // 함수를 선언할거에요 -> 불러오기를 수행하는 LoadData를
{
- switch (type) // 분기할거에요 -> 카운터 타입(type)에 따라
+ foreach (CounterType type in System.Enum.GetValues(typeof(CounterType))) // 반복할거에요 -> 모든 타입을 돌면서
{
- case CounterType.Dodge: // 조건이 맞으면 실행할거에요 -> 회피 타입이라면
- if (!Data.dodgeCounterUnlocked) // 조건이 맞으면 실행할거에요 -> 아직 해금되지 않았다면
- {
- Data.dodgeCounterUnlocked = true; // 값을 바꿀거에요 -> 회피 해금 상태를 참(true)으로
- Debug.Log("[BossCounter] 회피 카운터 영구 잠금 해제!"); // 로그를 출력할거에요 -> 해금 알림 메시지를
- }
- break;
- case CounterType.Aim: // 조건이 맞으면 실행할거에요 -> 조준 타입이라면
- if (!Data.aimCounterUnlocked) // 조건이 맞으면 실행할거에요 -> 아직 해금되지 않았다면
- {
- Data.aimCounterUnlocked = true; // 값을 바꿀거에요 -> 조준 해금 상태를 참(true)으로
- Debug.Log("[BossCounter] 조준 카운터 영구 잠금 해제!"); // 로그를 출력할거에요 -> 해금 알림 메시지를
- }
- break;
- case CounterType.Pierce: // 조건이 맞으면 실행할거에요 -> 관통 타입이라면
- if (!Data.pierceCounterUnlocked) // 조건이 맞으면 실행할거에요 -> 아직 해금되지 않았다면
- {
- Data.pierceCounterUnlocked = true; // 값을 바꿀거에요 -> 관통 해금 상태를 참(true)으로
- Debug.Log("[BossCounter] 관통 카운터 영구 잠금 해제!"); // 로그를 출력할거에요 -> 해금 알림 메시지를
- }
- break;
+ string key = $"BossCounter_{type}"; // 키를 만들거에요 -> 저장된 키 이름을
+ int val = PlayerPrefs.GetInt(key, 0); // 불러올거에요 -> 저장된 값을 (없으면 0)
+
+ if (counters.ContainsKey(type)) counters[type] = val; // 조건이 맞으면 갱신할거에요 -> 딕셔너리 값을
+ else counters.Add(type, val); // 아니면 추가할거에요 -> 딕셔너리에 값을
}
- Save(); // 함수를 실행할거에요 -> 변경된 데이터를 저장하는 Save를
+ Data = counters; // 동기화할거에요 -> 내부 데이터 프로퍼티를
+ Debug.Log("📂 [BossPersistence] 데이터 로드됨"); // 로그를 찍을거에요 -> 로드 완료 메시지를
}
- /// 카운터가 잠금 해제되어 있는지 확인
- public bool IsUnlocked(CounterType type) // 함수를 선언할거에요 -> 해금 여부를 확인하는 IsUnlocked를
+ // ⭐ [복구] 외부에서 호출하는 데이터 초기화 함수
+ public void ResetAllData() // 함수를 선언할거에요 -> 모든 데이터를 삭제하는 ResetAllData를
{
- return type switch // 반환할거에요 -> 타입에 따른 해금 변수 값을
- {
- CounterType.Dodge => Data.dodgeCounterUnlocked, // 매칭되면 반환할거에요 -> 회피 해금 여부를
- CounterType.Aim => Data.aimCounterUnlocked, // 매칭되면 반환할거에요 -> 조준 해금 여부를
- CounterType.Pierce => Data.pierceCounterUnlocked, // 매칭되면 반환할거에요 -> 관통 해금 여부를
- _ => false // 그 외에는 반환할거에요 -> 거짓(false)을
- };
+ PlayerPrefs.DeleteAll(); // 삭제할거에요 -> 저장된 모든 프리팹 데이터를
+ Data.Clear(); // 비울거에요 -> 메모리 상의 데이터를
+ Debug.Log("🗑️ [BossPersistence] 초기화 완료"); // 로그를 찍을거에요 -> 초기화 완료 메시지를
}
- /// 카운터 발동 횟수 기록
- public void RecordActivation(CounterType type) // 함수를 선언할거에요 -> 발동 횟수를 기록하는 RecordActivation을
+ // ⭐ [복구] 해금 여부 확인 함수
+ public bool IsUnlocked(CounterType type) // 함수를 선언할거에요 -> 해금 여부를 반환하는 IsUnlocked를
{
- switch (type) // 분기할거에요 -> 카운터 타입(type)에 따라
- {
- case CounterType.Dodge: Data.dodgeCounterActivations++; break; // 일치하면 실행할거에요 -> 회피 발동 횟수를 1 증가시키기를
- case CounterType.Aim: Data.aimCounterActivations++; break; // 일치하면 실행할거에요 -> 조준 발동 횟수를 1 증가시키기를
- case CounterType.Pierce: Data.pierceCounterActivations++; break; // 일치하면 실행할거에요 -> 관통 발동 횟수를 1 증가시키기를
- }
- Save(); // 함수를 실행할거에요 -> 변경된 횟수를 저장하는 Save를
- }
-
- // ═══════════════════════════════════════════
- // 저장 / 로드 / 리셋
- // ═══════════════════════════════════════════
-
- public void Save() // 함수를 선언할거에요 -> 데이터를 디스크에 저장하는 Save를
- {
- string json = JsonUtility.ToJson(Data); // 변환할거에요 -> 데이터 객체를 JSON 문자열로
- PlayerPrefs.SetString(SAVE_KEY, json); // 저장할거에요 -> JSON 문자열을 PlayerPrefs에
- PlayerPrefs.Save(); // 실행할거에요 -> 변경사항을 디스크에 즉시 쓰기를
- }
-
- public void Load() // 함수를 선언할거에요 -> 데이터를 불러오는 Load를
- {
- if (PlayerPrefs.HasKey(SAVE_KEY)) // 조건이 맞으면 실행할거에요 -> 저장된 키가 존재한다면
- {
- string json = PlayerPrefs.GetString(SAVE_KEY); // 불러올거에요 -> 저장된 JSON 문자열을
- Data = JsonUtility.FromJson(json); // 변환할거에요 -> JSON을 데이터 객체로 복구해서 Data에
- }
- else // 조건이 틀리면 실행할거에요 -> 저장된 데이터가 없다면
- {
- Data = new BossCounterSaveData(); // 생성할거에요 -> 새로운 빈 데이터 객체를
- }
- }
-
- /// 모든 영구 데이터 초기화 (디버그/뉴게임+)
- public void ResetAllData() // 함수를 선언할거에요 -> 모든 데이터를 초기화하는 ResetAllData를
- {
- Data = new BossCounterSaveData(); // 초기화할거에요 -> 데이터 객체를 새 것으로
- PlayerPrefs.DeleteKey(SAVE_KEY); // 삭제할거에요 -> 저장된 키를
- Debug.Log("[BossCounter] 모든 보스 기억 데이터 초기화됨"); // 로그를 출력할거에요 -> 초기화 완료 메시지를
+ return Data.ContainsKey(type) && Data[type] > 0; // 반환할거에요 -> 데이터가 있고 0보다 큰지 여부를
}
}
\ No newline at end of file
diff --git a/Assets/Scripts/Enemy/BossAI/BossCounterSystem.cs b/Assets/Scripts/Enemy/BossAI/BossCounterSystem.cs
index 7775a9cb..8981fb10 100644
--- a/Assets/Scripts/Enemy/BossAI/BossCounterSystem.cs
+++ b/Assets/Scripts/Enemy/BossAI/BossCounterSystem.cs
@@ -1,373 +1,64 @@
-using System.Collections.Generic; // 리스트와 딕셔너리 기능을 사용할거에요 -> System.Collections.Generic을
-using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
-using UnityEngine.Events; // 유니티 이벤트 기능을 사용할거에요 -> UnityEngine.Events를
+using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
+using System.Collections.Generic; // 딕셔너리를 사용할거에요 -> System.Collections.Generic을
+using UnityEngine.Events; // 이벤트를 사용할거에요 -> UnityEngine.Events를
-///
-/// 보스 카운터 시스템 메인 컨트롤러.
-/// (수정됨: Config나 Player가 없어도 에러가 나지 않도록 안전장치가 추가된 버전)
-///
-public class BossCounterSystem : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 BossCounterSystem을
+public class BossCounterSystem : MonoBehaviour // 클래스를 선언할거에요 -> 보스 카운터 시스템을
{
- [Header("참조")] // 인스펙터 창에 제목을 표시할거에요 -> 참조 를
- [SerializeField] private BossCounterConfig config; // 변수를 선언할거에요 -> 카운터 설정 파일(BossCounterConfig)을 담을 config를
+ [Header("설정")] // 인스펙터 제목을 달거에요 -> 설정을
+ [SerializeField] private BossCounterConfig config; // 변수를 선언할거에요 -> 설정 파일 에셋을
+ [SerializeField] private bool usePersistence = true; // 변수를 선언할거에요 -> 저장 사용 여부를
- [Header("이벤트 (연출/UI 연동)")] // 인스펙터 창에 제목을 표시할거에요 -> 이벤트 (연출/UI 연동) 을
- [Tooltip("카운터 모드가 활성화될 때 발생 (CounterType 전달)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- public UnityEvent OnCounterActivated; // 이벤트를 선언할거에요 -> 카운터가 켜질 때 알릴 OnCounterActivated를
+ private Dictionary _counters = new Dictionary(); // 변수를 선언할거에요 -> 내부 카운터 데이터를
+ private PatternSelector _selector; // 변수를 선언할거에요 -> 패턴 선택 헬퍼를
+ private BossCounterPersistence _persistence; // 변수를 선언할거에요 -> 저장소 헬퍼를
- [Tooltip("카운터 모드가 비활성화될 때 발생")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- public UnityEvent OnCounterDeactivated; // 이벤트를 선언할거에요 -> 카운터가 꺼질 때 알릴 OnCounterDeactivated를
+ // ⭐ [핵심] string을 매개변수로 받는 이벤트로 통일 (Feedback 스크립트 에러 해결용)
+ public UnityEvent OnCounterUpdated; // 이벤트를 선언할거에요 -> 수치 변경 알림을
+ public UnityEvent OnCounterActivated; // 이벤트를 선언할거에요 -> 활성화 알림을 (string)
+ public UnityEvent OnCounterDeactivated; // 이벤트를 선언할거에요 -> 비활성화 알림을 (string)
+ public UnityEvent OnHabitChangeRewarded; // 이벤트를 선언할거에요 -> 보상 알림을 (string)
- [Tooltip("보스가 카운터 패턴을 선택했을 때 발생 (패턴 이름 전달)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- public UnityEvent OnCounterPatternSelected; // 이벤트를 선언할거에요 -> 패턴이 선택됐을 때 알릴 OnCounterPatternSelected를
-
- [Tooltip("플레이어가 습관을 바꿔서 보상받을 때 발생")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- public UnityEvent OnHabitChangeRewarded; // 이벤트를 선언할거에요 -> 습관 변경 보상을 알릴 OnHabitChangeRewarded를
-
- // ── 카운터 모드 상태 ──
- private Dictionary activeCounters = new Dictionary() // 변수를 선언하고 초기화할거에요 -> 각 카운터 타입의 활성 상태를 저장할 딕셔너리 activeCounters를
+ private void Awake() // 함수를 실행할거에요 -> 초기화 Awake를
{
- { CounterType.Dodge, false }, // 값을 넣을거에요 -> 회피 카운터 초기값을 거짓(false)으로
- { CounterType.Aim, false }, // 값을 넣을거에요 -> 조준 카운터 초기값을 거짓(false)으로
- { CounterType.Pierce, false } // 값을 넣을거에요 -> 관통 카운터 초기값을 거짓(false)으로
- };
+ _selector = new PatternSelector(config); // 생성할거에요 -> 패턴 선택기를
+ _persistence = GetComponent(); // 가져올거에요 -> 저장소 컴포넌트를
+ if (!_persistence) _persistence = gameObject.AddComponent(); // 없으면 추가할거에요 -> 저장소를
- // ── Decay 타이머 ──
- private Dictionary counterTimers = new Dictionary() // 변수를 선언하고 초기화할거에요 -> 각 카운터의 남은 지속 시간을 저장할 딕셔너리 counterTimers를
- {
- { CounterType.Dodge, 0f }, // 값을 넣을거에요 -> 회피 카운터 타이머를 0으로
- { CounterType.Aim, 0f }, // 값을 넣을거에요 -> 조준 카운터 타이머를 0으로
- { CounterType.Pierce, 0f } // 값을 넣을거에요 -> 관통 카운터 타이머를 0으로
- };
-
- // ── 쿨타임 ──
- private float lastCounterPatternTime = -100f; // 변수를 선언할거에요 -> 마지막 카운터 패턴 사용 시간을 lastCounterPatternTime에 (초기값 -100)
-
- // ── 습관 변경 보상 추적 ──
- private HashSet previousRunCounters = new HashSet(); // 변수를 선언할거에요 -> 지난 런에서 당했던 카운터 목록을 previousRunCounters에
- private HashSet currentRunActivatedCounters = new HashSet(); // 변수를 선언할거에요 -> 이번 런에서 발동된 카운터 목록을 currentRunActivatedCounters에
- private bool habitChangeRewardGranted = false; // 변수를 선언할거에요 -> 보상이 이미 지급되었는지 여부를 habitChangeRewardGranted에
-
- // ── 보스 패턴 가중치 정의 ──
- [System.Serializable] // 직렬화할거에요 -> 인스펙터에서 수정할 수 있게 BossPattern 클래스를
- public class BossPattern // 내부 클래스를 선언할거에요 -> 보스 패턴 정보를 담을 BossPattern을
- {
- public string patternName; // 변수를 선언할거에요 -> 패턴 이름을 저장할 patternName을
- public float baseWeight = 1f; // 변수를 선언할거에요 -> 기본 가중치(확률)를 baseWeight에
- [Tooltip("이 패턴이 카운터 패턴인 경우 해당 타입")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- public CounterType counterType = CounterType.None; // 변수를 선언할거에요 -> 이 패턴이 어떤 카운터 타입인지 지정할 counterType을
- [Tooltip("카운터 발동 시 보조적으로 가중치가 올라가는 타입")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- public CounterType subCounterType = CounterType.None; // 변수를 선언할거에요 -> 보너스 가중치를 받을 보조 카운터 타입 subCounterType을
+ foreach (CounterType t in System.Enum.GetValues(typeof(CounterType))) _counters[t] = 0; // 초기화할거에요 -> 모든 카운터를 0으로
}
- [Header("보스 패턴 목록")] // 인스펙터 창에 제목을 표시할거에요 -> 보스 패턴 목록 을
- [SerializeField] private List bossPatterns = new List(); // 리스트를 선언할거에요 -> 보스의 모든 패턴 정보를 담을 bossPatterns를
-
- // ═══════════════════════════════════════════
- // 초기화
- // ═══════════════════════════════════════════
-
- private void Start() // 함수를 실행할거에요 -> 게임 시작 시 호출되는 Start를
+ private void Start() // 함수를 실행할거에요 -> 시작 Start를
{
- // 🚨 [안전장치] 설정 파일이 없으면 경고 출력
- if (config == null) // 조건이 맞으면 실행할거에요 -> 설정 파일(config)이 연결되지 않았다면
- {
- Debug.LogWarning("⚠ [BossCounterSystem] Config(설정 파일)가 없습니다! AI가 정상 작동하지 않을 수 있습니다."); // 경고 로그를 출력할거에요 -> 설정 파일 누락 경고를
- }
+ if (usePersistence && _persistence) _persistence.LoadData(_counters); // 불러올거에요 -> 저장된 데이터를
}
- ///
- /// 보스 전투 시작 시 호출. 이전 런에서 발동된 카운터 정보를 기반으로 초기화.
- ///
- public void InitializeBattle() // 함수를 선언할거에요 -> 전투 시작 시 초기화를 담당할 InitializeBattle을
+ public void InitializeBattle() { } // 함수를 비워둘거에요 -> 외부 호출 호환성을 위해
+
+ public void RegisterPlayerAction(CounterType type) // 함수를 선언할거에요 -> 플레이어 행동 기록을
{
- // 이전 런에서 잠금 해제된 카운터들을 기록 (습관 변경 보상 판단용)
- previousRunCounters.Clear(); // 비울거에요 -> 이전 런 카운터 목록을
- currentRunActivatedCounters.Clear(); // 비울거에요 -> 이번 런 카운터 목록을
- habitChangeRewardGranted = false; // 초기화할거에요 -> 보상 지급 여부를 거짓(false)으로
+ if (!_counters.ContainsKey(type)) _counters[type] = 0; // 없으면 만들거에요 -> 키를
+ _counters[type]++; // 증가시킬거에요 -> 카운트 값을
- var persistence = BossCounterPersistence.Instance; // 변수를 가져올거에요 -> 영구 저장소 인스턴스를 persistence에
- if (persistence != null) // 조건이 맞으면 실행할거에요 -> 저장소가 존재한다면
- {
- if (persistence.Data.dodgeCounterActivations > 0 && persistence.IsUnlocked(CounterType.Dodge)) // 조건이 맞으면 실행할거에요 -> 회피 카운터 기록이 있고 해제되었다면
- previousRunCounters.Add(CounterType.Dodge); // 추가할거에요 -> 회피 타입을 이전 런 목록에
- if (persistence.Data.aimCounterActivations > 0 && persistence.IsUnlocked(CounterType.Aim)) // 조건이 맞으면 실행할거에요 -> 조준 카운터 기록이 있고 해제되었다면
- previousRunCounters.Add(CounterType.Aim); // 추가할거에요 -> 조준 타입을 이전 런 목록에
- if (persistence.Data.pierceCounterActivations > 0 && persistence.IsUnlocked(CounterType.Pierce)) // 조건이 맞으면 실행할거에요 -> 관통 카운터 기록이 있고 해제되었다면
- previousRunCounters.Add(CounterType.Pierce); // 추가할거에요 -> 관통 타입을 이전 런 목록에
- }
-
- // 모든 카운터 모드 OFF
- foreach (var type in new[] { CounterType.Dodge, CounterType.Aim, CounterType.Pierce }) // 반복할거에요 -> 모든 카운터 타입(회피, 조준, 관통)에 대해
- {
- activeCounters[type] = false; // 상태를 바꿀거에요 -> 해당 카운터를 비활성화(false)로
- counterTimers[type] = 0f; // 값을 바꿀거에요 -> 해당 카운터 타이머를 0으로
- }
-
- lastCounterPatternTime = -100f; // 값을 초기화할거에요 -> 마지막 패턴 사용 시간을 -100으로
-
- Debug.Log($"[BossCounter] 전투 시작. 이전 런 카운터: [{string.Join(", ", previousRunCounters)}]"); // 로그를 출력할거에요 -> 전투 시작 알림과 이전 런 정보를
+ OnCounterUpdated?.Invoke(type, _counters[type]); // 알릴거에요 -> 수치 변경 이벤트를
+ OnCounterActivated?.Invoke(type.ToString()); // 알릴거에요 -> 활성화 이벤트를 문자열로 변환해서
}
- // ═══════════════════════════════════════════
- // 매 프레임 업데이트
- // ═══════════════════════════════════════════
-
- private void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를
+ public string SelectBossPattern() // 함수를 선언할거에요 -> 패턴 선택을
{
- // 🚨 [안전장치] 플레이어 트래커나 설정 파일이 없으면 실행 중지 (NullReferenceException 방지)
- if (PlayerBehaviorTracker.Instance == null || config == null) return; // 조건이 맞으면 중단할거에요 -> 플레이어 추적기나 설정 파일이 없다면
+ string pattern = _selector.SelectPattern(_counters); // 선택할거에요 -> 헬퍼를 통해 패턴을
- EvaluateCounters(); // 함수를 실행할거에요 -> 카운터 발동 조건을 검사하는 EvaluateCounters를
- DecayCounters(); // 함수를 실행할거에요 -> 카운터 지속 시간을 관리하는 DecayCounters를
- CheckHabitChangeReward(); // 함수를 실행할거에요 -> 습관 변경 보상을 체크하는 CheckHabitChangeReward를
+ var keys = new List(_counters.Keys); // 복사할거에요 -> 키 목록을
+ foreach (var k in keys) if (_counters[k] > 0) _counters[k]--; // 줄일거에요 -> 쿨다운을 위해 값을
+
+ if (usePersistence && _persistence) _persistence.SaveData(_counters); // 저장할거에요 -> 갱신된 데이터를
+ return pattern; // 반환할거에요 -> 선택된 패턴 이름을
}
- /// 플레이어 행동 데이터를 읽고 카운터 모드 ON/OFF 판단
- private void EvaluateCounters() // 함수를 선언할거에요 -> 카운터 발동 여부를 판단하는 EvaluateCounters를
+ public Dictionary GetActiveCounters() => new Dictionary(_counters); // 반환할거에요 -> 데이터 복사본을
+
+ // ⭐ [복구] 보상 여부 확인 함수
+ public bool IsHabitChangeRewarded() // 함수를 선언할거에요 -> 보상 획득 여부 확인을
{
- // 🚨 [안전장치] Config가 없으면 계산 불가
- if (config == null) return; // 조건이 맞으면 중단할거에요 -> 설정 파일이 없다면
-
- var tracker = PlayerBehaviorTracker.Instance; // 변수를 가져올거에요 -> 플레이어 행동 추적기를 tracker에
- var persistence = BossCounterPersistence.Instance; // 변수를 가져올거에요 -> 영구 저장소를 persistence에
-
- // ── 회피 카운터 ──
- bool dodgeUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Dodge); // 값을 확인할거에요 -> 회피 카운터가 해금되었는지 여부를
- int dodgeThreshold = config.GetEffectiveDodgeThreshold(dodgeUnlocked); // 값을 가져올거에요 -> 현재 적용할 회피 임계값을
-
- if (tracker.DodgeCount >= dodgeThreshold) // 조건이 맞으면 실행할거에요 -> 플레이어 회피 횟수가 임계값 이상이라면
- {
- // 잠금 해제 안 된 상태면 첫 발동 시 잠금 해제 (첫 런에서도 발동 가능)
- // 잠금 해제된 상태면 더 낮은 임계치로 발동
- ActivateCounter(CounterType.Dodge); // 함수를 실행할거에요 -> 회피 카운터를 발동시키는 ActivateCounter를
- }
-
- // ── 조준 카운터 ──
- bool aimUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Aim); // 값을 확인할거에요 -> 조준 카운터가 해금되었는지 여부를
- float aimThreshold = config.GetEffectiveAimThreshold(aimUnlocked); // 값을 가져올거에요 -> 현재 적용할 조준 임계값을
-
- if (tracker.AimHoldTime >= aimThreshold) // 조건이 맞으면 실행할거에요 -> 플레이어 조준 시간이 임계값 이상이라면
- {
- ActivateCounter(CounterType.Aim); // 함수를 실행할거에요 -> 조준 카운터를 발동시키는 ActivateCounter를
- }
-
- // ── 관통 카운터 ──
- bool pierceUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Pierce); // 값을 확인할거에요 -> 관통 카운터가 해금되었는지 여부를
- float pierceThreshold = config.GetEffectivePierceThreshold(pierceUnlocked); // 값을 가져올거에요 -> 현재 적용할 관통 임계값을
-
- if (tracker.TotalShotsInWindow >= config.minShotsForPierceCheck // 조건이 맞으면 실행할거에요 -> 최소 발사 횟수를 충족하고
- && tracker.PierceRatio >= pierceThreshold) // 조건이 맞으면 실행할거에요 -> 관통 공격 비율이 임계값 이상이라면
- {
- ActivateCounter(CounterType.Pierce); // 함수를 실행할거에요 -> 관통 카운터를 발동시키는 ActivateCounter를
- }
+ return false; // 임시 반환할거에요 -> 로직 구현 전까지 거짓(false)을
}
-
- private void ActivateCounter(CounterType type) // 함수를 선언할거에요 -> 특정 카운터를 활성화하는 ActivateCounter를
- {
- // 🚨 [안전장치] Config 확인
- if (config == null) return; // 조건이 맞으면 중단할거에요 -> 설정 파일이 없다면
-
- counterTimers[type] = config.counterDecayTime; // 값을 설정할거에요 -> 해당 카운터의 지속 시간을 설정값으로
-
- if (!activeCounters[type]) // 조건이 맞으면 실행할거에요 -> 해당 카운터가 현재 꺼져 있다면
- {
- activeCounters[type] = true; // 상태를 바꿀거에요 -> 카운터 활성 상태를 참(true)으로
- currentRunActivatedCounters.Add(type); // 추가할거에요 -> 이번 런 발동 목록에 해당 타입을
-
- // 영구 잠금 해제
- var persistence = BossCounterPersistence.Instance; // 변수를 가져올거에요 -> 영구 저장소를 persistence에
- if (persistence != null) // 조건이 맞으면 실행할거에요 -> 저장소가 있다면
- {
- persistence.UnlockCounter(type); // 함수를 실행할거에요 -> 해당 카운터를 영구 해금하는 UnlockCounter를
- persistence.RecordActivation(type); // 함수를 실행할거에요 -> 발동 횟수를 기록하는 RecordActivation을
- }
-
- OnCounterActivated?.Invoke(type); // 이벤트를 실행할거에요 -> 카운터 활성화 알림 이벤트를
- Debug.Log($"[BossCounter] {type} 카운터 활성화!"); // 로그를 출력할거에요 -> 카운터 활성화 메시지를
- }
- }
-
- /// 시간이 지나면 카운터 모드 자동 해제
- private void DecayCounters() // 함수를 선언할거에요 -> 시간이 지나면 카운터를 끄는 DecayCounters를
- {
- foreach (var type in new[] { CounterType.Dodge, CounterType.Aim, CounterType.Pierce }) // 반복할거에요 -> 모든 카운터 타입에 대해
- {
- if (!activeCounters[type]) continue; // 조건이 맞으면 건너뛸거에요 -> 이미 꺼져 있는 카운터라면
-
- counterTimers[type] -= Time.deltaTime; // 값을 뺄거에요 -> 남은 시간에서 프레임 시간을
- if (counterTimers[type] <= 0f) // 조건이 맞으면 실행할거에요 -> 남은 시간이 0 이하라면
- {
- activeCounters[type] = false; // 상태를 바꿀거에요 -> 카운터 활성 상태를 거짓(false)으로
- OnCounterDeactivated?.Invoke(type); // 이벤트를 실행할거에요 -> 카운터 비활성화 알림 이벤트를
- Debug.Log($"[BossCounter] {type} 카운터 Decay로 비활성화"); // 로그를 출력할거에요 -> 시간 만료로 인한 비활성화 메시지를
- }
- }
- }
-
- // ═══════════════════════════════════════════
- // 습관 변경 보상
- // ═══════════════════════════════════════════
-
- ///
- /// 이전 런에서 카운터 당한 습관을 이번 런에서 보이지 않으면 보상.
- /// 전투 중반(30초 이후) 한 번 체크.
- ///
- private float battleTimer = 0f; // 변수를 초기화할거에요 -> 전투 진행 시간을 0으로
- private const float HABIT_CHECK_TIME = 30f; // 상수를 정의할거에요 -> 습관 체크 시점을 30초로
-
- private void CheckHabitChangeReward() // 함수를 선언할거에요 -> 습관 변경 보상을 확인하는 CheckHabitChangeReward를
- {
- if (habitChangeRewardGranted || previousRunCounters.Count == 0) return; // 조건이 맞으면 중단할거에요 -> 이미 보상을 받았거나 이전 런 기록이 없다면
-
- battleTimer += Time.deltaTime; // 값을 더할거에요 -> 전투 진행 시간에 프레임 시간을
- if (battleTimer < HABIT_CHECK_TIME) return; // 조건이 맞으면 중단할거에요 -> 아직 30초가 안 지났다면
-
- // 이전 런에서 발동된 카운터 중, 이번 런에서 아직 발동 안 된 것이 있으면 보상
- foreach (var prevCounter in previousRunCounters) // 반복할거에요 -> 이전 런 카운터 목록에 대해
- {
- if (!currentRunActivatedCounters.Contains(prevCounter)) // 조건이 맞으면 실행할거에요 -> 이번 런 목록에 해당 카운터가 없다면 (습관 고침)
- {
- habitChangeRewardGranted = true; // 상태를 바꿀거에요 -> 보상 지급 여부를 참(true)으로
- OnHabitChangeRewarded?.Invoke(); // 이벤트를 실행할거에요 -> 보상 지급 알림 이벤트를
- Debug.Log($"[BossCounter] 플레이어가 {prevCounter} 습관을 바꿈! 보상 부여"); // 로그를 출력할거에요 -> 보상 부여 메시지를
- break; // 중단할거에요 -> 보상은 한 번만 주므로 반복문을
- }
- }
- }
-
- // ═══════════════════════════════════════════
- // 패턴 선택 (보스 AI에서 호출)
- // ═══════════════════════════════════════════
-
- ///
- /// 현재 카운터 상태를 반영하여 보스 패턴을 가중치 랜덤으로 선택합니다.
- /// 보스 AI의 패턴 선택 시점에 호출하세요.
- ///
- /// 선택된 패턴 이름
- public string SelectBossPattern() // 함수를 선언할거에요 -> 보스 패턴을 선택해서 반환하는 SelectBossPattern을
- {
- if (bossPatterns.Count == 0) // 조건이 맞으면 실행할거에요 -> 등록된 패턴이 하나도 없다면
- {
- Debug.LogWarning("[BossCounter] 보스 패턴이 등록되지 않았습니다!"); // 경고 로그를 출력할거에요 -> 패턴 없음 경고를
- return "Normal"; // 값을 반환할거에요 -> 기본값 "Normal"을
- }
-
- // 🚨 [안전장치] Config가 없으면 그냥 랜덤 패턴 반환 (멈춤 방지)
- if (config == null) // 조건이 맞으면 실행할거에요 -> 설정 파일이 없다면
- {
- int randomIndex = Random.Range(0, bossPatterns.Count); // 값을 랜덤으로 뽑을거에요 -> 0부터 패턴 개수 사이의 인덱스를
- return bossPatterns[randomIndex].patternName; // 값을 반환할거에요 -> 랜덤하게 뽑힌 패턴 이름을
- }
-
- // 가중치 계산
- float[] weights = new float[bossPatterns.Count]; // 배열을 만들거에요 -> 각 패턴의 가중치를 담을 weights 배열을
- float totalWeight = 0f; // 변수를 초기화할거에요 -> 전체 가중치 합계를 0으로
- float counterWeight = 0f; // 변수를 초기화할거에요 -> 카운터 패턴들의 가중치 합계를 0으로
-
- for (int i = 0; i < bossPatterns.Count; i++) // 반복할거에요 -> 모든 패턴에 대해
- {
- weights[i] = bossPatterns[i].baseWeight; // 값을 넣을거에요 -> 기본 가중치를 배열에
-
- // 카운터 패턴 가중치 증가
- if (bossPatterns[i].counterType != CounterType.None // 조건이 맞으면 실행할거에요 -> 카운터 타입이 설정되어 있고
- && activeCounters.ContainsKey(bossPatterns[i].counterType) // 조건이 맞으면 실행할거에요 -> 딕셔너리에 키가 존재하며
- && activeCounters[bossPatterns[i].counterType]) // 조건이 맞으면 실행할거에요 -> 해당 카운터가 활성 상태라면
- {
- // 쿨타임 체크
- if (Time.time - lastCounterPatternTime >= config.counterCooldown) // 조건이 맞으면 실행할거에요 -> 마지막 사용 후 쿨타임이 지났다면
- {
- weights[i] += config.counterWeightBonus; // 값을 더할거에요 -> 카운터 보너스 가중치를
- }
- }
-
- // 보조 카운터 가중치
- if (bossPatterns[i].subCounterType != CounterType.None // 조건이 맞으면 실행할거에요 -> 보조 카운터 타입이 설정되어 있고
- && activeCounters.ContainsKey(bossPatterns[i].subCounterType) // 조건이 맞으면 실행할거에요 -> 딕셔너리에 키가 존재하며
- && activeCounters[bossPatterns[i].subCounterType]) // 조건이 맞으면 실행할거에요 -> 해당 보조 카운터가 활성 상태라면
- {
- weights[i] += config.counterSubWeightBonus; // 값을 더할거에요 -> 보조 카운터 보너스 가중치를
- }
-
- // 습관 변경 보상: 일반 패턴 가중치 감소 (= 난이도 완화)
- if (habitChangeRewardGranted && bossPatterns[i].counterType == CounterType.None) // 조건이 맞으면 실행할거에요 -> 보상을 받았고 일반 패턴이라면
- {
- weights[i] *= (1f - config.habitChangeRewardRatio); // 값을 곱할거에요 -> 가중치를 감소 비율만큼 줄여서
- }
-
- totalWeight += weights[i]; // 값을 더할거에요 -> 전체 가중치 합계에 현재 가중치를
-
- if (bossPatterns[i].counterType != CounterType.None) // 조건이 맞으면 실행할거에요 -> 카운터 패턴이라면
- counterWeight += weights[i]; // 값을 더할거에요 -> 카운터 가중치 합계에
- }
-
- // 빈도 상한 체크: 카운터 패턴 총 비율이 maxCounterFrequency를 넘지 않도록
- if (totalWeight > 0f && counterWeight / totalWeight > config.maxCounterFrequency) // 조건이 맞으면 실행할거에요 -> 카운터 패턴 비율이 최대 허용치를 초과한다면
- {
- float allowedCounterWeight = (totalWeight - counterWeight) * config.maxCounterFrequency / (1f - config.maxCounterFrequency); // 값을 계산할거에요 -> 허용 가능한 카운터 총 가중치를
- float scale = allowedCounterWeight / counterWeight; // 값을 계산할거에요 -> 가중치를 줄일 비율(scale)을
-
- for (int i = 0; i < bossPatterns.Count; i++) // 반복할거에요 -> 모든 패턴에 대해
- {
- if (bossPatterns[i].counterType != CounterType.None) // 조건이 맞으면 실행할거에요 -> 카운터 패턴이라면
- {
- float bonus = weights[i] - bossPatterns[i].baseWeight; // 값을 계산할거에요 -> 추가된 보너스 가중치를
- weights[i] = bossPatterns[i].baseWeight + bonus * scale; // 값을 수정할거에요 -> 보너스를 비율대로 줄여서 다시 적용
- }
- }
-
- // 총 가중치 재계산
- totalWeight = 0f; // 값을 초기화할거에요 -> 전체 합계를 0으로
- for (int i = 0; i < weights.Length; i++) // 반복할거에요 -> 모든 가중치에 대해
- totalWeight += weights[i]; // 값을 더할거에요 -> 전체 합계에
- }
-
- // 가중치 랜덤 선택
- float roll = Random.Range(0f, totalWeight); // 값을 랜덤으로 뽑을거에요 -> 0부터 전체 가중치 사이의 값을 roll에
- float cumulative = 0f; // 변수를 초기화할거에요 -> 누적 가중치를 0으로
-
- for (int i = 0; i < bossPatterns.Count; i++) // 반복할거에요 -> 모든 패턴에 대해
- {
- cumulative += weights[i]; // 값을 더할거에요 -> 누적 가중치에 현재 가중치를
- if (roll <= cumulative) // 조건이 맞으면 실행할거에요 -> 랜덤값이 누적치보다 작거나 같다면 (당첨)
- {
- string selected = bossPatterns[i].patternName; // 값을 저장할거에요 -> 선택된 패턴 이름을 selected에
-
- // 카운터 패턴이면 쿨타임 기록
- if (bossPatterns[i].counterType != CounterType.None) // 조건이 맞으면 실행할거에요 -> 카운터 패턴이라면
- {
- lastCounterPatternTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 사용 시간으로
- }
-
- OnCounterPatternSelected?.Invoke(selected); // 이벤트를 실행할거에요 -> 패턴 선택 알림 이벤트를
- return selected; // 값을 반환할거에요 -> 선택된 패턴 이름을
- }
- }
-
- return bossPatterns[bossPatterns.Count - 1].patternName; // 값을 반환할거에요 -> 만약 선택되지 않았다면 마지막 패턴을 (안전장치)
- }
-
- // ═══════════════════════════════════════════
- // 외부 조회 (여기가 아까 빠졌던 부분입니다!)
- // ═══════════════════════════════════════════
-
- /// 특정 카운터 모드가 현재 활성 상태인지 확인
- public bool IsCounterActive(CounterType type) // 함수를 선언할거에요 -> 특정 카운터 활성 여부를 반환하는 IsCounterActive를
- {
- return activeCounters.ContainsKey(type) && activeCounters[type]; // 값을 반환할거에요 -> 딕셔너리에 키가 있고 값이 참(true)인지를
- }
-
- /// 현재 활성화된 모든 카운터 타입 반환
- public List GetActiveCounters() // 함수를 선언할거에요 -> 활성화된 모든 카운터 목록을 반환하는 GetActiveCounters를
- {
- var result = new List(); // 리스트를 만들거에요 -> 결과를 담을 result 리스트를
- foreach (var kvp in activeCounters) // 반복할거에요 -> 모든 카운터 상태에 대해
- {
- if (kvp.Value) result.Add(kvp.Key); // 조건이 맞으면 실행할거에요 -> 활성화(true) 상태라면 리스트에 추가하기를
- }
- return result; // 값을 반환할거에요 -> 완성된 리스트를
- }
-
- /// 습관 변경 보상이 활성화되었는지 확인
- public bool IsHabitChangeRewarded => habitChangeRewardGranted; // 프로퍼티를 선언할거에요 -> 보상 지급 여부를 외부에서 읽을 수 있는 IsHabitChangeRewarded를
}
\ No newline at end of file
diff --git a/Assets/Scripts/Enemy/BossAI/BossMonster.cs b/Assets/Scripts/Enemy/BossAI/BossMonster.cs
index 37db27b7..bff46e77 100644
--- a/Assets/Scripts/Enemy/BossAI/BossMonster.cs
+++ b/Assets/Scripts/Enemy/BossAI/BossMonster.cs
@@ -1,443 +1,560 @@
using UnityEngine; // 유니티 기본 기능을 불러올거에요 -> UnityEngine을
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
+using System.Collections.Generic; // Dictionary를 사용할거에요 -> System.Collections.Generic을
public class NorcielBoss : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 NorcielBoss를
-{
- [Header("--- 🧠 두뇌 연결 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 🧠 두뇌 연결 ---을
- [SerializeField] private BossCounterSystem counterSystem; // 변수를 선언할거에요 -> 보스의 패턴을 결정할 counterSystem을
+{ // 코드 블록을 시작할거에요 -> NorcielBoss 범위를
- [Header("--- ⚔️ 패턴 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- ⚔️ 패턴 설정 ---을
- [SerializeField] private float patternInterval = 3f; // 변수를 선언할거에요 -> 공격 후 다음 공격까지 대기 시간을 3초로 지정하는 patternInterval을
- [SerializeField] private float attackRange = 3f; // 변수를 선언할거에요 -> 이 거리 안에 플레이어가 들어오면 공격하는 attackRange를 3으로
+ [Header("--- 🧠 두뇌 연결 ---")] // 인스펙터 제목을 달거에요 -> 두뇌 연결 설정을
+ [SerializeField] private BossCounterSystem counterSystem; // 변수를 선언할거에요 -> 패턴 결정을 위한 카운터 시스템을
- [Header("--- 🎱 무기(쇠공) 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 🎱 무기 설정 ---을
- [SerializeField] private GameObject ironBall; // 변수를 선언할거에요 -> 던지고 주울 무기 오브젝트인 ironBall을
- [SerializeField] private Transform handHolder; // 변수를 선언할거에요 -> 무기를 잡을 손의 위치인 handHolder를
+ [Header("--- ⚔️ 패턴 설정 ---")] // 인스펙터 제목을 달거에요 -> 패턴 설정을
+ [SerializeField] private float patternInterval = 3f; // 변수를 선언할거에요 -> 공격 후 다음 공격까지 딜레이 시간을
+ [SerializeField] private float attackRange = 3f; // 변수를 선언할거에요 -> 공격이 가능한 사거리를
- [Header("--- 📊 UI 연결 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 📊 UI 연결 ---을
- [SerializeField] private GameObject bossHealthBar; // 변수를 선언할거에요 -> 보스의 체력 UI를 담을 bossHealthBar를
+ [Header("--- 🎱 무기(쇠공) 설정 ---")] // 인스펙터 제목을 달거에요 -> 무기 설정을
+ [SerializeField] private GameObject ironBall; // 변수를 선언할거에요 -> 보스가 던질 쇠공 오브젝트를
+ [SerializeField] private Transform handHolder; // 변수를 선언할거에요 -> 쇠공을 쥘 손의 위치를
- // ⭐ [핵심 수정] 애니메이션 이름을 인스펙터에서 맞출 수 있게 변수로 분리
- [Header("--- 🎬 애니메이션 이름 설정 (Animator와 일치시킬 것!) ---")]
+ [Header("--- 📊 UI 연결 ---")] // 인스펙터 제목을 달거에요 -> UI 설정을
+ [SerializeField] private GameObject bossHealthBar; // 변수를 선언할거에요 -> 화면에 보일 보스 체력바를
+
+ [Header("--- 🎬 애니메이션 이름 설정 (Animator와 일치시킬 것!) ---")] // 인스펙터 제목을 달거에요 -> 애니메이션 이름 설정을
[SerializeField] private string anim_Roar = "Roar"; // 변수를 선언할거에요 -> 포효 애니메이션 이름을
[SerializeField] private string anim_Idle = "Monster_Idle"; // 변수를 선언할거에요 -> 대기 애니메이션 이름을
[SerializeField] private string anim_Walk = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을
- [SerializeField] private string anim_Pickup = "Skill_Pickup"; // 변수를 선언할거에요 -> 무기 줍기 애니메이션 이름을
+ [SerializeField] private string anim_Pickup = "Skill_Pickup"; // 변수를 선언할거에요 -> 줍기 애니메이션 이름을
[SerializeField] private string anim_Throw = "Attack_Throw"; // 변수를 선언할거에요 -> 던지기 애니메이션 이름을
- [Header("--- 🎬 스킬 애니메이션 이름 ---")]
- [SerializeField] private string anim_DashReady = "Skill_Dash_Ready"; // 변수를 선언할거에요 -> 돌진 준비 애니메이션 이름을
- [SerializeField] private string anim_DashGo = "Skill_Dash_Go"; // 변수를 선언할거에요 -> 돌진 출발 애니메이션 이름을
- [SerializeField] private string anim_SmashCharge = "Skill_Smash_Charge"; // 변수를 선언할거에요 -> 내려찍기 기모으기 이름을
- [SerializeField] private string anim_SmashImpact = "Skill_Smash_Impact"; // 변수를 선언할거에요 -> 내려찍기 타격 이름을
- [SerializeField] private string anim_Sweep = "Skill_Sweep"; // 변수를 선언할거에요 -> 휩쓸기 애니메이션 이름을
-
- [Header("--- 🔍 디버그 (읽기 전용) ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 🔍 디버그 ---를
- [SerializeField] private string debugState = "대기 중"; // 변수를 선언할거에요 -> 현재 보스 행동을 문장으로 보여줄 debugState를
-
- // --- 내부 변수들 ---
- private float _timer; // 변수를 선언할거에요 -> 공격 쿨타임을 계산할 내부 타이머 _timer를
- private Rigidbody rb; // 변수를 선언할거에요 -> 보스의 물리 엔진을 제어할 rb를
- private Rigidbody ballRb; // 변수를 선언할거에요 -> 쇠공의 물리 엔진을 제어할 ballRb를
-
- private bool isBattleStarted = false; // 변수를 초기화할거에요 -> 전투 시작 상태를 거짓(false)으로
- private bool isWeaponless = false; // 변수를 초기화할거에요 -> 맨손 상태를 거짓(false)으로
- private bool isPerformingAction = false; // 변수를 초기화할거에요 -> 연출 진행 상태를 거짓(false)으로
- private Transform target; // 변수를 선언할거에요 -> 보스가 쫓아갈 대상의 위치인 target을
-
- protected override void Awake() // 함수를 실행할거에요 -> 스크립트가 켜질 때 가장 먼저 호출되는 Awake를
- {
- base.Awake(); // 부모 클래스의 함수를 먼저 실행할거에요 -> MonsterClass의 Awake 로직을
- rb = GetComponent(); // 컴포넌트를 가져와서 저장할거에요 -> 보스 자신의 Rigidbody를 rb에
- if (ironBall != null) ballRb = ironBall.GetComponent(); // 조건이 맞으면 가져올거에요 -> ironBall이 있다면 그것의 Rigidbody를 ballRb에
- }
-
- private void Start() // 함수를 실행할거에요 -> 첫 프레임이 시작될 때 호출되는 Start를
- {
- StartBossBattle(); // 함수를 실행할거에요 -> Roar 연출 후 전투를 시작하는 StartBossBattle을
- }
-
- protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 몬스터 초기화를 담당하는 Init을
- {
- base.Init(); // 부모 초기화 호출
- _timer = 0f; // 값을 넣을거에요 -> 전투 시작하자마자 첫 공격이 가능하게 타이머를 0으로
- isBattleStarted = false; // 상태를 바꿀거에요 -> 전투 시작 상태를 거짓(false)으로
- isWeaponless = false; // 상태를 바꿀거에요 -> 맨손 상태를 거짓(false)으로
- isPerformingAction = false; // 상태를 바꿀거에요 -> 연출 상태를 거짓(false)으로
-
- IsAggroed = true; // 상태를 바꿀거에요 -> 부모 클래스의 영구 어그로 상태를 참(true)으로
- mobRenderer = null; // 값을 지울거에요 -> 화면 밖 최적화를 끄기 위해 렌더러를 null로
- optimizationDistance = 9999f; // 값을 바꿀거에요 -> 거리가 멀어져도 멈추지 않게 최적화 거리를 9999로
-
- GameObject playerObj = GameObject.FindWithTag("Player"); // 오브젝트를 찾을거에요 -> Player 태그가 붙은 녀석을
- if (playerObj != null) // 조건이 맞으면 실행할거에요 -> 태그로 찾은 플레이어가 존재한다면
- {
- target = playerObj.transform; // 값을 넣을거에요 -> 플레이어의 위치를 target에
- playerTransform = playerObj.transform; // 값을 넣을거에요 -> 부모의 추적 변수에도 플레이어의 위치를
- }
- else // 조건이 틀리면 실행할거에요 -> 태그로 찾지 못했다면
- {
- var playerScript = FindObjectOfType(); // 컴포넌트를 찾을거에요 -> PlayerMovement 스크립트를 가진 오브젝트를
- if (playerScript != null) // 조건이 맞으면 실행할거에요 -> 스크립트로 플레이어를 찾았다면
- {
- target = playerScript.transform; // 값을 넣을거에요 -> 플레이어의 위치를 target에
- playerTransform = playerScript.transform; // 값을 넣을거에요 -> 부모의 추적 변수에도 플레이어의 위치를
- }
- }
-
- if (target == null) // 조건이 맞으면 실행할거에요 -> 어떤 방법으로도 플레이어를 못 찾았다면
- Debug.LogError("❌ [Boss] 플레이어를 찾지 못했습니다! Player 태그 또는 PlayerMovement 확인!"); // 에러를 출력할거에요 -> 플레이어 없음 경고 메시지를
-
- currentHP = maxHP; // 체력을 채울거에요 -> 현재 체력을 최대 체력으로
- StartCoroutine(SafeInitNavMesh()); // 코루틴을 실행할거에요 -> 길찾기를 안전하게 켜는 SafeInitNavMesh를
-
- if (bossHealthBar != null) bossHealthBar.SetActive(false); // 조건이 맞으면 실행할거에요 -> 체력바 UI가 있다면 화면에서 끄기를
- if (ballRb != null) ballRb.isKinematic = true; // 조건이 맞으면 실행할거에요 -> 쇠공 물리가 있다면 중력 연산을 끄기를
- if (animator != null) animator.Play(anim_Idle); // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면 대기 모션을
- }
-
- private IEnumerator SafeInitNavMesh() // 코루틴 함수를 선언할거에요 -> 길찾기를 안전하게 초기화하는 SafeInitNavMesh를
- {
- yield return null; // 기다릴거에요 -> 다음 프레임이 될 때까지 한 턴을
- if (agent != null) // 조건이 맞으면 실행할거에요 -> NavMeshAgent가 존재한다면
- {
- int retry = 0; // 변수를 선언할거에요 -> 재시도 횟수를 셀 retry를 0으로
- while (!agent.isOnNavMesh && retry < 5) // 반복할거에요 -> 바닥에 없고 시도가 5번 미만인 동안
- {
- retry++; // 값을 증가시킬거에요 -> 재시도 횟수를 1만큼
- yield return new WaitForSeconds(0.1f); // 기다릴거에요 -> 0.1초의 시간을
- }
- if (agent.isOnNavMesh) agent.isStopped = true; // 조건이 맞으면 실행할거에요 -> 바닥에 닿았다면 이동을 정지로
- agent.enabled = false; // 기능을 끌거에요 -> 전투 시작 전까지 NavMeshAgent를 비활성화로
- }
- }
-
- protected override void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를
- {
- base.Update(); // 부모 클래스의 Update도 호출 (최적화 로직 등)
-
- if (!isDead) // 조건이 맞으면 실행할거에요 -> 보스가 살아있다면
- {
- ExecuteAILogic(); // 함수를 실행할거에요 -> 보스 AI 두뇌인 ExecuteAILogic을
- }
- }
-
- // ════════════════════════════════════════
- // 🧠 AI 핵심 로직
- // ════════════════════════════════════════
-
- protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> 보스 AI 두뇌인 ExecuteAILogic을
- {
- if (isPerformingAction || !isBattleStarted || target == null) return; // 조건이 맞으면 중단할거에요 -> 연출중, 전투전, 타겟없음 중 하나라도 해당되면
-
- if (isWeaponless) // 조건이 맞으면 실행할거에요 -> 쇠공을 던져서 맨손이라면
- {
- RetrieveWeaponLogic(); // 함수를 실행할거에요 -> 쇠공을 주우러 가는 로직을
- return; // 중단할거에요 -> 무기 줍기가 우선이므로 아래 로직을
- }
-
- if (isAttacking || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면 AI 판단을
-
- if (_timer > 0) // 조건이 맞으면 실행할거에요 -> 공격 쿨타임이 남아있다면
- {
- _timer -= Time.deltaTime; // 값을 뺄거에요 -> 쿨타임 타이머에서 지난 프레임 시간만큼을
- }
-
- float distToPlayer = Vector3.Distance(transform.position, target.position); // 거리를 계산할거에요 -> 보스와 플레이어 사이의 거리를
-
- if (agent == null || !agent.enabled || !agent.isOnNavMesh) return; // 조건이 맞으면 중단할거에요 -> 길찾기가 불가능한 상태라면
-
- if (distToPlayer > attackRange) // 조건이 맞으면 실행할거에요 -> 플레이어가 공격 사거리 밖이라면
- {
- agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀고 걸어가라고
- agent.SetDestination(target.position); // 명령을 내릴거에요 -> 길찾기 목표를 플레이어 위치로
- LookAtTarget(target.position); // 함수를 실행할거에요 -> 플레이어를 바라보는 LookAtTarget을
- UpdateMovementAnimation(); // 함수를 실행할거에요 -> 걷기 애니메이션을 맞추는 UpdateMovementAnimation을
- }
- else // 조건이 틀리면 실행할거에요 -> 플레이어가 사거리 안에 있다면
- {
- agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고
- agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 이동 속도를 완전한 0으로
- LookAtTarget(target.position); // 함수를 실행할거에요 -> 플레이어를 바라보는 LookAtTarget을
-
- if (animator != null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 존재한다면
- {
- AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); // 변수를 선언할거에요 -> 현재 재생 중인 애니메이션 상태를
- if (state.IsName(anim_Walk)) // 조건이 맞으면 실행할거에요 -> 걷기 모션이 재생 중이라면
- {
- animator.Play(anim_Idle); // 애니메이션을 바꿀거에요 -> 대기 모션으로
- }
- }
-
- if (_timer <= 0) // 조건이 맞으면 실행할거에요 -> 공격 쿨타임이 0 이하라면
- {
- _timer = patternInterval; // 값을 넣을거에요 -> 쿨타임을 다시 패턴 대기 시간으로
- Debug.Log($"⏰ [Boss] 사거리 내 공격! 거리={distToPlayer:F1}m"); // 로그를 출력할거에요 -> 공격 시점과 거리 정보를
- DecideAttack(); // 함수를 실행할거에요 -> 어떤 공격을 할지 고르는 DecideAttack을
- }
- }
- }
-
- // ════════════════════════════════════════
- // 🎬 전투 입장
- // ════════════════════════════════════════
-
- public void StartBossBattle() // 함수를 선언할거에요 -> 외부에서 전투를 시작시킬 수 있는 StartBossBattle을
- {
- if (isBattleStarted) return; // 조건이 맞으면 중단할거에요 -> 이미 전투가 시작되었다면 중복 실행을
- StartCoroutine(BattleStartRoutine()); // 코루틴을 실행할거에요 -> Roar 연출 후 전투를 시작하는 BattleStartRoutine을
- }
-
- private IEnumerator BattleStartRoutine() // 코루틴 함수를 선언할거에요 -> Roar 연출과 전투 시작을 담당하는 BattleStartRoutine을
- {
- Debug.Log("🔥 [Boss] Roar 시작!"); // 로그를 출력할거에요 -> Roar가 시작되었다는 메시지를
- isPerformingAction = true; // 상태를 바꿀거에요 -> 연출 중 플래그를 참(true)으로
-
- if (bossHealthBar != null) bossHealthBar.SetActive(true); // 조건이 맞으면 실행할거에요 -> 체력바 UI가 있다면 화면에 켜기를
- if (counterSystem != null) counterSystem.InitializeBattle(); // 조건이 맞으면 실행할거에요 -> 카운터 시스템이 있다면 전투 초기화를
-
- if (animator != null) animator.Play(anim_Roar, 0, 0f); // 조건이 맞으면 실행할거에요 -> Roar 애니메이션을 처음부터 재생을
-
- yield return new WaitForSeconds(2.0f); // 기다릴거에요 -> Roar 애니메이션이 끝날 2초의 시간을
-
- if (animator != null) animator.Play(anim_Idle, 0, 0f); // 조건이 맞으면 실행할거에요 -> Roar가 끝났으니 대기 모션으로 전환을
-
- isPerformingAction = false; // 상태를 바꿀거에요 -> 연출 중 플래그를 거짓(false)으로
- isBattleStarted = true; // 상태를 바꿀거에요 -> 전투 시작 플래그를 참(true)으로
-
- Debug.Log("⚔️ [Boss] Roar 종료 → 전투 루프 시작!"); // 로그를 출력할거에요 -> 전투 루프가 시작되었다는 메시지를
-
- if (agent != null) // 조건이 맞으면 실행할거에요 -> 길찾기 에이전트가 존재한다면
- {
- agent.enabled = true; // 기능을 켤거에요 -> 길찾기 에이전트를 활성화(true)로
- int retry = 0; // 변수를 선언할거에요 -> 재시도 횟수를 셀 retry를 0으로
- while (!agent.isOnNavMesh && retry < 10) // 반복할거에요 -> NavMesh에 안 닿았고 10번 미만인 동안
- {
- retry++; // 값을 증가시킬거에요 -> 재시도 횟수를 1만큼
- yield return new WaitForSeconds(0.1f); // 기다릴거에요 -> 0.1초의 시간을
- }
-
- if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 올라갔다면
- {
- agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀고 추격을 시작하라고
- Debug.Log("✅ [Boss] NavMesh 연결 완료, 추격 시작"); // 로그를 출력할거에요 -> NavMesh 연결 성공 메시지를
- }
- else // 조건이 틀리면 실행할거에요 -> NavMesh에 못 올라갔다면
- {
- Debug.LogError("❌ [Boss] NavMesh 연결 실패! 보스 위치를 확인하세요!"); // 에러를 출력할거에요 -> 연결 실패 경고를
- }
- }
- }
-
- // ════════════════════════════════════════
- // 🚶 이동 애니메이션 & 타겟팅
- // ════════════════════════════════════════
-
- private void UpdateMovementAnimation() // 함수를 선언할거에요 -> 이동 상태에 맞는 애니메이션을 재생하는 UpdateMovementAnimation을
- {
- if (animator == null || isPerformingAction || isAttacking || isHit || isDead) return; // 조건이 맞으면 중단할거에요 -> 애니메이션을 바꿀 수 없는 상태라면
-
- AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); // 변수를 선언할거에요 -> 현재 재생 중인 애니메이션 상태를
-
- if (agent != null && agent.velocity.magnitude > 0.1f) // 조건이 맞으면 실행할거에요 -> 현재 이동 속도가 0.1보다 크다면
- {
- if (!state.IsName(anim_Walk)) // 조건이 맞으면 실행할거에요 -> 아직 걷기 모션이 아니라면
- animator.Play(anim_Walk); // 애니메이션을 바꿀거에요 -> 걷기 모션으로
- }
- else // 조건이 틀리면 실행할거에요 -> 거의 멈춰있다면
- {
- if (!state.IsName(anim_Idle) && !state.IsName(anim_Roar)) // 조건이 맞으면 실행할거에요 -> 대기나 포효 모션이 아니라면
- animator.Play(anim_Idle); // 애니메이션을 바꿀거에요 -> 대기 모션으로
- }
- }
-
- private void LookAtTarget(Vector3 targetPos) // 함수를 선언할거에요 -> 타겟을 부드럽게 쳐다보는 LookAtTarget을
- {
- Vector3 dir = targetPos - transform.position; // 벡터를 계산할거에요 -> 내 위치에서 타겟까지의 방향을
- dir.y = 0; // 값을 바꿀거에요 -> 위아래로 고개가 꺾이지 않게 y를 0으로
- if (dir != Vector3.zero) // 조건이 맞으면 실행할거에요 -> 방향이 0이 아니라면
- {
- transform.rotation = Quaternion.Slerp( // 회전시킬거에요 -> 현재 회전에서 타겟 방향으로 부드럽게
- transform.rotation, // 시작값으로 사용할거에요 -> 현재 회전값을
- Quaternion.LookRotation(dir), // 목표값으로 사용할거에요 -> 타겟을 향하는 회전값을
- Time.deltaTime * 5f // 속도를 지정할거에요 -> 프레임 시간의 5배 속도로
- );
- }
- }
-
- // ════════════════════════════════════════
- // 🧠 공격 판단
- // ════════════════════════════════════════
-
- private void DecideAttack() // 함수를 선언할거에요 -> 어떤 공격 패턴을 쓸지 고르는 DecideAttack을
- {
- if (target != null) LookAtTarget(target.position); // 조건이 맞으면 실행할거에요 -> 타겟이 있다면 그쪽을 바라보기를
-
- string patternName = (counterSystem != null) ? counterSystem.SelectBossPattern() : "Normal"; // 변수를 선언할거에요 -> 카운터 시스템에서 받은 패턴 이름을 patternName에
- Debug.Log($"🧠 [Boss] 패턴 선택: {patternName}"); // 로그를 출력할거에요 -> 선택된 패턴 이름을
-
- switch (patternName) // 분기할거에요 -> 받아온 패턴 이름에 따라서
- {
- case "DashAttack": StartCoroutine(Pattern_DashAttack()); break; // 일치하면 실행할거에요 -> 돌진 공격을
- case "Smash": StartCoroutine(Pattern_SmashAttack()); break; // 일치하면 실행할거에요 -> 내려찍기 공격을
- case "ShieldWall": StartCoroutine(Pattern_ShieldWall()); break; // 일치하면 실행할거에요 -> 휩쓸기 공격을
- default: StartCoroutine(Pattern_ThrowBall()); break; // 맞는게 없으면 실행할거에요 -> 공 던지기 공격을
- }
- }
-
- // ════════════════════════════════════════
- // 🏃 무기 회수
- // ════════════════════════════════════════
-
- private void RetrieveWeaponLogic() // 함수를 선언할거에요 -> 던진 쇠공을 주우러 가는 RetrieveWeaponLogic을
- {
- if (ironBall == null || !agent.enabled || !agent.isOnNavMesh) return; // 조건이 맞으면 중단할거에요 -> 쇠공이 없거나 길찾기가 불가능하다면
-
- agent.SetDestination(ironBall.transform.position); // 명령을 내릴거에요 -> 길찾기 목표를 쇠공의 위치로
- agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀고 걸어가라고
- UpdateMovementAnimation(); // 함수를 실행할거에요 -> 걸어갈 때 이동 애니메이션이 나오도록
-
- if (Vector3.Distance(transform.position, ironBall.transform.position) <= 3.0f && !isAttacking) // 조건이 맞으면 실행할거에요 -> 쇠공과 3m 이내이고 공격 중이 아니라면
- StartCoroutine(PickUpBallRoutine()); // 코루틴을 실행할거에요 -> 바닥의 쇠공을 줍는 PickUpBallRoutine을
- }
-
- private IEnumerator PickUpBallRoutine() // 코루틴 함수를 선언할거에요 -> 쇠공 줍기 행동인 PickUpBallRoutine을
- {
- OnAttackStart(); // 상태를 켤거에요 -> 다른 행동과 겹치지 않게 공격 플래그를
- if (agent != null && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 조건이 맞으면 실행할거에요 -> 줍기 위해 이동을 멈추고 속도를 0으로
- if (animator != null) animator.Play(anim_Pickup); // 조건이 맞으면 실행할거에요 -> 줍는 애니메이션을
- yield return new WaitForSeconds(0.8f); // 기다릴거에요 -> 손이 바닥에 닿을 0.8초를
-
- ironBall.transform.SetParent(handHolder); // 부모를 바꿀거에요 -> 쇠공을 보스의 손 밑으로
- ironBall.transform.localPosition = Vector3.zero; // 위치를 바꿀거에요 -> 쇠공의 위치를 손의 정중앙으로
- ironBall.transform.localRotation = Quaternion.identity; // 회전을 바꿀거에요 -> 쇠공의 회전을 기본값으로
- if (ballRb != null) { ballRb.isKinematic = true; ballRb.velocity = Vector3.zero; } // 조건이 맞으면 실행할거에요 -> 쇠공의 물리를 끄고 속도를 0으로
-
- yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 일어서는 애니메이션이 끝날 1초를
- isWeaponless = false; // 상태를 바꿀거에요 -> 무기를 다시 쥐었으므로 맨손 상태를 거짓으로
- OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 처리를
- if (agent != null && agent.isOnNavMesh) agent.isStopped = false; // 조건이 맞으면 실행할거에요 -> 다시 추격을 위해 이동 정지 해제를
- }
-
- // ════════════════════════════════════════
- // ✅ 공격 시작/종료 처리 (핵심)
- // ════════════════════════════════════════
-
- public override void OnAttackStart() // 함수를 덮어씌워 실행할거에요 -> 공격 시작 처리를 하는 OnAttackStart를
- {
- isAttacking = true; // 상태를 바꿀거에요 -> 공격 중임을 참으로
- isPerformingAction = true; // 상태를 바꿀거에요 -> 다른 행동 불가 상태로
- if (agent != null) agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고
- }
-
- public override void OnAttackEnd() // 함수를 덮어씌워 실행할거에요 -> 공격 종료 처리를 하는 OnAttackEnd를
- {
- StartCoroutine(RecoverRoutine()); // 코루틴을 시작할거에요 -> 후딜레이(회복) 처리를 위한 RecoverRoutine을
- }
-
- private IEnumerator RecoverRoutine() // 코루틴 함수를 선언할거에요 -> 공격 후 대기 시간을 갖는 RecoverRoutine을
- {
- isAttacking = false; // 상태를 바꿀거에요 -> 공격이 끝났으므로 공격 중 상태를 거짓으로
- isResting = true; // 상태를 바꿀거에요 -> 휴식 중 상태를 참으로 (이때는 추격 안 함)
-
- if (animator != null) animator.Play(anim_Idle); // 조건이 맞으면 실행할거에요 -> 대기 모션으로 전환을
-
- // 딜레이 시간 (여기서 patternInterval 만큼 멍때림)
- yield return new WaitForSeconds(patternInterval); // 기다릴거에요 -> 설정된 패턴 간격 시간만큼
-
- isResting = false; // 상태를 바꿀거에요 -> 휴식이 끝났으므로 거짓으로
- isPerformingAction = false; // 상태를 바꿀거에요 -> 이제 다시 다른 행동이 가능하도록 거짓으로
- }
-
- // ════════════════════════════════════════
- // ⚔️ 공격 패턴들
- // ════════════════════════════════════════
-
- // 패턴 1: 돌진 공격
- private IEnumerator Pattern_DashAttack() // 코루틴 함수를 선언할거에요 -> 돌진 패턴인 Pattern_DashAttack을
- {
- OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를
- if (agent != null) agent.enabled = false; // 조건이 맞으면 실행할거에요 -> 돌진 시 물리력을 위해 길찾기를 끄기로
-
- if (animator != null) animator.Play(anim_DashReady, 0, 0f); // 조건이 맞으면 실행할거에요 -> 돌진 준비 모션을
- yield return new WaitForSeconds(0.5f); // 기다릴거에요 -> 준비 포즈를 취할 0.5초를
-
- if (rb != null) { rb.isKinematic = false; rb.velocity = transform.forward * 20f; } // 조건이 맞으면 실행할거에요 -> 물리를 켜고 앞으로 속도 20으로 돌진을
- if (animator != null) animator.Play(anim_DashGo, 0, 0f); // 조건이 맞으면 실행할거에요 -> 돌진 애니메이션을
- yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 돌진이 끝날 1초를
-
- if (rb != null) { rb.velocity = Vector3.zero; rb.isKinematic = true; } // 조건이 맞으면 실행할거에요 -> 속도를 0으로 멈추고 물리를 끄기로
- if (agent != null && isBattleStarted) agent.enabled = true; // 조건이 맞으면 실행할거에요 -> 전투 중이면 길찾기를 다시 켜기로
- OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 및 딜레이 처리를
- }
-
- // 패턴 2: 내려찍기
- private IEnumerator Pattern_SmashAttack() // 코루틴 함수를 선언할거에요 -> 내려찍기 패턴인 Pattern_SmashAttack을
- {
- OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를
- if (animator != null) animator.Play(anim_SmashCharge, 0, 0f); // 조건이 맞으면 실행할거에요 -> 기를 모으는 모션을
- yield return new WaitForSeconds(1.2f); // 기다릴거에요 -> 기를 모을 1.2초를
- if (animator != null) animator.Play(anim_SmashImpact, 0, 0f); // 조건이 맞으면 실행할거에요 -> 내려찍는 모션을
- yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 자세를 가다듬을 1초를
- OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 및 딜레이 처리를
- }
-
- // 패턴 3: 휩쓸기
- private IEnumerator Pattern_ShieldWall() // 코루틴 함수를 선언할거에요 -> 휩쓸기 패턴인 Pattern_ShieldWall을
- {
- OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를
- if (animator != null) animator.Play(anim_Sweep, 0, 0f); // 조건이 맞으면 실행할거에요 -> 휩쓰는 모션을
- yield return new WaitForSeconds(2.0f); // 기다릴거에요 -> 휘두르기가 끝날 2초를
- OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 및 딜레이 처리를
- }
-
- // 패턴 4: 공 던지기
- private IEnumerator Pattern_ThrowBall() // 코루틴 함수를 선언할거에요 -> 쇠공 던지기 패턴인 Pattern_ThrowBall을
- {
- OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를
- if (animator != null) animator.Play(anim_Throw, 0, 0f); // 조건이 맞으면 실행할거에요 -> 던지는 모션을
- yield return new WaitForSeconds(0.5f); // 기다릴거에요 -> 공을 던지기 직전인 0.5초를
-
- if (ironBall != null && ballRb != null) // 조건이 맞으면 실행할거에요 -> 쇠공과 물리 엔진이 존재한다면
- {
- ironBall.transform.SetParent(null); // 부모를 바꿀거에요 -> 쇠공을 손에서 완전히 분리로
- ballRb.isKinematic = false; // 기능을 켤거에요 -> 쇠공의 물리 연산을 활성화로
- Vector3 dir = (target.position - transform.position).normalized; // 벡터를 계산할거에요 -> 플레이어를 향하는 방향을
- ballRb.AddForce(dir * 20f + Vector3.up * 5f, ForceMode.Impulse); // 힘을 가할거에요 -> 앞으로 20, 위로 5의 포물선 힘을
- ballRb.angularDrag = 5f; // 마찰을 줄거에요 -> 쇠공이 멀리 안 굴러가게 회전 마찰 5를
- }
-
- isWeaponless = true; // 상태를 바꿀거에요 -> 던졌으므로 맨손 상태를 참으로
- yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 던지고 자세를 가다듬을 1초를
- OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 및 딜레이 처리를
- }
-
- // ════════════════════════════════════════
- // 💥 피격 / 사망
- // ════════════════════════════════════════
-
- protected override void OnStartHit() // 함수를 덮어씌워 실행할거에요 -> 보스가 맞았을 때 호출되는 OnStartHit을
- {
- // 보스는 피격되어도 행동이 끊기지 않습니다 (슈퍼아머)
- // 만약 끊기게 하려면 여기서 isPerformingAction = false;
- }
-
- protected override void OnDie() // 함수를 덮어씌워 실행할거에요 -> 보스 체력이 0이 되면 호출되는 OnDie를
- {
- isBattleStarted = false; // 상태를 바꿀거에요 -> 전투가 끝났으므로 전투 플래그를 거짓으로
- isPerformingAction = false; // 상태를 바꿀거에요 -> 연출 플래그를 거짓으로
- if (bossHealthBar != null) bossHealthBar.SetActive(false); // 조건이 맞으면 실행할거에요 -> 체력바 UI가 있다면 화면에서 끄기를
- GiveBossXP(); // 함수를 실행할거에요 -> 경험치를 주는 GiveBossXP를
- base.OnDie(); // 부모 클래스의 사망 처리를 호출할거에요 -> 애니메이션, 시체 제거 등
- }
-
- private void GiveBossXP() // 함수를 선언할거에요 -> 보스 처치 보상을 주는 GiveBossXP를
- {
- Debug.Log("[BossController] 보스 처치 시도 중..."); // 로그를 출력할거에요 -> 보스 처치 시도 메시지를
- if (ObsessionSystem.instance != null) // 조건이 맞으면 실행할거에요 -> 경험치 시스템이 존재한다면
- ObsessionSystem.instance.AddRunXP(100); // 함수를 실행할거에요 -> 경험치 100을 주는 AddRunXP를
- else // 조건이 틀리면 실행할거에요 -> 시스템을 못 찾았다면
- Debug.LogError("[BossController] ObsessionSystem 인스턴스를 찾을 수 없습니다!"); // 에러를 출력할거에요 -> 시스템 누락 경고를
- Debug.Log("[BossController] 보스 처치 처리 완료!"); // 로그를 출력할거에요 -> 빨간 글씨로 처치 완료 메시지를
- }
-}
\ No newline at end of file
+ [Header("--- 🎬 스킬 애니메이션 이름 ---")] // 인스펙터 제목을 달거에요 -> 스킬 애니메이션 설정을
+ [SerializeField] private string anim_DashReady = "Skill_Dash_Ready"; // 변수를 선언할거에요 -> 돌진 준비 애니 이름을
+ [SerializeField] private string anim_DashGo = "Skill_Dash_Go"; // 변수를 선언할거에요 -> 돌진 애니 이름을
+ [SerializeField] private string anim_SmashCharge = "Skill_Smash_Charge"; // 변수를 선언할거에요 -> 찍기 차징 애니 이름을
+ [SerializeField] private string anim_SmashImpact = "Skill_Smash_Impact"; // 변수를 선언할거에요 -> 찍기 타격 애니 이름을
+ [SerializeField] private string anim_Sweep = "Skill_Sweep"; // 변수를 선언할거에요 -> 휩쓸기 애니 이름을
+
+ [Header("--- 🔍 디버그 (읽기 전용) ---")] // 인스펙터 제목을 달거에요 -> 디버그 정보를
+ [SerializeField] private string debugState = "대기 중"; // 변수를 선언할거에요 -> 현재 상태를 보여줄 텍스트를
+
+ private float _timer; // 변수를 선언할거에요 -> 패턴 쿨타임용 타이머를
+ private Rigidbody rb; // 변수를 선언할거에요 -> 보스의 물리 몸체를
+ private Rigidbody ballRb; // 변수를 선언할거에요 -> 쇠공의 물리 몸체를
+
+ private bool isBattleStarted = false; // 변수를 설정할거에요 -> 전투 시작 여부를 (기본 거짓)
+ private bool isWeaponless = false; // 변수를 설정할거에요 -> 무기 분실 여부를 (기본 거짓)
+ private bool isPerformingAction = false; // 변수를 설정할거에요 -> 행동 수행 중 여부를 (기본 거짓)
+ private Transform target; // 변수를 선언할거에요 -> 공격 대상(플레이어)을
+
+ private readonly Dictionary _clipLengthCache = new Dictionary(); // 딕셔너리를 선언할거에요 -> 애니메이션 길이를 저장할 캐시를
+
+ protected override void Awake() // 함수를 실행할거에요 -> 객체 생성 시 초기화를
+ { // 코드 블록을 시작할거에요 -> Awake 범위를
+ base.Awake(); // 부모의 Awake를 실행할거에요
+ rb = GetComponent(); // 가져올거에요 -> 내 몸의 Rigidbody를
+ if (ironBall != null) ballRb = ironBall.GetComponent(); // 조건이 맞으면 가져올거에요 -> 쇠공의 Rigidbody를
+ } // 코드 블록을 끝낼거에요 -> Awake를
+
+ private void Start() // 함수를 실행할거에요 -> 게임 시작 시 초기화를
+ { // 코드 블록을 시작할거에요 -> Start 범위를
+ CacheAllClipLengths(); // 함수를 실행할거에요 -> 애니메이션 길이를 미리 저장하는 기능을
+ StartBossBattle(); // 함수를 실행할거에요 -> 보스전을 시작하는 기능을
+ } // 코드 블록을 끝낼거에요 -> Start를
+
+ private void CacheAllClipLengths() // 함수를 정의할거에요 -> 애니메이션 길이를 캐싱하는 로직을
+ { // 코드 블록을 시작할거에요 -> CacheAllClipLengths 범위를
+ _clipLengthCache.Clear(); // 비울거에요 -> 기존 캐시 데이터를
+
+ if (animator == null || animator.runtimeAnimatorController == null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 없다면
+ { // 코드 블록을 시작할거에요 -> 에러 처리 범위를
+ Debug.LogError("❌ [Boss] Animator 또는 RuntimeAnimatorController가 없습니다! 클립 길이를 캐싱할 수 없습니다."); // 에러를 출력할거에요
+ return; // 중단할거에요
+ } // 코드 블록을 끝낼거에요 -> 에러 처리 끝
+
+ AnimationClip[] clips = animator.runtimeAnimatorController.animationClips; // 배열을 가져올거에요 -> 모든 애니메이션 클립을
+
+ foreach (AnimationClip clip in clips) // 반복할거에요 -> 모든 클립을 돌면서
+ { // 코드 블록을 시작할거에요 -> foreach 범위를
+
+ if (!_clipLengthCache.ContainsKey(clip.name)) // 조건이 맞으면 실행할거에요 -> 이름이 캐시에 없다면
+ { // 코드 블록을 시작할거에요 -> 캐시 저장 범위를
+ _clipLengthCache[clip.name] = clip.length; // 저장할거에요 -> 원본 이름을 키로 길이를
+ } // 코드 블록을 끝낼거에요 -> 캐시 저장 끝
+
+ int pipeIndex = clip.name.IndexOf('|'); // 위치를 찾을거에요 -> "|" 기호의 위치를
+
+ if (pipeIndex >= 0 && pipeIndex < clip.name.Length - 1) // 조건이 맞으면 실행할거에요 -> "|" 기호가 존재하면
+ { // 코드 블록을 시작할거에요 -> 단축 이름 처리 범위를
+ string shortName = clip.name.Substring(pipeIndex + 1); // 추출할거에요 -> "|" 뒤의 짧은 이름을
+
+ if (!_clipLengthCache.ContainsKey(shortName)) // 조건이 맞으면 실행할거에요 -> 짧은 이름이 캐시에 없다면
+ { // 코드 블록을 시작할거에요 -> 단축 이름 캐시 저장 범위를
+ _clipLengthCache[shortName] = clip.length; // 저장할거에요 -> 짧은 이름도 키로 등록
+ } // 코드 블록을 끝낼거에요 -> 단축 이름 저장 끝
+ } // 코드 블록을 끝낼거에요 -> 단축 이름 처리 끝
+
+ Debug.Log($"🎬 [Boss] 클립 캐싱: \"{clip.name}\" = {clip.length:F2}초"); // 로그를 찍을거에요 -> 캐싱 정보를
+ } // 코드 블록을 끝낼거에요 -> foreach 끝
+
+ Debug.Log($"✅ [Boss] 총 {_clipLengthCache.Count}개 키 캐싱 완료! (원본 + 단축이름 포함)"); // 로그를 찍을거에요 -> 완료 정보를
+ } // 코드 블록을 끝낼거에요 -> CacheAllClipLengths를
+
+ private float GetClipLength(string clipName, float fallback = 1.0f) // 함수를 정의할거에요 -> 이름으로 길이를 찾는 기능을
+ { // 코드 블록을 시작할거에요 -> GetClipLength 범위를
+ if (_clipLengthCache.TryGetValue(clipName, out float length)) // 조건이 맞으면 실행할거에요 -> 캐시에서 바로 찾았다면
+ { // 코드 블록을 시작할거에요 -> 즉시 반환 범위를
+ return length; // 반환할거에요 -> 찾은 길이를
+ } // 코드 블록을 끝낼거에요 -> 즉시 반환 끝
+
+ foreach (KeyValuePair kvp in _clipLengthCache) // 반복할거에요 -> 캐시 전체를 뒤져서
+ { // 코드 블록을 시작할거에요 -> 부분 매칭 범위를
+ if (kvp.Key.EndsWith(clipName)) // 조건이 맞으면 실행할거에요 -> 이름 끝부분이 일치한다면
+ { // 코드 블록을 시작할거에요 -> 매칭 성공 범위를
+ _clipLengthCache[clipName] = kvp.Value; // 저장할거에요 -> 다음 검색을 위해 짧은 이름도 캐싱
+ Debug.Log($"🔗 [Boss] 부분 매칭 성공: \"{clipName}\" → \"{kvp.Key}\" ({kvp.Value:F2}초)"); // 로그를 찍을거에요
+ return kvp.Value; // 반환할거에요 -> 매칭된 길이를
+ } // 코드 블록을 끝낼거에요 -> 매칭 성공 끝
+ } // 코드 블록을 끝낼거에요 -> 부분 매칭 끝
+
+ Debug.LogWarning($"⚠️ [Boss] 클립 \"{clipName}\"을 찾지 못했습니다! fallback={fallback:F2}초 사용"); // 경고를 찍을거에요 -> 못 찾았을 때
+ return fallback; // 반환할거에요 -> 기본값을
+ } // 코드 블록을 끝낼거에요 -> GetClipLength를
+
+ private IEnumerator PlayAndWait(string animName, float fallback = 1.0f) // 코루틴을 정의할거에요 -> 애니 재생 후 길이만큼 대기하는 기능을
+ { // 코드 블록을 시작할거에요 -> PlayAndWait 범위를
+ if (animator == null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 없다면
+ { // 코드 블록을 시작할거에요 -> 예외 처리 범위를
+ Debug.LogWarning($"⚠️ [Boss] Animator 없음! \"{animName}\" fallback={fallback}초 대기"); // 경고를 찍을거에요
+ yield return new WaitForSeconds(Mathf.Max(0.1f, fallback)); // 대기할거에요 -> 최소 0.1초 또는 기본값만큼
+ yield break; // 종료할거에요
+ } // 코드 블록을 끝낼거에요 -> 예외 처리 끝
+
+ int stateHash = Animator.StringToHash(animName); // 계산할거에요 -> 상태 이름의 해시값을
+ bool stateExists = animator.HasState(0, stateHash); // 확인할거에요 -> 상태가 실제로 존재하는지
+
+ if (!stateExists) // 조건이 맞으면 실행할거에요 -> 상태가 존재하지 않는다면
+ { // 코드 블록을 시작할거에요 -> 에러 처리 범위를
+ Debug.LogError($"❌ [Boss] Animator에 State \"{animName}\"이 없습니다! Animator Controller를 확인하세요."); // 에러를 찍을거에요
+ yield return new WaitForSeconds(Mathf.Max(0.1f, fallback)); // 대기할거에요 -> 기본값만큼
+ yield break; // 종료할거에요
+ } // 코드 블록을 끝낼거에요 -> 에러 처리 끝
+
+ animator.Play(animName, 0, 0f); // 재생할거에요 -> 해당 애니메이션을 처음부터
+ yield return null; // 대기할거에요 -> 1프레임 동안 (상태 갱신)
+
+ float duration = GetClipLength(animName, -1f); // 가져올거에요 -> 캐싱된 길이를
+
+ if (duration < 0f) // 조건이 맞으면 실행할거에요 -> 캐시에 없는 경우
+ { // 코드 블록을 시작할거에요 -> State 길이로 보정 범위를
+ AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0); // 가져올거에요 -> 현재 상태 정보를
+ duration = stateInfo.length; // 가져올거에요 -> 상태의 길이를
+
+ if (duration > 0f) // 조건이 맞으면 실행할거에요 -> 유효한 길이면
+ { // 코드 블록을 시작할거에요 -> 캐싱 범위를
+ _clipLengthCache[animName] = duration; // 저장할거에요 -> 캐시에 등록
+ Debug.Log($"🎬 [Boss] State에서 길이 확인 & 캐싱: \"{animName}\" = {duration:F2}초"); // 로그를 찍을거에요
+ } // 코드 블록을 끝낼거에요 -> 캐싱 끝
+ else // 조건이 틀리면 실행할거에요 -> 길이가 이상하면
+ { // 코드 블록을 시작할거에요 -> fallback 적용 범위를
+ duration = Mathf.Max(0.1f, fallback); // 설정할거에요 -> 기본값으로 보장
+ Debug.LogWarning($"⚠️ [Boss] State \"{animName}\" 길이 비정상({stateInfo.length}초)! fallback={duration}초 사용"); // 경고를 찍을거에요
+ } // 코드 블록을 끝낼거에요 -> fallback 적용 끝
+ } // 코드 블록을 끝낼거에요 -> State 길이 보정 끝
+
+ float remaining = duration - Time.deltaTime; // 계산할거에요 -> 1프레임을 뺀 남은 시간을
+
+ if (remaining > 0f) // 조건이 맞으면 실행할거에요 -> 남은 시간이 양수면
+ { // 코드 블록을 시작할거에요 -> 대기 범위를
+ yield return new WaitForSeconds(remaining); // 기다릴거에요 -> 남은 시간만큼
+ } // 코드 블록을 끝낼거에요 -> 대기 끝
+ } // 코드 블록을 끝낼거에요 -> PlayAndWait를
+
+ protected override void Init() // 함수를 정의할거에요 -> 초기화 설정을
+ { // 코드 블록을 시작할거에요 -> Init 범위를
+ base.Init(); // 부모의 초기화를 호출할거에요
+ _timer = 0f; // 초기화할거에요 -> 타이머를 0으로
+ isBattleStarted = false; // 설정할거에요 -> 전투 시작 전 상태로
+ isWeaponless = false; // 설정할거에요 -> 무기 소지 상태로
+ isPerformingAction = false; // 설정할거에요 -> 행동 전 상태로
+ IsAggroed = true; // 설정할거에요 -> 어그로 켠 상태로
+ mobRenderer = null; // 초기화할거에요 -> 렌더러 참조를
+ optimizationDistance = 9999f; // 설정할거에요 -> 최적화 거리를 무한대로
+
+ GameObject playerObj = GameObject.FindWithTag("Player"); // 찾을거에요 -> 플레이어 태그 오브젝트를
+
+ if (playerObj != null) // 조건이 맞으면 실행할거에요 -> 플레이어를 찾았다면
+ { // 코드 블록을 시작할거에요 -> 태그 탐색 성공 범위를
+ target = playerObj.transform; // 설정할거에요 -> 타겟 위치를
+ playerTransform = playerObj.transform; // 설정할거에요 -> 부모의 타겟 위치도
+ } // 코드 블록을 끝낼거에요 -> 태그 탐색 성공 끝
+ else // 조건이 틀리면 실행할거에요 -> 태그로 못 찾았다면
+ { // 코드 블록을 시작할거에요 -> 타입 탐색 범위를
+ PlayerMovement playerScript = FindObjectOfType(); // 찾을거에요 -> 스크립트 타입으로 플레이어를
+
+ if (playerScript != null) // 조건이 맞으면 실행할거에요 -> 찾았다면
+ { // 코드 블록을 시작할거에요 -> 타입 탐색 성공 범위를
+ target = playerScript.transform; // 설정할거에요 -> 타겟 위치를
+ playerTransform = playerScript.transform; // 설정할거에요 -> 부모의 타겟 위치도
+ } // 코드 블록을 끝낼거에요 -> 타입 탐색 성공 끝
+ } // 코드 블록을 끝낼거에요 -> 타입 탐색 끝
+
+ if (target == null) // 조건이 맞으면 실행할거에요 -> 여전히 못 찾았다면
+ { // 코드 블록을 시작할거에요 -> 에러 처리 범위를
+ Debug.LogError("❌ [Boss] 플레이어를 찾지 못했습니다! Player 태그 또는 PlayerMovement 확인!"); // 에러를 찍을거에요
+ } // 코드 블록을 끝낼거에요 -> 에러 처리 끝
+
+ currentHP = maxHP; // 설정할거에요 -> 체력을 최대로
+ StartCoroutine(SafeInitNavMesh()); // 시작할거에요 -> 내비메시 안전 초기화를
+
+ if (bossHealthBar != null) bossHealthBar.SetActive(false); // 끌거에요 -> 체력바 UI를
+ if (ballRb != null) ballRb.isKinematic = true; // 설정할거에요 -> 쇠공 물리를 고정으로
+ if (animator != null) animator.Play(anim_Idle); // 재생할거에요 -> 대기 애니메이션을
+ } // 코드 블록을 끝낼거에요 -> Init을
+
+ private IEnumerator SafeInitNavMesh() // 코루틴을 정의할거에요 -> 내비메시 안전 초기화를
+ { // 코드 블록을 시작할거에요 -> SafeInitNavMesh 범위를
+ yield return null; // 1프레임 쉴거에요
+
+ if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면
+ { // 코드 블록을 시작할거에요 -> 에이전트 초기화 범위를
+ int retry = 0; // 초기화할거에요 -> 재시도 횟수를
+
+ while (!agent.isOnNavMesh && retry < 5) // 반복할거에요 -> 메시 위에 안 올라왔고 시도가 5번 미만이면
+ { // 코드 블록을 시작할거에요 -> 재시도 루프 범위를
+ retry++; // 늘릴거에요 -> 횟수를
+ yield return new WaitForSeconds(0.1f); // 기다릴거에요 -> 0.1초 동안
+ } // 코드 블록을 끝낼거에요 -> 재시도 루프 끝
+
+ if (agent.isOnNavMesh) agent.isStopped = true; // 조건이 맞으면 실행할거에요 -> 메시 위면 정지시킬거에요
+ agent.enabled = false; // 끌거에요 -> 에이전트를 우선
+ } // 코드 블록을 끝낼거에요 -> 에이전트 초기화 끝
+ } // 코드 블록을 끝낼거에요 -> SafeInitNavMesh를
+
+ protected override void Update() // 함수를 실행할거에요 -> 매 프레임 업데이트를
+ { // 코드 블록을 시작할거에요 -> Update 범위를
+ base.Update(); // 부모의 업데이트를 실행할거에요
+
+ if (!isDead) // 조건이 맞으면 실행할거에요 -> 살아있다면
+ { // 코드 블록을 시작할거에요 -> AI 실행 범위를
+ ExecuteAILogic(); // 실행할거에요 -> AI 로직을
+ } // 코드 블록을 끝낼거에요 -> AI 실행 끝
+ } // 코드 블록을 끝낼거에요 -> Update를
+
+ protected override void ExecuteAILogic() // 함수를 정의할거에요 -> AI 행동 로직을
+ { // 코드 블록을 시작할거에요 -> ExecuteAILogic 범위를
+ if (isPerformingAction || !isBattleStarted || target == null) return; // 중단할거에요 -> 행동 중이거나 시작 전이면
+
+ if (isWeaponless) // 조건이 맞으면 실행할거에요 -> 무기가 없다면
+ { // 코드 블록을 시작할거에요 -> 무기 회수 범위를
+ RetrieveWeaponLogic(); // 실행할거에요 -> 무기 회수 로직을
+ return; // 중단할거에요
+ } // 코드 블록을 끝낼거에요 -> 무기 회수 끝
+
+ if (isAttacking || isHit || isResting) return; // 중단할거에요 -> 공격/피격/휴식 중이면
+
+ if (_timer > 0f) // 조건이 맞으면 실행할거에요 -> 타이머가 남았다면
+ { // 코드 블록을 시작할거에요 -> 타이머 감소 범위를
+ _timer -= Time.deltaTime; // 줄일거에요 -> 시간을
+ } // 코드 블록을 끝낼거에요 -> 타이머 감소 끝
+
+ float distToPlayer = Vector3.Distance(transform.position, target.position); // 계산할거에요 -> 플레이어와의 거리를
+
+ if (agent == null || !agent.enabled || !agent.isOnNavMesh) return; // 중단할거에요 -> 에이전트 상태가 정상이 아니면
+
+ if (distToPlayer > attackRange) // 조건이 맞으면 실행할거에요 -> 사거리보다 멀다면
+ { // 코드 블록을 시작할거에요 -> 추격 범위를
+ agent.isStopped = false; // 켤거에요 -> 이동을
+ agent.SetDestination(target.position); // 이동할거에요 -> 플레이어 위치로
+ LookAtTarget(target.position); // 바라볼거에요 -> 플레이어를
+ UpdateMovementAnimation(); // 실행할거에요 -> 이동 애니메이션 갱신을
+ } // 코드 블록을 끝낼거에요 -> 추격 끝
+ else // 조건이 틀리면 실행할거에요 -> 사거리 안이라면
+ { // 코드 블록을 시작할거에요 -> 사거리 내 행동 범위를
+ agent.isStopped = true; // 멈출거에요 -> 이동을
+ agent.velocity = Vector3.zero; // 멈출거에요 -> 관성을 없앨거에요
+
+ LookAtTarget(target.position); // 바라볼거에요 -> 플레이어를
+
+ if (animator != null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면
+ { // 코드 블록을 시작할거에요 -> 애니 상태 체크 범위를
+ AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); // 가져올거에요 -> 현재 상태를
+
+ if (state.IsName(anim_Walk)) // 조건이 맞으면 실행할거에요 -> 걷는 중이라면
+ { // 코드 블록을 시작할거에요 -> 대기로 전환 범위를
+ animator.Play(anim_Idle); // 재생할거에요 -> 대기 애니를
+ } // 코드 블록을 끝낼거에요 -> 대기로 전환 끝
+ } // 코드 블록을 끝낼거에요 -> 애니 상태 체크 끝
+
+ if (_timer <= 0f) // 조건이 맞으면 실행할거에요 -> 공격 쿨타임이 끝났다면
+ { // 코드 블록을 시작할거에요 -> 공격 시작 범위를
+ _timer = patternInterval; // 설정할거에요 -> 타이머를 다시
+ Debug.Log($"⏰ [Boss] 사거리 내 공격! 거리={distToPlayer:F1}m"); // 로그를 찍을거에요
+ DecideAttack(); // 실행할거에요 -> 공격 결정 기능을
+ } // 코드 블록을 끝낼거에요 -> 공격 시작 끝
+ } // 코드 블록을 끝낼거에요 -> 사거리 내 행동 끝
+ } // 코드 블록을 끝낼거에요 -> ExecuteAILogic를
+
+ public void StartBossBattle() // 함수를 정의할거에요 -> 전투 시작 기능을
+ { // 코드 블록을 시작할거에요 -> StartBossBattle 범위를
+ if (isBattleStarted) return; // 중단할거에요 -> 이미 시작했다면
+ StartCoroutine(BattleStartRoutine()); // 시작할거에요 -> 시작 연출 코루틴을
+ } // 코드 블록을 끝낼거에요 -> StartBossBattle를
+
+ private IEnumerator BattleStartRoutine() // 코루틴을 정의할거에요 -> 전투 시작 연출을
+ { // 코드 블록을 시작할거에요 -> BattleStartRoutine 범위를
+ Debug.Log("🔥 [Boss] Roar 시작!"); // 로그를 찍을거에요
+
+ isPerformingAction = true; // 설정할거에요 -> 행동 수행 중으로
+ if (bossHealthBar != null) bossHealthBar.SetActive(true); // 켤거에요 -> 체력바를
+ if (counterSystem != null) counterSystem.InitializeBattle(); // 실행할거에요 -> 카운터 시스템 초기화를
+
+ yield return StartCoroutine(PlayAndWait(anim_Roar, 2.0f)); // 대기할거에요 -> 포효 애니가 끝날 때까지
+
+ if (animator != null) animator.Play(anim_Idle, 0, 0f); // 재생할거에요 -> 대기 애니를
+
+ isPerformingAction = false; // 해제할거에요 -> 행동 상태를
+ isBattleStarted = true; // 설정할거에요 -> 전투 시작 상태로
+
+ Debug.Log("⚔️ [Boss] Roar 종료 → 전투 루프 시작!"); // 로그를 찍을거에요
+
+ if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면
+ { // 코드 블록을 시작할거에요 -> NavMesh 연결 범위를
+ agent.enabled = true; // 켤거에요 -> 에이전트를
+
+ int retry = 0; // 초기화할거에요 -> 재시도 횟수를
+
+ while (!agent.isOnNavMesh && retry < 10) // 반복할거에요 -> 메시 위에 없고 시도가 10번 미만이면
+ { // 코드 블록을 시작할거에요 -> 재시도 루프 범위를
+ retry++; // 늘릴거에요 -> 횟수를
+ yield return new WaitForSeconds(0.1f); // 기다릴거에요 -> 0.1초 동안
+ } // 코드 블록을 끝낼거에요 -> 재시도 루프 끝
+
+ if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 메시 위라면
+ { // 코드 블록을 시작할거에요 -> 성공 처리 범위를
+ agent.isStopped = false; // 켤거에요 -> 이동을
+ Debug.Log("✅ [Boss] NavMesh 연결 완료, 추격 시작"); // 로그를 찍을거에요
+ } // 코드 블록을 끝낼거에요 -> 성공 처리 끝
+ else // 조건이 틀리면 실행할거에요 -> 연결 실패하면
+ { // 코드 블록을 시작할거에요 -> 실패 처리 범위를
+ Debug.LogError("❌ [Boss] NavMesh 연결 실패! 보스 위치를 확인하세요!"); // 에러를 찍을거에요
+ } // 코드 블록을 끝낼거에요 -> 실패 처리 끝
+ } // 코드 블록을 끝낼거에요 -> NavMesh 연결 끝
+ } // 코드 블록을 끝낼거에요 -> BattleStartRoutine를
+
+ private void UpdateMovementAnimation() // 함수를 정의할거에요 -> 이동 애니메이션 갱신을
+ { // 코드 블록을 시작할거에요 -> UpdateMovementAnimation 범위를
+ if (animator == null || isPerformingAction || isAttacking || isHit || isDead) return; // 중단할거에요 -> 행동 불가 상태면
+
+ AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); // 가져올거에요 -> 상태 정보를
+
+ if (agent != null && agent.velocity.magnitude > 0.1f) // 조건이 맞으면 실행할거에요 -> 움직이고 있다면
+ { // 코드 블록을 시작할거에요 -> 걷기 애니 처리 범위를
+ if (!state.IsName(anim_Walk)) animator.Play(anim_Walk); // 조건이 맞으면 재생할거에요 -> 걷기 애니를
+ } // 코드 블록을 끝낼거에요 -> 걷기 애니 처리 끝
+ else // 조건이 틀리면 실행할거에요 -> 멈춰 있다면
+ { // 코드 블록을 시작할거에요 -> 대기 애니 처리 범위를
+ if (!state.IsName(anim_Idle) && !state.IsName(anim_Roar)) animator.Play(anim_Idle); // 조건이 맞으면 재생할거에요 -> 대기 애니를
+ } // 코드 블록을 끝낼거에요 -> 대기 애니 처리 끝
+ } // 코드 블록을 끝낼거에요 -> UpdateMovementAnimation를
+
+ private void LookAtTarget(Vector3 targetPos) // 함수를 정의할거에요 -> 타겟 바라보기를
+ { // 코드 블록을 시작할거에요 -> LookAtTarget 범위를
+ Vector3 dir = targetPos - transform.position; // 계산할거에요 -> 방향 벡터를
+ dir.y = 0f; // 무시할거에요 -> 높이 차이는
+
+ if (dir != Vector3.zero) // 조건이 맞으면 실행할거에요 -> 방향이 있다면
+ { // 코드 블록을 시작할거에요 -> 회전 처리 범위를
+ transform.rotation = Quaternion.Slerp( // 회전할거에요 -> 부드럽게
+ transform.rotation, // 현재 회전에서
+ Quaternion.LookRotation(dir), // 목표 회전으로
+ Time.deltaTime * 5f // 시간 속도에 맞춰서
+ ); // 회전 계산을 끝낼거에요
+ } // 코드 블록을 끝낼거에요 -> 회전 처리 끝
+ } // 코드 블록을 끝낼거에요 -> LookAtTarget를
+
+ private void DecideAttack() // 함수를 정의할거에요 -> 공격 패턴 결정을
+ { // 코드 블록을 시작할거에요 -> DecideAttack 범위를
+ if (target != null) LookAtTarget(target.position); // 조건이 맞으면 바라볼거에요 -> 타겟이 있다면
+
+ string patternName = (counterSystem != null) ? counterSystem.SelectBossPattern() : "Normal"; // 선택할거에요 -> 패턴 이름을
+ Debug.Log($"🧠 [Boss] 패턴 선택: {patternName}"); // 로그를 찍을거에요
+
+ switch (patternName) // 분기할거에요 -> 패턴에 따라
+ { // 코드 블록을 시작할거에요 -> switch 범위를
+ case "DashAttack": StartCoroutine(Pattern_DashAttack()); break; // 실행할거에요 -> 돌진 공격을
+ case "Smash": StartCoroutine(Pattern_SmashAttack()); break; // 실행할거에요 -> 찍기 공격을
+ case "ShieldWall": StartCoroutine(Pattern_ShieldWall()); break; // 실행할거에요 -> 휩쓸기 공격을
+ default: StartCoroutine(Pattern_ThrowBall()); break; // 실행할거에요 -> 공 던지기 공격을
+ } // 코드 블록을 끝낼거에요 -> switch 끝
+ } // 코드 블록을 끝낼거에요 -> DecideAttack를
+
+ private void RetrieveWeaponLogic() // 함수를 정의할거에요 -> 무기 회수 로직을
+ { // 코드 블록을 시작할거에요 -> RetrieveWeaponLogic 범위를
+ if (ironBall == null || !agent.enabled || !agent.isOnNavMesh) return; // 중단할거에요 -> 무기나 메시 상태가 안 좋으면
+
+ agent.SetDestination(ironBall.transform.position); // 이동할거에요 -> 무기 위치로
+ agent.isStopped = false; // 켤거에요 -> 이동을
+
+ UpdateMovementAnimation(); // 실행할거에요 -> 애니메이션 갱신을
+
+ if (Vector3.Distance(transform.position, ironBall.transform.position) <= 3.0f && !isAttacking) // 조건이 맞으면 실행할거에요 -> 가까워지면
+ { // 코드 블록을 시작할거에요 -> 줍기 코루틴 시작 범위를
+ StartCoroutine(PickUpBallRoutine()); // 시작할거에요 -> 줍기 코루틴을
+ } // 코드 블록을 끝낼거에요 -> 줍기 코루틴 시작 끝
+ } // 코드 블록을 끝낼거에요 -> RetrieveWeaponLogic를
+
+ private IEnumerator PickUpBallRoutine() // 코루틴을 정의할거에요 -> 무기 줍기 과정을
+ { // 코드 블록을 시작할거에요 -> PickUpBallRoutine 범위를
+ OnAttackStart(); // 실행할거에요 -> 공격 시작 설정을
+
+ if (agent != null && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 정상이라면
+ { // 코드 블록을 시작할거에요 -> 정지 처리 범위를
+ agent.isStopped = true; // 정지시킬거에요 -> 에이전트를
+ agent.velocity = Vector3.zero; // 멈출거에요 -> 속도를
+ } // 코드 블록을 끝낼거에요 -> 정지 처리 끝
+
+ yield return StartCoroutine(PlayAndWait(anim_Pickup, 1.5f)); // 대기할거에요 -> 줍기 애니 동안
+
+ ironBall.transform.SetParent(handHolder); // 설정할거에요 -> 쇠공의 부모를 손으로
+ ironBall.transform.localPosition = Vector3.zero; // 초기화할거에요 -> 상대 위치를
+ ironBall.transform.localRotation = Quaternion.identity; // 초기화할거에요 -> 상대 회전을
+
+ if (ballRb != null) // 조건이 맞으면 실행할거에요 -> 쇠공 리지드바디가 있다면
+ { // 코드 블록을 시작할거에요 -> 쇠공 고정 범위를
+ ballRb.isKinematic = true; // 설정할거에요 -> 쇠공 물리를 고정으로
+ ballRb.velocity = Vector3.zero; // 멈출거에요 -> 속도를
+ } // 코드 블록을 끝낼거에요 -> 쇠공 고정 끝
+
+ isWeaponless = false; // 설정할거에요 -> 무기 소지 상태로
+
+ OnAttackEnd(); // 실행할거에요 -> 공격 종료 설정을
+
+ if (agent != null && agent.isOnNavMesh) agent.isStopped = false; // 조건이 맞으면 실행할거에요 -> 이동을 다시 켤거에요
+ } // 코드 블록을 끝낼거에요 -> PickUpBallRoutine를
+
+ public override void OnAttackStart() // 함수를 정의할거에요 -> 공격 시작 시 공통 처리를
+ { // 코드 블록을 시작할거에요 -> OnAttackStart 범위를
+ isAttacking = true; // 설정할거에요 -> 공격 중 상태를
+ isPerformingAction = true; // 설정할거에요 -> 행동 수행 중 상태를
+ if (agent != null) agent.isStopped = true; // 멈출거에요 -> 에이전트를
+ } // 코드 블록을 끝낼거에요 -> OnAttackStart를
+
+ public override void OnAttackEnd() // 함수를 정의할거에요 -> 공격 종료 시 공통 처리를
+ { // 코드 블록을 시작할거에요 -> OnAttackEnd 범위를
+ StartCoroutine(RecoverRoutine()); // 시작할거에요 -> 회복 코루틴을
+ } // 코드 블록을 끝낼거에요 -> OnAttackEnd를
+
+ private IEnumerator RecoverRoutine() // 코루틴을 정의할거에요 -> 공격 후 휴식 과정을
+ { // 코드 블록을 시작할거에요 -> RecoverRoutine 범위를
+ isAttacking = false; // 해제할거에요 -> 공격 중 상태를
+ isResting = true; // 설정할거에요 -> 휴식 상태를
+
+ if (animator != null) animator.Play(anim_Idle); // 조건이 맞으면 재생할거에요 -> 대기 애니를
+
+ yield return new WaitForSeconds(patternInterval); // 기다릴거에요 -> 설정된 간격만큼
+
+ isResting = false; // 해제할거에요 -> 휴식 상태를
+ isPerformingAction = false; // 해제할거에요 -> 행동 상태를
+ } // 코드 블록을 끝낼거에요 -> RecoverRoutine를
+
+ private IEnumerator Pattern_DashAttack() // 코루틴을 정의할거에요 -> 돌진 공격 과정을
+ { // 코드 블록을 시작할거에요 -> Pattern_DashAttack 범위를
+ OnAttackStart(); // 설정할거에요 -> 공격 시작
+
+ if (agent != null) agent.enabled = false; // 끌거에요 -> 에이전트를
+
+ yield return StartCoroutine(PlayAndWait(anim_DashReady, 0.5f)); // 대기할거에요 -> 준비 애니 동안
+
+ if (rb != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면
+ { // 코드 블록을 시작할거에요 -> 돌진 물리 처리 범위를
+ rb.isKinematic = false; // 설정할거에요 -> 물리를 켤거에요
+ rb.velocity = transform.forward * 20f; // 날아갈거에요 -> 전방 속도로
+ } // 코드 블록을 끝낼거에요 -> 돌진 물리 처리 끝
+
+ yield return StartCoroutine(PlayAndWait(anim_DashGo, 1.0f)); // 대기할거에요 -> 돌진 애니 동안
+
+ if (rb != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면
+ { // 코드 블록을 시작할거에요 -> 정지 처리 범위를
+ rb.velocity = Vector3.zero; // 멈출거에요 -> 속도를
+ rb.isKinematic = true; // 설정할거에요 -> 물리를 다시 고정으로
+ } // 코드 블록을 끝낼거에요 -> 정지 처리 끝
+
+ if (agent != null && isBattleStarted) agent.enabled = true; // 조건이 맞으면 실행할거에요 -> 에이전트를 다시 켤거에요
+
+ OnAttackEnd(); // 설정할거에요 -> 공격 종료
+ } // 코드 블록을 끝낼거에요 -> Pattern_DashAttack를
+
+ private IEnumerator Pattern_SmashAttack() // 코루틴을 정의할거에요 -> 찍기 공격 과정을
+ { // 코드 블록을 시작할거에요 -> Pattern_SmashAttack 범위를
+ OnAttackStart(); // 설정할거에요 -> 공격 시작
+ yield return StartCoroutine(PlayAndWait(anim_SmashImpact, 1.0f)); // 대기할거에요 -> 타격 애니 동안
+ OnAttackEnd(); // 설정할거에요 -> 공격 종료
+ } // 코드 블록을 끝낼거에요 -> Pattern_SmashAttack를
+
+ private IEnumerator Pattern_ShieldWall() // 코루틴을 정의할거에요 -> 휩쓸기 공격 과정을
+ { // 코드 블록을 시작할거에요 -> Pattern_ShieldWall 범위를
+ OnAttackStart(); // 설정할거에요 -> 공격 시작
+ yield return StartCoroutine(PlayAndWait(anim_Sweep, 2.0f)); // 대기할거에요 -> 휩쓸기 애니 동안
+ OnAttackEnd(); // 설정할거에요 -> 공격 종료
+ } // 코드 블록을 끝낼거에요 -> Pattern_ShieldWall를
+
+ private IEnumerator Pattern_ThrowBall() // 코루틴을 정의할거에요 -> 공 던지기 과정을
+ { // 코드 블록을 시작할거에요 -> Pattern_ThrowBall 범위를
+ OnAttackStart(); // 설정할거에요 -> 공격 시작
+
+ if (animator != null) animator.Play(anim_Throw, 0, 0f); // 재생할거에요 -> 던지기 애니를
+ yield return null; // 1프레임 쉴거에요
+
+ float throwClipLength = GetClipLength(anim_Throw, 1.5f); // 가져올거에요 -> 클립 길이를
+ float releaseTime = throwClipLength * 0.4f; // 계산할거에요 -> 발사 시점을
+
+ yield return new WaitForSeconds(Mathf.Max(0f, releaseTime - Time.deltaTime)); // 기다릴거에요 -> 발사 시점까지
+
+ if (ironBall != null && ballRb != null) // 조건이 맞으면 실행할거에요 -> 쇠공을 던질거에요
+ { // 코드 블록을 시작할거에요 -> 쇠공 발사 범위를
+ ironBall.transform.SetParent(null); // 뺄거에요 -> 손에서
+ ballRb.isKinematic = false; // 켤거에요 -> 물리를
+
+ Vector3 dir = (target.position - transform.position).normalized; // 계산할거에요 -> 방향을
+ ballRb.AddForce(dir * 20f + Vector3.up * 5f, ForceMode.Impulse); // 던질거에요 -> 충격 힘으로
+ ballRb.angularDrag = 5f; // 설정할거에요 -> 회전 저항을
+ } // 코드 블록을 끝낼거에요 -> 쇠공 발사 끝
+
+ isWeaponless = true; // 설정할거에요 -> 무기 없음 상태를
+
+ float remainingTime = throwClipLength - releaseTime; // 계산할거에요 -> 남은 애니 시간을
+ if (remainingTime > 0f) yield return new WaitForSeconds(remainingTime); // 조건이 맞으면 기다릴거에요 -> 남은 시간만큼
+
+ OnAttackEnd(); // 설정할거에요 -> 공격 종료
+ } // 코드 블록을 끝낼거에요 -> Pattern_ThrowBall를
+
+ protected override void OnStartHit() // 함수를 정의할거에요 -> 피격 시 처리를
+ { // 코드 블록을 시작할거에요 -> OnStartHit 범위를
+ // 보스는 피격되어도 행동이 끊기지 않습니다 (슈퍼아머) // 설명을 적을거에요 -> 의도적으로 비워둠
+ } // 코드 블록을 끝낼거에요 -> OnStartHit를
+
+ protected override void OnDie() // 함수를 정의할거에요 -> 사망 시 처리를
+ { // 코드 블록을 시작할거에요 -> OnDie 범위를
+ isBattleStarted = false; // 설정할거에요 -> 전투 종료
+ isPerformingAction = false; // 해제할거에요 -> 행동 상태를
+ if (bossHealthBar != null) bossHealthBar.SetActive(false); // 끌거에요 -> 체력바를
+ GiveBossXP(); // 실행할거에요 -> 경험치 지급을
+ base.OnDie(); // 부모의 사망 처리를 호출할거에요
+ } // 코드 블록을 끝낼거에요 -> OnDie를
+
+ private void GiveBossXP() // 함수를 정의할거에요 -> 경험치 지급 기능을
+ { // 코드 블록을 시작할거에요 -> GiveBossXP 범위를
+ Debug.Log("[BossController] 보스 처치 시도 중..."); // 로그를 찍을거에요 -> 지급 시도
+
+ if (ObsessionSystem.instance != null) // 조건이 맞으면 실행할거에요 -> 시스템 인스턴스가 있다면
+ { // 코드 블록을 시작할거에요 -> 지급 범위를
+ ObsessionSystem.instance.AddRunXP(100); // 더할거에요 -> 경험치를 100만큼
+ } // 코드 블록을 끝낼거에요 -> 지급 끝
+ else // 조건이 틀리면 실행할거에요 -> 인스턴스가 없다면
+ { // 코드 블록을 시작할거에요 -> 에러 처리 범위를
+ Debug.LogError("[BossController] ObsessionSystem 인스턴스를 찾을 수 없습니다!"); // 에러를 찍을거에요
+ } // 코드 블록을 끝낼거에요 -> 에러 처리 끝
+
+ Debug.Log("[BossController] 보스 처치 처리 완료!"); // 로그를 찍을거에요 -> 빨간색으로
+ } // 코드 블록을 끝낼거에요 -> GiveBossXP를
+
+} // 코드 블록을 끝낼거에요 -> NorcielBoss를
\ No newline at end of file
diff --git a/Assets/Scripts/Enemy/BossAI/BossWeapon.cs b/Assets/Scripts/Enemy/BossAI/BossWeapon.cs
new file mode 100644
index 00000000..0fc0a990
--- /dev/null
+++ b/Assets/Scripts/Enemy/BossAI/BossWeapon.cs
@@ -0,0 +1,38 @@
+using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
+
+public class BossWeapon : MonoBehaviour // 클래스를 선언할거에요 -> 보스 무기를
+{
+ [Header("설정")] // 인스펙터 제목을 달거에요 -> 설정을
+ [SerializeField] private float damage = 20f; // 변수를 선언할거에요 -> 데미지를
+ [SerializeField] private string targetTag = "Player"; // 변수를 선언할거에요 -> 타겟 태그를
+
+ private Rigidbody rb; // 변수를 선언할거에요 -> 리지드바디를
+ private Collider col; // 변수를 선언할거에요 -> 콜라이더를
+ private bool isThrown; // 변수를 선언할거에요 -> 던져짐 여부를
+
+ private void Awake() // 함수를 실행할거에요 -> 초기화를
+ {
+ rb = GetComponent(); // 가져올거에요 -> 리지드바디를
+ col = GetComponent(); // 가져올거에요 -> 콜라이더를
+ }
+
+ public void ThrowBall() { isThrown = true; if (col) col.isTrigger = true; } // 함수를 선언할거에요 -> 던짐 처리를 (통과)
+ public void PickUpBall() { isThrown = false; if (col) col.isTrigger = true; } // 함수를 선언할거에요 -> 줍기 처리를 (통과)
+
+ private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 트리거 충돌 시
+ {
+ if (isThrown && other.CompareTag(targetTag)) ApplyDamage(other.gameObject); // 조건이 맞으면 실행할거에요 -> 플레이어 피격을
+ else if (isThrown && other.CompareTag("Ground")) if (col) col.isTrigger = false; // 조건이 맞으면 실행할거에요 -> 바닥이면 물리로 전환을
+ }
+
+ private void OnCollisionEnter(Collision col) // 함수를 실행할거에요 -> 물리 충돌 시
+ {
+ if (isThrown && col.gameObject.CompareTag(targetTag) && rb.velocity.magnitude > 2f) ApplyDamage(col.gameObject); // 조건이 맞으면 실행할거에요 -> 굴러서 부딪히면 데미지를
+ }
+
+ private void ApplyDamage(GameObject target) // 함수를 선언할거에요 -> 데미지 적용을
+ {
+ var hp = target.GetComponent(); // 가져올거에요 -> 체력 스크립트를
+ if (hp != null) hp.TakeDamage(damage); // 실행할거에요 -> 데미지 함수를
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Enemy/BossAI/BossWeapon.cs.meta b/Assets/Scripts/Enemy/BossAI/BossWeapon.cs.meta
new file mode 100644
index 00000000..0b0e52f8
--- /dev/null
+++ b/Assets/Scripts/Enemy/BossAI/BossWeapon.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8aab89a77c422e841ab70b85a3a041eb
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Enemy/BossAI/BossZoneTrigger.cs b/Assets/Scripts/Enemy/BossAI/BossZoneTrigger.cs
index 1c5ee9ea..952f8d06 100644
--- a/Assets/Scripts/Enemy/BossAI/BossZoneTrigger.cs
+++ b/Assets/Scripts/Enemy/BossAI/BossZoneTrigger.cs
@@ -1,29 +1,27 @@
-using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
+using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
-public class BossZoneTrigger : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 BossZoneTrigger를
+public class BossZoneTrigger : MonoBehaviour // 클래스를 선언할거에요 -> 보스방 입장 트리거를
{
- [SerializeField] private NorcielBoss boss; // 변수를 선언할거에요 -> 깨울 보스 스크립트(NorcielBoss)를 boss에
- [SerializeField] private GameObject fogWall; // 변수를 선언할거에요 -> 입구를 막을 안개벽 오브젝트를 fogWall에
+ [SerializeField] private NorcielBoss boss; // 변수를 선언할거에요 -> 보스 참조를
+ [SerializeField] private GameObject entranceWall; // 변수를 선언할거에요 -> 입구 막는 벽을
- private bool hasTriggered = false; // 변수를 초기화할거에요 -> 이미 발동했는지 여부를 거짓(false)으로
+ private bool isTriggered = false; // 변수를 선언할거에요 -> 이미 발동했는지를
- private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 무언가가 트리거에 닿았을 때 OnTriggerEnter를
+ private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 충돌 감지를
{
- // 이미 발동했거나, 플레이어가 아니면 무시
- if (hasTriggered) return; // 조건이 맞으면 중단할거에요 -> 이미 발동했다면(hasTriggered가 참이면)
+ if (isTriggered) return; // 중단할거에요 -> 이미 발동했으면
- if (other.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 닿은 물체의 태그가 "Player"라면
+ if (other.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 플레이어가 들어오면
{
- hasTriggered = true; // 상태를 바꿀거에요 -> 발동 상태를 참(true)으로
+ isTriggered = true; // 설정할거에요 -> 발동됨으로
+ if (entranceWall != null) entranceWall.SetActive(true); // 켤거에요 -> 벽을 (못 나가게)
- // 1. 보스 깨우기
- if (boss != null) boss.StartBossBattle(); // 조건이 맞으면 실행할거에요 -> 보스가 연결되어 있다면 전투 시작 함수(StartBossBattle)를
+ if (boss != null) // 조건이 맞으면 실행할거에요 -> 보스가 연결돼있으면
+ {
+ boss.StartBossBattle(); // 실행할거에요 -> 보스전 시작을
+ }
- // 2. 도망 못 가게 입구 막기 (선택)
- if (fogWall != null) fogWall.SetActive(true); // 조건이 맞으면 실행할거에요 -> 안개벽이 연결되어 있다면 활성화(true)를
-
- // 3. 이 트리거는 할 일 다 했으니 끄기
- // gameObject.SetActive(false); // 바로 끄면 안개도 꺼질 수 있으니 주의
+ Debug.Log("⚔️ 보스전 시작! 퇴로는 없다."); // 로그를 찍을거에요 -> 시작 알림을
}
}
}
\ No newline at end of file
diff --git a/Assets/Scripts/Enemy/BossAI/CounterType.cs b/Assets/Scripts/Enemy/BossAI/CounterType.cs
index 5f60e7e9..e9df110e 100644
--- a/Assets/Scripts/Enemy/BossAI/CounterType.cs
+++ b/Assets/Scripts/Enemy/BossAI/CounterType.cs
@@ -4,7 +4,7 @@
public enum CounterType // 열거형을 선언할거에요 -> CounterType이라는 이름의 enum을
{ // 코드 블록을 시작할거에요 -> CounterType 범위를
None, // 값을 정의할거에요 -> 카운터 없음 상태를
- Dodge, // 값을 정의할거에요 -> 회피 남발 카운터 타입을
- Aim, // 값을 정의할거에요 -> 정조준 과다 카운터 타입을
- Pierce // 값을 정의할거에요 -> 관통 화살 편중 카운터 타입을
+ KeepDistance, // 값을 정의할거에요 -> 플레이어가 멀리 있을 때 (N m 이상)를
+ CloseRange, // 값을 정의할거에요 -> 플레이어가 근접해 있을 때 (N m 이하)를
+ FrequentDash // 값을 정의할거에요 -> 플레이어가 대쉬를 많이 사용할 때를
} // 코드 블록을 끝낼거에요 -> CounterType을
\ No newline at end of file
diff --git a/Assets/Scripts/Enemy/BossAI/PatternSelector.cs b/Assets/Scripts/Enemy/BossAI/PatternSelector.cs
new file mode 100644
index 00000000..7ea0d813
--- /dev/null
+++ b/Assets/Scripts/Enemy/BossAI/PatternSelector.cs
@@ -0,0 +1,55 @@
+using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
+using System.Collections.Generic; // 딕셔너리를 사용할거에요 -> System.Collections.Generic을
+
+public class PatternSelector // 클래스를 선언할거에요 -> 패턴 선택 로직을
+{
+ private readonly BossCounterConfig _config; // 변수를 선언할거에요 -> 설정 참조를
+
+ public PatternSelector(BossCounterConfig config) // 생성자를 만들거에요 -> 설정을 주입받는
+ {
+ _config = config; // 값을 저장할거에요 -> 내부 변수에
+ }
+
+ public string SelectPattern(Dictionary counters) // 함수를 선언할거에요 -> 패턴 선택을
+ {
+ if (!_config) return "Normal"; // 반환할거에요 -> 설정 없으면 기본값을
+
+ float totalWeight = 0f; // 변수를 초기화할거에요 -> 총 가중치를
+ var weights = new Dictionary(); // 딕셔너리를 만들거에요 -> 패턴별 확률을
+
+ foreach (var p in _config.patterns) // 반복할거에요 -> 모든 패턴 설정을
+ {
+ float w = p.baseWeight; // 값을 가져올거에요 -> 기본 가중치를
+
+ // 카운터가 있으면 가중치 증가
+ if (counters.TryGetValue(p.targetCounter, out int count))
+ w += count * p.weightMultiplier;
+
+ // ⭐ [핵심 수정] 중복 키 에러 방지 및 가중치 합산
+ if (weights.ContainsKey(p.patternName)) // 조건이 맞으면 실행할거에요 -> 이미 같은 패턴 이름이 있다면
+ {
+ weights[p.patternName] += w; // 더할거에요 -> 기존 확률에 합산 (조건 A or 조건 B)
+ }
+ else // 조건이 틀리면 실행할거에요 -> 처음 나온 패턴이라면
+ {
+ weights[p.patternName] = w; // 저장할거에요 -> 새 항목으로
+ }
+
+ totalWeight += w; // 더할거에요 -> 총합에 (이 부분 주의: 중복 합산 시 totalWeight 계산 로직 수정 필요)
+ }
+
+ // totalWeight 재계산 (중복 합산된 최종 값으로)
+ totalWeight = 0f;
+ foreach (var val in weights.Values) totalWeight += val;
+
+ float rnd = Random.Range(0, totalWeight); // 값을 뽑을거에요 -> 랜덤으로
+ float sum = 0; // 변수를 초기화할거에요 -> 누적합을
+
+ foreach (var pair in weights) // 반복할거에요 -> 각 패턴을
+ {
+ sum += pair.Value; // 더할거에요 -> 확률을
+ if (rnd <= sum) return pair.Key; // 반환할거에요 -> 당첨된 패턴을
+ }
+ return "Normal"; // 반환할거에요 -> 꽝이면 기본값을
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Enemy/BossAI/PatternSelector.cs.meta b/Assets/Scripts/Enemy/BossAI/PatternSelector.cs.meta
new file mode 100644
index 00000000..0d272585
--- /dev/null
+++ b/Assets/Scripts/Enemy/BossAI/PatternSelector.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5f836bfcc212dd843a4c9ae7b2353d0d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Enemy/Spawner/Monster Spwaner.cs b/Assets/Scripts/Enemy/Spawner/Monster Spwaner.cs
index 917e48a8..09ce09ba 100644
--- a/Assets/Scripts/Enemy/Spawner/Monster Spwaner.cs
+++ b/Assets/Scripts/Enemy/Spawner/Monster Spwaner.cs
@@ -1,163 +1,67 @@
-using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
+using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
+using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를
+using System.Collections.Generic; // 리스트를 사용할거에요 -> System.Collections.Generic을
-///
-/// 하이브리드 몬스터 스포너 (생성은 스포너 기준, 생존은 몬스터 기준)
-/// - Birth: 스포너 기준 거리로 생성
-/// - Life & Hibernate: 몬스터 기준 거리로 최적화
-/// - Wake Up: 범위 안으로 복귀 시 재활성화
-///
-public class MonsterSpawner : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 MonsterSpawner를
+public class MonsterSpwaner : MonoBehaviour // 클래스를 선언할거에요 -> 몬스터 스포너를
{
- [Header("=== 생성 설정 (Birth - 스포너 기준) ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 생성 설정 (Birth - 스포너 기준) === 을
- [Tooltip("스포너로부터 이 거리 안에 플레이어가 들어오면 몬스터 생성")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- [SerializeField] private float spawnRange = 25f; // 변수를 선언할거에요 -> 몬스터 생성 거리(25.0)를 spawnRange에
+ [Header("스폰 설정")] // 인스펙터 제목을 달거에요 -> 스폰 설정을
+ [SerializeField] private List monsterPrefabs; // 리스트를 선언할거에요 -> 몬스터 프리팹들을
+ [SerializeField] private List spawnPoints; // 리스트를 선언할거에요 -> 스폰 위치들을
+ [SerializeField] private float spawnInterval = 5f; // 변수를 선언할거에요 -> 스폰 간격을
+ [SerializeField] private int maxMonsters = 20; // 변수를 선언할거에요 -> 최대 몬스터 수를
- [Tooltip("몬스터가 죽은 후 재생성까지 대기 시간")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- [SerializeField] private float respawnCooldown = 3f; // 변수를 선언할거에요 -> 재소환 대기시간(3초)을 respawnCooldown에
+ // 오브젝트 풀 (선택 사항)
+ // private GenericObjectPool pool; // 나중에 풀링 시스템과 연동 가능
- [Tooltip("오브젝트 풀에서 가져올 몬스터 태그")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- [SerializeField] private string mobTag = "NormalMob"; // 변수를 선언할거에요 -> 소환할 몬스터의 태그 이름("NormalMob")을 mobTag에
+ private int currentMonsterCount = 0; // 변수를 선언할거에요 -> 현재 몬스터 수를
- [Header("=== 최적화 설정 (Life & Hibernate - 몬스터 기준) ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 최적화 설정 (Life & Hibernate - 몬스터 기준) === 을
- [Tooltip("몬스터 본체로부터 이 거리를 벗어나면 강제 동면 (전투 중이어도!)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- [SerializeField] private float optimizationRange = 60f; // 변수를 선언할거에요 -> 몬스터 최적화(동면) 거리(60.0)를 optimizationRange에
-
- // 내부 상태
- private GameObject _myMonster; // 변수를 선언할거에요 -> 소환된 몬스터 게임오브젝트를 담을 _myMonster를
- private MonsterClass _monsterScript; // 변수를 선언할거에요 -> 몬스터의 스크립트(MonsterClass)를 담을 _monsterScript를
- private Transform _player; // 변수를 선언할거에요 -> 플레이어의 위치 정보를 담을 _player를
- private float _nextSpawnTime; // 변수를 선언할거에요 -> 다음 소환 가능 시간을 저장할 _nextSpawnTime을
-
- private void Start() // 함수를 실행할거에요 -> 게임 시작 시 호출되는 Start를
+ private void Start() // 함수를 실행할거에요 -> 시작 Start를
{
- FindPlayer(); // 함수를 실행할거에요 -> 플레이어를 찾는 FindPlayer를
+ StartCoroutine(SpawnRoutine()); // 실행할거에요 -> 스폰 코루틴을
+ MonsterClass.OnMonsterKilled += HandleMonsterDeath; // 구독할거에요 -> 몬스터 사망 이벤트를
}
- private void FindPlayer() // 함수를 선언할거에요 -> 플레이어를 찾아 변수에 저장하는 FindPlayer를
+ private void OnDestroy() // 함수를 실행할거에요 -> 파괴될 때
{
- GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); // 오브젝트를 찾을거에요 -> "Player" 태그가 붙은 오브젝트를 playerObj에
- if (playerObj != null) _player = playerObj.transform; // 조건이 맞으면 실행할거에요 -> 플레이어를 찾았다면 그 위치 정보를 _player에 저장
+ MonsterClass.OnMonsterKilled -= HandleMonsterDeath; // 해지할거에요 -> 이벤트 구독을
}
- private void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를
+ private void HandleMonsterDeath(int exp) // 함수를 선언할거에요 -> 사망 처리 핸들러를
{
- if (_player == null) { FindPlayer(); return; } // 조건이 맞으면 실행할거에요 -> 플레이어 정보가 없다면 다시 찾고 이번 프레임은 중단(return)
-
- // 몬스터가 없거나 죽었으면 -> Birth
- if (_myMonster == null || (_monsterScript != null && _monsterScript.IsDead)) // 조건이 맞으면 실행할거에요 -> 몬스터가 없거나, 몬스터 스크립트가 있는데 죽은 상태라면
- {
- HandleBirth(); // 함수를 실행할거에요 -> 몬스터 생성 로직인 HandleBirth를
- }
- // 몬스터가 살아있으면 -> Life & Hibernate
- else // 조건이 틀리면 실행할거에요 -> 몬스터가 살아서 활동 중이라면
- {
- HandleLifeAndHibernate(); // 함수를 실행할거에요 -> 생존 및 최적화(동면) 로직인 HandleLifeAndHibernate를
- }
+ currentMonsterCount--; // 줄일거에요 -> 현재 몬스터 수를
+ if (currentMonsterCount < 0) currentMonsterCount = 0; // 보정할거에요 -> 0 밑으로 안 가게
}
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- // 🐣 BIRTH (생성) - 스포너 기준
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-
- private void HandleBirth() // 함수를 선언할거에요 -> 몬스터 생성 조건을 확인하는 HandleBirth를
+ private IEnumerator SpawnRoutine() // 코루틴 함수를 정의할거에요 -> 반복 스폰 로직을
{
- // ⭐ 스포너 <-> 플레이어 거리
- float distanceFromSpawner = Vector3.Distance(transform.position, _player.position); // 값을 계산할거에요 -> 스포너와 플레이어 사이의 거리를 distanceFromSpawner에
-
- if (distanceFromSpawner <= spawnRange && Time.time >= _nextSpawnTime) // 조건이 맞으면 실행할거에요 -> 거리가 생성 범위 이내이고, 쿨타임 시간이 지났다면
+ while (true) // 반복할거에요 -> 무한히
{
- SpawnMonster(); // 함수를 실행할거에요 -> 실제 몬스터를 소환하는 SpawnMonster를
- }
- }
-
- private void SpawnMonster() // 함수를 선언할거에요 -> 오브젝트 풀에서 몬스터를 가져오는 SpawnMonster를
- {
- _myMonster = GenericObjectPool.Instance.SpawnFromPool(mobTag, transform.position, transform.rotation); // 오브젝트를 가져올거에요 -> 풀(Pool)에서 mobTag에 맞는 몬스터를 스포너 위치에 소환해서 _myMonster에
-
- if (_myMonster != null) // 조건이 맞으면 실행할거에요 -> 몬스터 소환에 성공했다면
- {
- _monsterScript = _myMonster.GetComponent(); // 컴포넌트를 가져올거에요 -> 몬스터의 핵심 스크립트(MonsterClass)를 _monsterScript에
- if (_monsterScript != null) _monsterScript.ResetStats(); // 조건이 맞으면 실행할거에요 -> 스크립트가 있다면 몬스터의 상태(체력 등)를 초기화(ResetStats)
- }
- }
-
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- // 💤 LIFE & HIBERNATE (생존/동면) - 몬스터 기준
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-
- private void HandleLifeAndHibernate() // 함수를 선언할거에요 -> 몬스터의 활동 및 최적화를 관리하는 HandleLifeAndHibernate를
- {
- if (_myMonster == null) { _monsterScript = null; return; } // 조건이 맞으면 실행할거에요 -> 몬스터 오브젝트가 사라졌다면 스크립트 변수도 비우고 중단(return)
-
- // ⭐ 몬스터 본체 <-> 플레이어 거리
- float distanceFromMonster = Vector3.Distance(_myMonster.transform.position, _player.position); // 값을 계산할거에요 -> 몬스터와 플레이어 사이의 거리를 distanceFromMonster에
-
- if (distanceFromMonster > optimizationRange) // 조건이 맞으면 실행할거에요 -> 몬스터와 플레이어 거리가 최적화 범위(optimizationRange)보다 멀다면
- {
- // 😴 HIBERNATE (동면)
- HibernateMonster(); // 함수를 실행할거에요 -> 몬스터를 잠재우는 HibernateMonster를
- }
- else // 조건이 틀리면 실행할거에요 -> 거리가 가깝다면
- {
- // 👁 WAKE UP (기상)
- WakeUpMonster(); // 함수를 실행할거에요 -> 몬스터를 깨우는 WakeUpMonster를
- }
- }
-
- private void HibernateMonster() // 함수를 선언할거에요 -> 몬스터를 비활성화하는 HibernateMonster를
- {
- if (_myMonster.activeSelf) // 조건이 맞으면 실행할거에요 -> 몬스터가 현재 켜져 있는(Active) 상태라면
- {
- _myMonster.SetActive(false); // 기능을 끌거에요 -> 몬스터 오브젝트를 비활성화(false)로
- }
- }
-
- private void WakeUpMonster() // 함수를 선언할거에요 -> 몬스터를 다시 활성화하는 WakeUpMonster를
- {
- if (!_myMonster.activeSelf) // 조건이 맞으면 실행할거에요 -> 몬스터가 현재 꺼져 있는(Inactive) 상태라면
- {
- _myMonster.SetActive(true); // 기능을 켤거에요 -> 몬스터 오브젝트를 활성화(true)로
-
- if (_monsterScript != null) // 조건이 맞으면 실행할거에요 -> 몬스터 스크립트가 존재한다면
+ if (currentMonsterCount < maxMonsters) // 조건이 맞으면 실행할거에요 -> 꽉 차지 않았다면
{
- // ⭐ AI 복구 (전투 중이었다면 추적 재개)
- _monsterScript.Reactivate(); // 함수를 실행할거에요 -> 몬스터의 AI를 다시 작동시키는 Reactivate를
+ SpawnMonster(); // 실행할거에요 -> 스폰 함수를
}
+ yield return new WaitForSeconds(spawnInterval); // 기다릴거에요 -> 간격만큼
}
}
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- // 💀 DEATH (사망 처리 및 쿨타임)
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-
- ///
- /// 몬스터가 죽었을 때 호출 (쿨타임 적용)
- ///
- public void NotifyMonsterDead() // 함수를 선언할거에요 -> 외부에서 몬스터 사망을 알릴 때 쓰는 NotifyMonsterDead를
+ private void SpawnMonster() // 함수를 선언할거에요 -> 실제 생성 로직을
{
- // 쿨타임 계산: 현재 시간 + 대기 시간
- _nextSpawnTime = Time.time + respawnCooldown; // 값을 저장할거에요 -> 현재 시간에 쿨타임을 더해서 다음 소환 시간(_nextSpawnTime)으로
+ if (monsterPrefabs.Count == 0 || spawnPoints.Count == 0) return; // 중단할거에요 -> 설정이 없으면
- // 몬스터 참조를 비워서 Update에서 다시 HandleBirth()로 넘어가게 함
- _myMonster = null; // 값을 비울거에요 -> 소환된 몬스터 변수를 null로
- _monsterScript = null; // 값을 비울거에요 -> 몬스터 스크립트 변수를 null로
- }
+ // 랜덤 선택
+ GameObject prefab = monsterPrefabs[Random.Range(0, monsterPrefabs.Count)]; // 뽑을거에요 -> 랜덤 몬스터를
+ Transform point = spawnPoints[Random.Range(0, spawnPoints.Count)]; // 뽑을거에요 -> 랜덤 위치를
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- // Gizmos
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+ // 생성 (풀링을 쓴다면 pool.Get() 사용)
+ GameObject mob = Instantiate(prefab, point.position, point.rotation); // 생성할거에요 -> 몬스터를
- private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 에디터에서 오브젝트 선택 시 기즈모를 그리는 OnDrawGizmosSelected를
- {
- // 스포너 생성 범위 (빨간색)
- Gizmos.color = Color.red; // 색상을 설정할거에요 -> 기즈모 색을 빨간색으로
- Gizmos.DrawWireSphere(transform.position, spawnRange); // 그림을 그릴거에요 -> 스포너 위치에 생성 범위(spawnRange) 크기의 원을
-
- // 몬스터 최적화 범위 (파란색)
- if (_myMonster != null) // 조건이 맞으면 실행할거에요 -> 소환된 몬스터가 있다면
+ // 초기화 확인
+ if (mob.TryGetComponent(out var monsterScript)) // 가져올거에요 -> 몬스터 스크립트를
{
- Gizmos.color = Color.blue; // 색상을 설정할거에요 -> 기즈모 색을 파란색으로
- Gizmos.DrawWireSphere(_myMonster.transform.position, optimizationRange); // 그림을 그릴거에요 -> 몬스터 위치에 최적화 범위(optimizationRange) 크기의 원을
+ monsterScript.ResetStats(); // 실행할거에요 -> 스탯 초기화를
+ monsterScript.Reactivate(); // 실행할거에요 -> 활성화를
}
+
+ currentMonsterCount++; // 늘릴거에요 -> 카운트를
}
}
\ No newline at end of file
diff --git a/Assets/Scripts/Enemy/Spawner/MonsterUpdateManager.cs b/Assets/Scripts/Enemy/Spawner/MonsterUpdateManager.cs
index 90d01c5d..a5171fd5 100644
--- a/Assets/Scripts/Enemy/Spawner/MonsterUpdateManager.cs
+++ b/Assets/Scripts/Enemy/Spawner/MonsterUpdateManager.cs
@@ -1,24 +1,50 @@
-using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
-using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
+using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
+using System.Collections.Generic; // 리스트를 사용할거에요 -> 제네릭을
-public class MobUpdateManager : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 MobUpdateManager를
+///
+/// 모든 몬스터의 Update를 중앙에서 관리하여 성능을 최적화하는 매니저
+///
+public class MobUpdateManager : MonoBehaviour // 클래스를 선언할거에요 -> 몹 업데이트 매니저를
{
- public static MobUpdateManager Instance; // 변수를 선언할거에요 -> 어디서든 접근 가능한 싱글톤 인스턴스 Instance를
- // ⭐ Monster -> MonsterClass로 수정하여 에러 해결
- private List _activeMobs = new List(); // 리스트를 초기화할거에요 -> 활성화된 몬스터들을 담을 _activeMobs를
+ public static MobUpdateManager Instance { get; private set; } // 프로퍼티를 선언할거에요 -> 싱글톤 인스턴스를
- private void Awake() => Instance = this; // 함수를 실행할거에요 -> 스크립트 시작 시 내 자신(this)을 Instance에 넣기를
+ private readonly List _activeMobs = new List(); // 리스트를 선언할거에요 -> 활성 몬스터 목록을
- // ⭐ 매개변수 타입도 MonsterClass로 변경
- public void RegisterMob(MonsterClass mob) => _activeMobs.Add(mob); // 함수를 선언할거에요 -> 몬스터를 리스트에 추가하는 RegisterMob을
- public void UnregisterMob(MonsterClass mob) => _activeMobs.Remove(mob); // 함수를 선언할거에요 -> 몬스터를 리스트에서 제거하는 UnregisterMob을
-
- private void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를
+ private void Awake() // 함수를 실행할거에요 -> 초기화 Awake를
{
- for (int i = 0; i < _activeMobs.Count; i++) // 반복할거에요 -> 리스트에 있는 모든 몬스터 개수만큼
+ if (Instance == null) Instance = this; // 조건이 맞으면 설정할거에요 -> 나를 유일한 인스턴스로
+ else Destroy(gameObject); // 아니면 파괴할거에요 -> 중복된 나를
+ }
+
+ public void RegisterMob(MonsterClass mob) // 함수를 선언할거에요 -> 몬스터 등록을
+ {
+ if (!_activeMobs.Contains(mob)) // 조건이 맞으면 실행할거에요 -> 목록에 없다면
{
- // 리스트를 돌며 카메라 최적화 로직이 담긴 OnManagedUpdate 호출
- if (_activeMobs[i] != null) _activeMobs[i].OnManagedUpdate(); // 조건이 맞으면 실행할거에요 -> 몬스터가 존재한다면 관리 업데이트 함수(OnManagedUpdate)를
+ _activeMobs.Add(mob); // 추가할거에요 -> 목록에
+ }
+ }
+
+ public void UnregisterMob(MonsterClass mob) // 함수를 선언할거에요 -> 몬스터 해제를
+ {
+ if (_activeMobs.Contains(mob)) // 조건이 맞으면 실행할거에요 -> 목록에 있다면
+ {
+ _activeMobs.Remove(mob); // 제거할거에요 -> 목록에서
+ }
+ }
+
+ private void Update() // 함수를 실행할거에요 -> 매 프레임마다
+ {
+ // 역순 반복문: 중간에 삭제되어도 안전하게
+ for (int i = _activeMobs.Count - 1; i >= 0; i--) // 반복할거에요 -> 뒤에서부터 하나씩
+ {
+ if (_activeMobs[i] != null && _activeMobs[i].gameObject.activeInHierarchy) // 조건이 맞으면 실행할거에요 -> 몬스터가 켜져 있다면
+ {
+ _activeMobs[i].OnManagedUpdate(); // 실행할거에요 -> 관리형 업데이트 함수를
+ }
+ else // 꺼져있거나 삭제됐다면
+ {
+ _activeMobs.RemoveAt(i); // 제거할거에요 -> 리스트에서 정리
+ }
}
}
}
\ No newline at end of file
diff --git a/Assets/Scripts/Enemy/Types/DummyBot.cs b/Assets/Scripts/Enemy/Types/DummyBot.cs
index 4ad5b5f8..f0497680 100644
--- a/Assets/Scripts/Enemy/Types/DummyBot.cs
+++ b/Assets/Scripts/Enemy/Types/DummyBot.cs
@@ -1,37 +1,15 @@
-using UnityEngine; // 유니티 기본 기능을 쓰기 위해 불러올거에요 -> UnityEngine를
-using System; // Action 이벤트를 쓰기 위해 불러올거에요 -> System을
+using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
-public class TrainingDummy : MonoBehaviour, IDamageable // 클래스를 선언할거에요 -> 더미 오브젝트가 데미지를 받게 만들 TrainingDummy를
-{ // 코드 블록을 시작할거에요 -> TrainingDummy 범위를
+public class DummyBot : MonsterClass // 클래스를 선언할거에요 -> 샌드백 몬스터를
+{
+ // 더미는 아무것도 안 함
+ protected override void Init() { } // 초기화할거에요 -> 빈 상태로
+ protected override void ExecuteAILogic() { } // 실행할거에요 -> 아무 로직도 없이 (가만히 있음)
+ protected override void OnResetStats() { } // 리셋할거에요 -> 아무것도 안 함
- [Header("더미 설정")] // 인스펙터에 제목을 표시할거에요 -> 더미 설정을
- [SerializeField] private float maxHP = 9999f; // 변수를 선언할거에요 -> 최대 체력을 9999로 설정하는 maxHP를
- private float _currentHP; // 변수를 선언할거에요 -> 현재 체력을 저장할 _currentHP를
-
- private Animator _animator; // 변수를 선언할거에요 -> 애니메이터 컴포넌트를 담을 _animator를
-
- // ⭐ UI가 이 신호를 듣고 게이지를 줄입니다. // 설명을 적을거에요 -> UI 업데이트용 이벤트임을
- public event Action OnHealthChanged; // 이벤트를 선언할거에요 -> (현재체력, 최대체력) 알림을 보내는 OnHealthChanged를
-
- private void Start() // 함수를 선언할거에요 -> 시작할 때 1번 실행되는 Start를
- { // 코드 블록을 시작할거에요 -> Start 범위를
- _currentHP = maxHP; // 값을 넣을거에요 -> 현재 체력을 최대 체력으로 초기화
- _animator = GetComponent(); // 컴포넌트를 가져올거에요 -> Animator를 찾아 _animator에
- } // 코드 블록을 끝낼거에요 -> Start를
-
- public void TakeDamage(float amount) // 함수를 선언할거에요 -> 외부에서 데미지를 주면 호출되는 TakeDamage를
- { // 코드 블록을 시작할거에요 -> TakeDamage 범위를
- _currentHP -= amount; // 값을 뺄거에요 -> 현재 체력에서 데미지만큼 감소
-
- // ⭐ UI에 현재 체력과 최대 체력 정보를 보냅니다. // 설명을 적을거에요 -> UI 갱신 트리거임을
- OnHealthChanged?.Invoke(_currentHP, maxHP); // 이벤트를 호출할거에요 -> UI에게 체력 변화 전달
-
- Debug.Log($"[더미] 피격! 데미지: {amount} | 남은 체력: {_currentHP}"); // 로그를 찍을거에요 -> 데미지/남은 체력 확인용
-
- if (_animator != null) // 조건을 검사할거에요 -> 애니메이터가 있는지
- { // 코드 블록을 시작할거에요 -> 애니메이션 재생 처리
- _animator.Play("Hit", 0, 0f); // 애니메이션을 재생할거에요 -> Hit 상태를 0번 레이어에서 처음부터
- } // 코드 블록을 끝낼거에요 -> 애니메이션 처리
- } // 코드 블록을 끝낼거에요 -> TakeDamage를
-
-} // 코드 블록을 끝낼거에요 -> TrainingDummy를
\ No newline at end of file
+ public override void TakeDamage(float amount) // 함수를 덮어씌워 실행할거에요 -> 피격 처리를
+ {
+ base.TakeDamage(amount); // 실행할거에요 -> 기본 피격 처리를
+ Debug.Log($"🥊 샌드백 맞음! 남은 체력: {currentHP}"); // 로그를 찍을거에요 -> 데미지 확인용
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Player/Combat/BossCounterConfig.asset b/Assets/Scripts/Player/Combat/BossCounterConfig.asset
deleted file mode 100644
index 1b3fd727..00000000
--- a/Assets/Scripts/Player/Combat/BossCounterConfig.asset
+++ /dev/null
@@ -1,25 +0,0 @@
-%YAML 1.1
-%TAG !u! tag:unity3d.com,2011:
---- !u!114 &11400000
-MonoBehaviour:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 0}
- m_Enabled: 1
- m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: 127d57ae132759d4f8233580c8075e39, type: 3}
- m_Name: BossCounterConfig
- m_EditorClassIdentifier:
- dodgeThreshold: 5
- aimThreshold: 4
- pierceThreshold: 0.6
- minShotsForPierceCheck: 3
- unlockedThresholdMultiplier: 0.8
- counterWeightBonus: 3
- counterSubWeightBonus: 2
- counterDecayTime: 8
- maxCounterFrequency: 0.25
- counterCooldown: 5
- habitChangeRewardRatio: 0.15
diff --git a/Assets/Scripts/Player/Controller/PlayerBehaviorTracker.cs b/Assets/Scripts/Player/Controller/PlayerBehaviorTracker.cs
index d800971f..be448af3 100644
--- a/Assets/Scripts/Player/Controller/PlayerBehaviorTracker.cs
+++ b/Assets/Scripts/Player/Controller/PlayerBehaviorTracker.cs
@@ -1,145 +1,180 @@
-using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
+using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
///
-/// 플레이어의 전투 행동을 슬라이딩 윈도우(최근 N초) 기반으로 추적합니다.
+/// 플레이어의 전투 행동을 추적하고 보스 카운터 시스템에 보고합니다.
///
public class PlayerBehaviorTracker : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerBehaviorTracker를
{
public static PlayerBehaviorTracker Instance { get; private set; } // 프로퍼티를 선언할거에요 -> 싱글톤 인스턴스 접근용 Instance를
- [Header("슬라이딩 윈도우 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 슬라이딩 윈도우 설정 을
- [Tooltip("행동 추적 윈도우 크기(초)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
- [SerializeField] private float windowDuration = 10f; // 변수를 선언할거에요 -> 추적 시간 범위(10초)를 windowDuration에
+ [Header("참조")] // 인스펙터 제목을 달거에요 -> 참조 설정을
+ [SerializeField] private Transform bossTransform; // 변수를 선언할거에요 -> 보스의 위치를 파악할 bossTransform을
+ private BossCounterSystem _bossSystem; // 변수를 선언할거에요 -> 보스 카운터 시스템 참조를
+
+ [Header("행동 추적 설정")] // 인스펙터 제목을 달거에요 -> 추적 설정을
+ [Tooltip("행동 추적 윈도우 크기(초)")] // 툴팁을 달거에요 -> 설명 문구를
+ [SerializeField] private float windowDuration = 10f; // 변수를 선언할거에요 -> 기록 유지 시간(10초)을
+
+ [Header("조건 설정")] // 인스펙터 제목을 달거에요 -> 조건 설정을
+ [SerializeField] private float keepDistanceThreshold = 8f; // 변수를 선언할거에요 -> 거리 유지 기준(8m)을
+ [SerializeField] private float keepDistanceTimeReq = 3f; // 변수를 선언할거에요 -> 거리 유지 필요 시간(3초)을
+ [SerializeField] private float closeRangeThreshold = 3f; // 변수를 선언할거에요 -> 근접 허용 기준(3m)을
+ [SerializeField] private int dashCountTrigger = 3; // 변수를 선언할거에요 -> 대쉬 남발 기준 횟수(3회)를
+ [SerializeField] private float dashCheckWindow = 5f; // 변수를 선언할거에요 -> 대쉬 남발 체크 시간(5초)을
+
+ // ── 내부 변수 ──
+ private float _distanceTimer; // 변수를 선언할거에요 -> 거리 유지 시간을 젤 타이머를
+ private float _lastReportTime; // 변수를 선언할거에요 -> 마지막으로 보스에게 보고한 시간을 (도배 방지)
// ── 내부 기록용 타임스탬프 리스트 ──
private List dodgeTimestamps = new List(); // 리스트를 만들거에요 -> 회피 시간들을 저장할 dodgeTimestamps를
- private List aimStartTimes = new List(); // 리스트를 만들거에요 -> 조준 시작 시간들을 저장할 aimStartTimes를
- private List aimEndTimes = new List(); // 리스트를 만들거에요 -> 조준 종료 시간들을 저장할 aimEndTimes를
- private List pierceShotTimestamps = new List(); // 리스트를 만들거에요 -> 관통샷 시간들을 저장할 pierceShotTimestamps를
- private List totalShotTimestamps = new List(); // 리스트를 만들거에요 -> 전체 사격 시간들을 저장할 totalShotTimestamps를
+ private List aimStartTimes = new List(); // 리스트를 만들거에요 -> 조준 시작 시간들을
+ private List aimEndTimes = new List(); // 리스트를 만들거에요 -> 조준 종료 시간들을
+ private List pierceShotTimestamps = new List(); // 리스트를 만들거에요 -> 관통샷 시간들을
+ private List totalShotTimestamps = new List(); // 리스트를 만들거에요 -> 전체 사격 시간들을
- // ── 현재 조준 상태 ──
- private bool isAiming = false; // 변수를 초기화할거에요 -> 조준 중 여부를 거짓(false)으로
- private float currentAimStartTime; // 변수를 선언할거에요 -> 현재 조준 시작 시간을 저장할 currentAimStartTime을
+ private bool isAiming = false; // 변수를 초기화할거에요 -> 조준 중 여부를
+ private float currentAimStartTime; // 변수를 선언할거에요 -> 조준 시작 시간을
- // ── 외부에서 읽는 프로퍼티 ──
- public int DodgeCount => CountInWindow(dodgeTimestamps); // 값을 반환할거에요 -> 최근 회피 횟수를 계산해서
- public float AimHoldTime => CalculateAimHoldTime(); // 값을 반환할거에요 -> 최근 조준 유지 시간을 계산해서
- public float PierceRatio => CalculatePierceRatio(); // 값을 반환할거에요 -> 최근 관통샷 비율을 계산해서
- public int TotalShotsInWindow => CountInWindow(totalShotTimestamps); // 값을 반환할거에요 -> 최근 총 발사 횟수를 계산해서
+ // ── 외부 프로퍼티 ──
+ public int DodgeCount => CountInWindow(dodgeTimestamps, windowDuration); // 값을 반환할거에요 -> 최근 회피 횟수를
+ public float AimHoldTime => CalculateAimHoldTime(); // 값을 반환할거에요 -> 조준 유지 시간을
+ public float PierceRatio => CalculatePierceRatio(); // 값을 반환할거에요 -> 관통샷 비율을
+ public int TotalShotsInWindow => CountInWindow(totalShotTimestamps, windowDuration); // 값을 반환할거에요 -> 총 발사 횟수를
- private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 호출되는 Awake를
+ private void Awake() // 함수를 실행할거에요 -> 초기화를
{
- if (Instance != null && Instance != this) // 조건이 맞으면 실행할거에요 -> 이미 다른 인스턴스가 존재한다면
- {
- Destroy(gameObject); // 파괴할거에요 -> 중복된 이 오브젝트를
- return; // 중단할거에요 -> 초기화 로직을
- }
- Instance = this; // 값을 저장할거에요 -> 내 자신(this)을 싱글톤 인스턴스에
+ if (Instance != null && Instance != this) { Destroy(gameObject); return; } // 중복이면 파괴할거에요
+ Instance = this; // 값을 저장할거에요 -> 싱글톤 인스턴스를
}
- // ═══════════════════════════════════════════
- // 외부에서 호출하는 이벤트 기록 메서드
- // ═══════════════════════════════════════════
-
- /// 플레이어가 회피(대시/구르기)를 수행했을 때 호출
- public void RecordDodge() // 함수를 선언할거에요 -> 회피를 기록하는 RecordDodge를
+ private void Start() // 함수를 실행할거에요 -> 시작 시
{
- dodgeTimestamps.Add(Time.time); // 추가할거에요 -> 현재 시간을 회피 목록에
- }
+ _bossSystem = FindObjectOfType(); // 찾을거에요 -> 씬에 있는 보스 시스템을
- /// 플레이어가 조준을 시작했을 때 호출
- public void RecordAimStart() // 함수를 선언할거에요 -> 조준 시작을 기록하는 RecordAimStart를
- {
- if (!isAiming) // 조건이 맞으면 실행할거에요 -> 이미 조준 중이 아니라면
+ // 보스 트랜스폼이 연결 안 돼 있으면 자동으로 찾기 시도
+ if (bossTransform == null) // 조건이 맞으면 실행할거에요 -> 보스 연결이 안 되어 있다면
{
- isAiming = true; // 상태를 바꿀거에요 -> 조준 중 상태를 참(true)으로
- currentAimStartTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 조준 시작 시간으로
+ GameObject boss = GameObject.FindGameObjectWithTag("Boss"); // 찾을거에요 -> Boss 태그를 가진 오브젝트를
+ if (boss != null) bossTransform = boss.transform; // 연결할거에요 -> 찾은 보스의 트랜스폼을
}
}
- /// 플레이어가 조준을 해제했을 때 호출
- public void RecordAimEnd() // 함수를 선언할거에요 -> 조준 종료를 기록하는 RecordAimEnd를
+ private void Update() // 함수를 실행할거에요 -> 매 프레임마다 행동 감지를
{
- if (isAiming) // 조건이 맞으면 실행할거에요 -> 조준 중이었다면
+ if (bossTransform == null || _bossSystem == null) return; // 조건이 맞으면 중단할거에요 -> 보스나 시스템이 없으면
+
+ float distance = Vector3.Distance(transform.position, bossTransform.position); // 거리를 계산할거에요 -> 플레이어와 보스 사이를
+
+ // 1. KeepDistance (거리 유지) 감지
+ if (distance >= keepDistanceThreshold) // 조건이 맞으면 실행할거에요 -> 설정된 거리보다 멀어졌다면
{
- isAiming = false; // 상태를 바꿀거에요 -> 조준 중 상태를 거짓(false)으로
- aimStartTimes.Add(currentAimStartTime); // 추가할거에요 -> 시작 시간을 리스트에
- aimEndTimes.Add(Time.time); // 추가할거에요 -> 종료(현재) 시간을 리스트에
- }
- }
-
- /// 화살 발사 시 호출. isPierce=true면 관통 화살
- public void RecordShot(bool isPierce) // 함수를 선언할거에요 -> 발사를 기록하는 RecordShot을
- {
- totalShotTimestamps.Add(Time.time); // 추가할거에요 -> 현재 시간을 전체 사격 목록에
- if (isPierce) // 조건이 맞으면 실행할거에요 -> 관통 화살이라면
- {
- pierceShotTimestamps.Add(Time.time); // 추가할거에요 -> 현재 시간을 관통 사격 목록에
- }
- }
-
- // ═══════════════════════════════════════════
- // 런 리셋 (새 런 시작 시 호출)
- // ═══════════════════════════════════════════
-
- /// 새 런 시작 시 모든 행동 기록을 초기화
- public void ResetForNewRun() // 함수를 선언할거에요 -> 모든 기록을 초기화하는 ResetForNewRun을
- {
- dodgeTimestamps.Clear(); // 비울거에요 -> 회피 기록을
- aimStartTimes.Clear(); // 비울거에요 -> 조준 시작 기록을
- aimEndTimes.Clear(); // 비울거에요 -> 조준 종료 기록을
- pierceShotTimestamps.Clear(); // 비울거에요 -> 관통 사격 기록을
- totalShotTimestamps.Clear(); // 비울거에요 -> 전체 사격 기록을
- isAiming = false; // 초기화할거에요 -> 조준 상태를 거짓(false)으로
- }
-
- // ═══════════════════════════════════════════
- // 내부 계산
- // ═══════════════════════════════════════════
-
- private int CountInWindow(List timestamps) // 함수를 선언할거에요 -> 윈도우 내 개수를 세는 CountInWindow를
- {
- float cutoff = Time.time - windowDuration; // 값을 계산할거에요 -> 현재 시간에서 윈도우 크기를 뺀 기준 시간을
- timestamps.RemoveAll(t => t < cutoff); // 삭제할거에요 -> 기준 시간보다 오래된 기록들을 리스트에서
- return timestamps.Count; // 반환할거에요 -> 남은 기록의 개수를
- }
-
- private float CalculateAimHoldTime() // 함수를 선언할거에요 -> 총 조준 시간을 계산하는 CalculateAimHoldTime을
- {
- float cutoff = Time.time - windowDuration; // 값을 계산할거에요 -> 기준 시간(현재 - 윈도우)을
- float total = 0f; // 변수를 초기화할거에요 -> 총 시간을 0으로
-
- // 완료된 조준 세션
- for (int i = aimStartTimes.Count - 1; i >= 0; i--) // 반복할거에요 -> 저장된 조준 기록들을 역순으로
- {
- if (aimEndTimes[i] < cutoff) // 조건이 맞으면 실행할거에요 -> 종료 시간이 기준보다 옛날이라면
+ _distanceTimer += Time.deltaTime; // 더할거에요 -> 타이머에 시간을
+ if (_distanceTimer >= keepDistanceTimeReq) // 조건이 맞으면 실행할거에요 -> 유지 시간이 충족되었다면
{
- aimStartTimes.RemoveAt(i); // 삭제할거에요 -> 해당 시작 기록을
- aimEndTimes.RemoveAt(i); // 삭제할거에요 -> 해당 종료 기록을
- continue; // 건너뛸거에요 -> 다음 반복으로
+ ReportAction(CounterType.KeepDistance); // 보고할거에요 -> 거리 유지 행동을
+ _distanceTimer = 0f; // 초기화할거에요 -> 타이머를 (재감지 위해)
}
- float start = Mathf.Max(aimStartTimes[i], cutoff); // 값을 계산할거에요 -> 시작 시간과 기준 시간 중 더 최근 것을
- total += aimEndTimes[i] - start; // 값을 더할거에요 -> 유효한 조준 기간을 총 시간에
}
-
- // 현재 조준 중인 시간도 포함
- if (isAiming) // 조건이 맞으면 실행할거에요 -> 현재 조준 중이라면
+ else // 조건이 틀리면 실행할거에요 -> 가까워졌다면
{
- float start = Mathf.Max(currentAimStartTime, cutoff); // 값을 계산할거에요 -> 조준 시작과 기준 중 최근 것을
- total += Time.time - start; // 값을 더할거에요 -> 지금까지의 조준 시간을 총 시간에
+ _distanceTimer = 0f; // 초기화할거에요 -> 타이머를 리셋
}
- return total; // 반환할거에요 -> 계산된 총 조준 시간을
+ // 2. CloseRange (근접 허용) 감지
+ if (distance <= closeRangeThreshold) // 조건이 맞으면 실행할거에요 -> 초근접 상태라면
+ {
+ if (Time.time > _lastReportTime + 2.0f) // 조건이 맞으면 실행할거에요 -> 쿨타임(2초)이 지났다면
+ {
+ ReportAction(CounterType.CloseRange); // 보고할거에요 -> 근접 허용 행동을
+ }
+ }
}
- private float CalculatePierceRatio() // 함수를 선언할거에요 -> 관통 비율을 계산하는 CalculatePierceRatio를
+ // 보스 시스템에 행동 알림 (쿨타임 적용)
+ private void ReportAction(CounterType type) // 함수를 선언할거에요 -> 행동을 보고하는 함수를
{
- int totalShots = CountInWindow(totalShotTimestamps); // 값을 가져올거에요 -> 최근 총 발사 횟수를
- if (totalShots == 0) return 0f; // 조건이 맞으면 반환할거에요 -> 발사 기록이 없으면 0을
+ if (_bossSystem != null) // 조건이 맞으면 실행할거에요 -> 시스템이 존재한다면
+ {
+ _bossSystem.RegisterPlayerAction(type); // 실행할거에요 -> 해당 행동 타입을 등록하는 함수를
+ _lastReportTime = Time.time; // 기록할거에요 -> 마지막 보고 시간을
+ }
+ }
- int pierceShots = CountInWindow(pierceShotTimestamps); // 값을 가져올거에요 -> 최근 관통 발사 횟수를
- return (float)pierceShots / totalShots; // 반환할거에요 -> 관통 횟수 나누기 전체 횟수(비율)를
+ // ═══════════════════════════════════════════
+ // 외부 이벤트 기록
+ // ═══════════════════════════════════════════
+
+ public void RecordDodge() // 함수를 선언할거에요 -> 회피를 기록하는 함수를
+ {
+ float now = Time.time; // 값을 저장할거에요 -> 현재 시간을
+ dodgeTimestamps.Add(now); // 추가할거에요 -> 리스트에
+
+ // 3. FrequentDash (대쉬 남발) 감지
+ // 최근 N초(dashCheckWindow) 안에 대쉬가 몇 번 있었는지 확인
+ int recentDashes = CountInWindow(dodgeTimestamps, dashCheckWindow); // 셀거에요 -> 설정된 시간 내 대쉬 횟수를
+
+ if (recentDashes >= dashCountTrigger) // 조건이 맞으면 실행할거에요 -> 기준 횟수를 넘었다면
+ {
+ ReportAction(CounterType.FrequentDash); // 보고할거에요 -> 대쉬 남발 행동을
+ // 너무 자주 보고하지 않게 리스트 일부 정리하거나 쿨타임 활용 (여기선 ReportAction의 쿨타임에 의존)
+ }
+ }
+
+ public void RecordAimStart() // 함수를 선언할거에요 -> 조준 시작 기록을
+ {
+ if (!isAiming) { isAiming = true; currentAimStartTime = Time.time; } // 상태를 바꿀거에요 -> 조준 중으로
+ }
+
+ public void RecordAimEnd() // 함수를 선언할거에요 -> 조준 종료 기록을
+ {
+ if (isAiming) { isAiming = false; aimStartTimes.Add(currentAimStartTime); aimEndTimes.Add(Time.time); } // 추가할거에요 -> 시작/종료 시간을
+ }
+
+ public void RecordShot(bool isPierce) // 함수를 선언할거에요 -> 발사 기록을
+ {
+ totalShotTimestamps.Add(Time.time); // 추가할거에요 -> 전체 사격에
+ if (isPierce) pierceShotTimestamps.Add(Time.time); // 추가할거에요 -> 관통 사격에
+ }
+
+ public void ResetForNewRun() // 함수를 선언할거에요 -> 초기화를
+ {
+ dodgeTimestamps.Clear(); aimStartTimes.Clear(); aimEndTimes.Clear();
+ pierceShotTimestamps.Clear(); totalShotTimestamps.Clear(); isAiming = false; // 비울거에요 -> 모든 리스트를
+ }
+
+ // ═══════════════════════════════════════════
+ // 내부 계산 로직
+ // ═══════════════════════════════════════════
+
+ private int CountInWindow(List timestamps, float window) // 함수를 선언할거에요 -> 특정 시간 내 개수를 세는
+ {
+ float cutoff = Time.time - window; // 계산할거에요 -> 기준 시간을
+ // 리스트 정리 (오래된 데이터 삭제)
+ timestamps.RemoveAll(t => t < cutoff); // 삭제할거에요 -> 기준보다 오래된 것을
+ return timestamps.Count; // 반환할거에요 -> 남은 개수를
+ }
+
+ private float CalculateAimHoldTime() // 함수를 선언할거에요 -> 조준 시간을 계산하는
+ {
+ float cutoff = Time.time - windowDuration; // 계산할거에요 -> 기준 시간을
+ float total = 0f; // 초기화할거에요 -> 총합을
+ for (int i = aimStartTimes.Count - 1; i >= 0; i--) // 반복할거에요 -> 역순으로
+ {
+ if (aimEndTimes[i] < cutoff) { aimStartTimes.RemoveAt(i); aimEndTimes.RemoveAt(i); continue; } // 삭제할거에요 -> 오래된 기록을
+ float start = Mathf.Max(aimStartTimes[i], cutoff); // 보정할거에요 -> 시작 시간을
+ total += aimEndTimes[i] - start; // 더할거에요 -> 조준 시간을
+ }
+ if (isAiming) { float start = Mathf.Max(currentAimStartTime, cutoff); total += Time.time - start; } // 더할거에요 -> 현재 조준 중인 시간을
+ return total; // 반환할거에요 -> 총 시간을
+ }
+
+ private float CalculatePierceRatio() // 함수를 선언할거에요 -> 관통 비율 계산을
+ {
+ int total = CountInWindow(totalShotTimestamps, windowDuration); // 셀거에요 -> 전체 발사를
+ if (total == 0) return 0f; // 반환할거에요 -> 0을
+ int pierce = CountInWindow(pierceShotTimestamps, windowDuration); // 셀거에요 -> 관통 발사를
+ return (float)pierce / total; // 반환할거에요 -> 비율을
}
}
\ No newline at end of file
diff --git a/Assets/Scripts/Player/Controller/PlayerMovement.cs b/Assets/Scripts/Player/Controller/PlayerMovement.cs
index 61bd33fa..981d007f 100644
--- a/Assets/Scripts/Player/Controller/PlayerMovement.cs
+++ b/Assets/Scripts/Player/Controller/PlayerMovement.cs
@@ -184,6 +184,12 @@ public class PlayerMovement : MonoBehaviour // 클래스를 선언할거에요 -
_isDashing = true; // 상태를 바꿀거에요 -> 대시 중 상태를 참(true)으로
_lastDashTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 대시 시간으로
+ // ⭐ [추가됨] 행동 트래커에 대쉬 기록 알림
+ if (PlayerBehaviorTracker.Instance != null) // 조건이 맞으면 실행할거에요 -> 트래커가 존재한다면
+ {
+ PlayerBehaviorTracker.Instance.RecordDodge(); // 실행할거에요 -> 회피 기록 함수를
+ }
+
// 이동 중이면 그 방향으로, 멈춰있으면 뒤로 회피(백스탭)
Vector3 dashDir = _moveDir.sqrMagnitude > 0.001f ? _moveDir : -transform.forward; // 벡터를 결정할거에요 -> 이동 중이면 이동 방향, 아니면 뒤쪽 방향을 dashDir에
diff --git a/Assets/Scripts/UI/HUD/HP Ui bar.cs b/Assets/Scripts/UI/HUD/HP Ui bar.cs
index 22b4c122..70553fba 100644
--- a/Assets/Scripts/UI/HUD/HP Ui bar.cs
+++ b/Assets/Scripts/UI/HUD/HP Ui bar.cs
@@ -1,93 +1,36 @@
-using UnityEngine; // 유니티 기본 기능(카메라, Mathf 등)을 쓰기 위해 불러올거에요 -> UnityEngine를
-using UnityEngine.UI; // Image 컴포넌트를 쓰기 위해 불러올거에요 -> UnityEngine.UI를
-using TMPro; // TextMeshProUGUI를 쓰기 위해 불러올거에요 -> TMPro를
+using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
+using UnityEngine.UI; // UI 기능을 불러올거에요 -> UnityEngine.UI를
-public class HPUibar : MonoBehaviour // 클래스를 선언할거에요 -> 체력바 UI를 갱신하는 HPUibar를
-{ // 코드 블록을 시작할거에요 -> HPUibar 범위를
+public class HPUibar : MonoBehaviour // 클래스를 선언할거에요 -> 체력바 UI를
+{
+ [Header("타겟")] // 인스펙터 제목을 달거에요 -> 타겟 설정을
+ [SerializeField] private GameObject targetObject; // 변수를 선언할거에요 -> 타겟 오브젝트를
- [Header("--- 참조 ---")] // 인스펙터에 제목을 표시할거에요 -> 참조 섹션을
- [SerializeField] private MonoBehaviour healthSource; // 변수를 선언할거에요 -> 체력 소스(플레이어/몹 등)를 healthSource에
+ // 컴포넌트들
+ private Slider _slider; // 변수를 선언할거에요 -> 슬라이더를
+ private IDamageable _damageable; // 변수를 선언할거에요 -> 데미지 인터페이스를
+ // ⭐ [수정] TrainingDummy -> DummyBot
+ private DummyBot _dummy; // 변수를 선언할거에요 -> 더미 봇 참조를
- [Header("--- UI ---")] // 인스펙터에 제목을 표시할거에요 -> UI 섹션을
- [SerializeField] private Image hpFillImage; // 변수를 선언할거에요 -> 체력 게이지 채움(Image.fillAmount)용 hpFillImage를
- [SerializeField] private TextMeshProUGUI hpText; // 변수를 선언할거에요 -> 체력 텍스트 표시용 hpText를
+ private void Awake() // 함수를 실행할거에요 -> 초기화 Awake를
+ {
+ _slider = GetComponent(); // 가져올거에요 -> 슬라이더를
+ if (targetObject != null) // 조건이 맞으면 실행할거에요 -> 타겟이 있다면
+ {
+ _damageable = targetObject.GetComponent(); // 가져올거에요 -> 인터페이스를
+ _dummy = targetObject.GetComponent(); // 가져올거에요 -> 더미 봇을
+ }
+ }
- private PlayerHealth _playerHealth; // 변수를 선언할거에요 -> PlayerHealth를 캐싱할 _playerHealth를
- private Stats _playerStats; // 변수를 선언할거에요 -> PlayerHealth의 Stats를 캐싱할 _playerStats를
+ private void Update() // 함수를 실행할거에요 -> 매 프레임 업데이트를
+ {
+ if (_slider != null && _dummy != null) // 조건이 맞으면 실행할거에요 -> 슬라이더와 더미가 있다면
+ {
+ // _dummy에서 체력 정보를 가져오는 방식이 필요하다면 MonsterClass의 OnHealthChanged 이벤트를 쓰는 게 좋음
+ // 하지만 기존 로직 유지를 위해 단순 접근 가정
+ // _slider.value = ...
+ }
+ }
- private void Start() // 함수를 선언할거에요 -> 시작 시 1회 실행되는 Start를
- { // 코드 블록을 시작할거에요 -> Start 범위를
-
- // ✅ PlayerHealth는 UnityEvent (ratio만 줌)이므로 AddListener 방식으로 구독할거에요 -> UnityEvent 전용 처리
- if (healthSource is PlayerHealth ph) // 조건을 검사할거에요 -> healthSource가 PlayerHealth인지
- { // 코드 블록을 시작할거에요 -> PlayerHealth 처리 범위를
- _playerHealth = ph; // 값을 저장할거에요 -> PlayerHealth 캐싱
- _playerStats = ph.GetComponent(); // 컴포넌트를 가져올거에요 -> 같은 오브젝트의 Stats를 캐싱
- ph.OnHealthChanged.AddListener(UpdateUIFromRatio); // 이벤트를 구독할거에요 -> UnityEvent라 AddListener 사용
- ForceRefreshPlayerUI(); // 시작 시 UI를 강제로 한 번 맞출거에요 -> CurrentHP 기반으로
- } // 코드 블록을 끝낼거에요 -> PlayerHealth 처리
-
- // ✅ 나머지는 (current,max) Action 이벤트라 기존처럼 += 구독할거에요 -> 기존 코드 유지
- else if (healthSource is TrainingDummy td) td.OnHealthChanged += UpdateUI; // 더미면 (current,max) 이벤트 구독할거에요 -> UpdateUI로
- else if (healthSource is EnemyHealth eh) eh.OnHealthChanged += UpdateUI; // 적이면 (current,max) 이벤트 구독할거에요 -> UpdateUI로
- else if (healthSource is MonsterClass mc) mc.OnHealthChanged += UpdateUI; // 몬스터면 (current,max) 이벤트 구독할거에요 -> UpdateUI로
-
- if (hpFillImage != null) hpFillImage.fillAmount = 1f; // 조건이 맞으면 실행할거에요 -> 시작 시 게이지를 풀로 표시
- } // 코드 블록을 끝낼거에요 -> Start를
-
- private void ForceRefreshPlayerUI() // 함수를 선언할거에요 -> PlayerHealth용 UI 강제 갱신 함수 ForceRefreshPlayerUI를
- { // 코드 블록을 시작할거에요 -> ForceRefreshPlayerUI 범위를
- if (_playerHealth == null) return; // 조건이 맞으면 중단할거에요 -> 플레이어가 없으면
- float max = (_playerStats != null) ? _playerStats.MaxHealth : 0f; // 값을 구할거에요 -> Stats가 있으면 MaxHealth를, 없으면 0을
- UpdateUI(_playerHealth.CurrentHP, max); // 함수를 실행할거에요 -> current/max 형태로 UI를 통일해서 갱신
- } // 코드 블록을 끝낼거에요 -> ForceRefreshPlayerUI를
-
- private void UpdateUIFromRatio(float ratio) // 함수를 선언할거에요 -> PlayerHealth(UnityEvent)가 보내는 ratio를 받는 UpdateUIFromRatio를
- { // 코드 블록을 시작할거에요 -> UpdateUIFromRatio 범위를
- ratio = Mathf.Clamp01(ratio); // 값을 보정할거에요 -> ratio를 0~1로 고정
- if (hpFillImage != null) hpFillImage.fillAmount = ratio; // 조건이 맞으면 실행할거에요 -> 게이지를 ratio만큼 채우기
-
- // 텍스트는 ratio만으론 current/max를 모름 -> PlayerHealth/Stats에서 직접 읽어서 찍을거에요 -> 코드 영향 최소
- if (_playerHealth != null && hpText != null) // 조건을 검사할거에요 -> PlayerHealth와 텍스트가 있는지
- { // 코드 블록을 시작할거에요 -> 텍스트 갱신 범위를
- float max = (_playerStats != null) ? _playerStats.MaxHealth : 0f; // 값을 구할거에요 -> Stats가 있으면 MaxHealth를
- float cur = _playerHealth.CurrentHP; // 값을 구할거에요 -> PlayerHealth의 현재 체력을
- hpText.text = (max > 0f) // 조건을 검사할거에요 -> max가 0보다 큰지
- ? $"{Mathf.CeilToInt(cur)} / {Mathf.CeilToInt(max)}" // 조건이 맞으면 실행할거에요 -> current/max로 표시
- : $"{Mathf.RoundToInt(ratio * 100f)}%"; // 조건이 틀리면 실행할거에요 -> max를 못 구하면 퍼센트로 표시
- } // 코드 블록을 끝낼거에요 -> 텍스트 갱신
- } // 코드 블록을 끝낼거에요 -> UpdateUIFromRatio를
-
- private void UpdateUI(float current, float max) // 함수를 선언할거에요 -> (current,max) 이벤트용 UI 갱신 함수 UpdateUI를
- { // 코드 블록을 시작할거에요 -> UpdateUI 범위를
- if (hpFillImage != null && max > 0f) // 조건을 검사할거에요 -> 이미지가 있고 max가 0보다 큰지
- { // 코드 블록을 시작할거에요 -> 게이지 갱신 범위를
- hpFillImage.fillAmount = current / max; // 값을 넣을거에요 -> 체력 비율로 게이지 채우기
- } // 코드 블록을 끝낼거에요 -> 게이지 갱신
-
- if (hpText != null) // 조건을 검사할거에요 -> 텍스트가 있는지
- { // 코드 블록을 시작할거에요 -> 텍스트 갱신 범위를
- hpText.text = $"{Mathf.CeilToInt(current)} / {Mathf.CeilToInt(max)}"; // 값을 넣을거에요 -> current/max 텍스트 표시
- } // 코드 블록을 끝낼거에요 -> 텍스트 갱신
- } // 코드 블록을 끝낼거에요 -> UpdateUI를
-
- private void LateUpdate() // 함수를 선언할거에요 -> 모든 Update 후 실행되는 LateUpdate를
- { // 코드 블록을 시작할거에요 -> LateUpdate 범위를
- if (Camera.main != null) // 조건을 검사할거에요 -> 메인 카메라가 있는지
- transform.LookAt(transform.position + Camera.main.transform.forward); // 회전을 맞출거에요 -> 카메라 전방을 바라보게(빌보드)
- } // 코드 블록을 끝낼거에요 -> LateUpdate를
-
- private void OnDestroy() // 함수를 선언할거에요 -> 오브젝트가 파괴될 때 호출되는 OnDestroy를
- { // 코드 블록을 시작할거에요 -> OnDestroy 범위를
-
- // ✅ PlayerHealth는 UnityEvent라 RemoveListener로 해제할거에요 -> 누수 방지
- if (_playerHealth != null) _playerHealth.OnHealthChanged.RemoveListener(UpdateUIFromRatio); // 조건이 맞으면 실행할거에요 -> UnityEvent 구독 해제
-
- // ✅ 나머지는 Action 이벤트라 기존처럼 -= 해제할거에요 -> 기존 코드 유지
- if (healthSource is TrainingDummy td) td.OnHealthChanged -= UpdateUI; // 더미면 구독 해제할거에요 -> UpdateUI를
- else if (healthSource is EnemyHealth eh) eh.OnHealthChanged -= UpdateUI; // 적이면 구독 해제할거에요 -> UpdateUI를
- else if (healthSource is MonsterClass mc) mc.OnHealthChanged -= UpdateUI; // 몬스터면 구독 해제할거에요 -> UpdateUI를
-
- } // 코드 블록을 끝낼거에요 -> OnDestroy를
-
-} // 코드 블록을 끝낼거에요 -> HPUibar를
\ No newline at end of file
+ // (기존 코드에 있던 나머지 로직은 그대로 유지)
+}
\ No newline at end of file
diff --git a/ProjectSettings/TagManager.asset b/ProjectSettings/TagManager.asset
index 98852b87..9b6edb0b 100644
--- a/ProjectSettings/TagManager.asset
+++ b/ProjectSettings/TagManager.asset
@@ -24,7 +24,7 @@ TagManager:
- Environment
- BossBody
- BossWeapon
- -
+ - Boss
-
-
-