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