diff --git a/Assets/DefaultNetworkPrefabs.asset b/Assets/DefaultNetworkPrefabs.asset index 6046df1..c2c2ae0 100644 --- a/Assets/DefaultNetworkPrefabs.asset +++ b/Assets/DefaultNetworkPrefabs.asset @@ -84,3 +84,18 @@ MonoBehaviour: SourcePrefabToOverride: {fileID: 0} SourceHashToOverride: 0 OverridingTargetPrefab: {fileID: 0} + - Override: 0 + Prefab: {fileID: 3659626783364531313, guid: 038d8e53e81683f478bd4a50b71cdb82, type: 3} + SourcePrefabToOverride: {fileID: 0} + SourceHashToOverride: 0 + OverridingTargetPrefab: {fileID: 0} + - Override: 0 + Prefab: {fileID: 3321405240327640087, guid: 2bb7e098e271eb44a873c856dbf59c7c, type: 3} + SourcePrefabToOverride: {fileID: 0} + SourceHashToOverride: 0 + OverridingTargetPrefab: {fileID: 0} + - Override: 0 + Prefab: {fileID: 6403733529880835406, guid: 443aa97110814434cb36b26656f1884c, type: 3} + SourcePrefabToOverride: {fileID: 0} + SourceHashToOverride: 0 + OverridingTargetPrefab: {fileID: 0} diff --git a/Assets/Prefabs/Core.prefab b/Assets/Prefabs/Core.prefab index de5d52a..9e10a05 100644 --- a/Assets/Prefabs/Core.prefab +++ b/Assets/Prefabs/Core.prefab @@ -13,6 +13,8 @@ GameObject: - component: {fileID: 6108920277980214130} - component: {fileID: 8496505565574430929} - component: {fileID: 6537968746546598605} + - component: {fileID: 5420652907638236344} + - component: {fileID: 1005129893123536417} m_Layer: 0 m_Name: Core m_TagString: Core @@ -68,8 +70,6 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: eda0106ca978373449769fd4ded4658f, type: 3} m_Name: m_EditorClassIdentifier: Assembly-CSharp::Core - maxHealth: 100 - currentHealth: 100 --- !u!33 &8496505565574430929 MeshFilter: m_ObjectHideFlags: 0 @@ -127,3 +127,44 @@ MeshRenderer: m_SortingOrder: 0 m_MaskInteraction: 0 m_AdditionalVertexStreams: {fileID: 0} +--- !u!114 &5420652907638236344 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6403733529880835406} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject + GlobalObjectIdHash: 948195259 + InScenePlacedSourceGlobalObjectIdHash: 0 + DeferredDespawnTick: 0 + Ownership: 1 + AlwaysReplicateAsRoot: 0 + SynchronizeTransform: 1 + ActiveSceneSynchronization: 0 + SceneMigrationSynchronization: 1 + SpawnWithObservers: 1 + DontDestroyWithOwner: 0 + AutoObjectParentSync: 1 + SyncOwnerTransformWhenParented: 1 + AllowOwnerToParent: 0 +--- !u!114 &1005129893123536417 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6403733529880835406} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a4b9c07450e6c9c4b8c741b633a2702e, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::HealthComponent + ShowTopMostFoldoutHeaderGroup: 1 + maxHealth: 100 + destroyOnDeath: 1 + destroyDelay: 0 diff --git a/Assets/Prefabs/Enemy.prefab b/Assets/Prefabs/Enemy.prefab index f9df1ac..7c3a1a3 100644 --- a/Assets/Prefabs/Enemy.prefab +++ b/Assets/Prefabs/Enemy.prefab @@ -17,6 +17,8 @@ GameObject: - component: {fileID: 7188026176818599596} - component: {fileID: 9004018437643743475} - component: {fileID: 2076938100137781138} + - component: {fileID: -5264722347087192178} + - component: {fileID: 2217125893430872806} m_Layer: 6 m_Name: Enemy m_TagString: Enemy @@ -211,4 +213,44 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: b2ce198f9f66bbe41a739abb07974082, type: 3} m_Name: m_EditorClassIdentifier: Assembly-CSharp::EnemyHealth - maxHealth: 50 +--- !u!114 &-5264722347087192178 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3659626783364531313} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject + GlobalObjectIdHash: 61460115 + InScenePlacedSourceGlobalObjectIdHash: 0 + DeferredDespawnTick: 0 + Ownership: 1 + AlwaysReplicateAsRoot: 0 + SynchronizeTransform: 1 + ActiveSceneSynchronization: 0 + SceneMigrationSynchronization: 1 + SpawnWithObservers: 1 + DontDestroyWithOwner: 0 + AutoObjectParentSync: 1 + SyncOwnerTransformWhenParented: 1 + AllowOwnerToParent: 0 +--- !u!114 &2217125893430872806 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3659626783364531313} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a4b9c07450e6c9c4b8c741b633a2702e, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::HealthComponent + ShowTopMostFoldoutHeaderGroup: 1 + maxHealth: 100 + destroyOnDeath: 1 + destroyDelay: 0 diff --git a/Assets/Prefabs/Gate.prefab b/Assets/Prefabs/Gate.prefab index 6a382d0..db617a7 100644 --- a/Assets/Prefabs/Gate.prefab +++ b/Assets/Prefabs/Gate.prefab @@ -14,6 +14,8 @@ GameObject: - component: {fileID: 1378637490813329061} - component: {fileID: 8775718735147868365} - component: {fileID: 8186900164864976727} + - component: {fileID: 1850162772760692556} + - component: {fileID: 1697202366912309714} m_Layer: 0 m_Name: Gate m_TagString: Untagged @@ -121,8 +123,6 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: aca054ee474238545a8a396d410bf5a3, type: 3} m_Name: m_EditorClassIdentifier: Assembly-CSharp::Gate - maxHealth: 50 - currentHealth: 50 --- !u!65 &8186900164864976727 BoxCollider: m_ObjectHideFlags: 0 @@ -144,3 +144,44 @@ BoxCollider: serializedVersion: 3 m_Size: {x: 1, y: 1, z: 1} m_Center: {x: 0.5, y: 0.5, z: 0.49999997} +--- !u!114 &1850162772760692556 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3321405240327640087} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject + GlobalObjectIdHash: 2008860137 + InScenePlacedSourceGlobalObjectIdHash: 0 + DeferredDespawnTick: 0 + Ownership: 1 + AlwaysReplicateAsRoot: 0 + SynchronizeTransform: 1 + ActiveSceneSynchronization: 0 + SceneMigrationSynchronization: 1 + SpawnWithObservers: 1 + DontDestroyWithOwner: 0 + AutoObjectParentSync: 1 + SyncOwnerTransformWhenParented: 1 + AllowOwnerToParent: 0 +--- !u!114 &1697202366912309714 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3321405240327640087} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a4b9c07450e6c9c4b8c741b633a2702e, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::HealthComponent + ShowTopMostFoldoutHeaderGroup: 1 + maxHealth: 100 + destroyOnDeath: 0 + destroyDelay: 0 diff --git a/Assets/Prefabs/MineableBlock.prefab b/Assets/Prefabs/MineableBlock.prefab index 17105fc..b1d174a 100644 --- a/Assets/Prefabs/MineableBlock.prefab +++ b/Assets/Prefabs/MineableBlock.prefab @@ -15,6 +15,7 @@ GameObject: - component: {fileID: 7528764990365051674} - component: {fileID: 3421159559893464927} - component: {fileID: -6050457801577222831} + - component: {fileID: 1313689911257131059} m_Layer: 12 m_Name: MineableBlock m_TagString: Untagged @@ -154,9 +155,11 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: Assembly-CSharp::MineableBlock ShowTopMostFoldoutHeaderGroup: 1 - maxHp: 100 + dropItemData: {fileID: 0} + genericDropPrefab: {fileID: 0} shakeDuration: 0.1 shakeMagnitude: 0.1 + darkIntensity: 0.2 --- !u!114 &-6050457801577222831 MonoBehaviour: m_ObjectHideFlags: 0 @@ -175,3 +178,19 @@ MonoBehaviour: precomputeOutline: 0 bakeKeys: [] bakeValues: [] +--- !u!114 &1313689911257131059 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 989066657509100432} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a4b9c07450e6c9c4b8c741b633a2702e, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::HealthComponent + ShowTopMostFoldoutHeaderGroup: 1 + maxHealth: 100 + destroyOnDeath: 1 + destroyDelay: 0 diff --git a/Assets/Prefabs/ResourceBlock.prefab b/Assets/Prefabs/ResourceBlock.prefab index a126232..e674250 100644 --- a/Assets/Prefabs/ResourceBlock.prefab +++ b/Assets/Prefabs/ResourceBlock.prefab @@ -15,6 +15,7 @@ GameObject: - component: {fileID: 7528764990365051674} - component: {fileID: 3421159559893464927} - component: {fileID: -8077891763218709486} + - component: {fileID: 1570038371682152649} m_Layer: 12 m_Name: ResourceBlock m_TagString: Untagged @@ -128,7 +129,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} m_Name: m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject - GlobalObjectIdHash: 822699884 + GlobalObjectIdHash: 1191681468 InScenePlacedSourceGlobalObjectIdHash: 1191681468 DeferredDespawnTick: 0 Ownership: 1 @@ -154,11 +155,11 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: Assembly-CSharp::MineableBlock ShowTopMostFoldoutHeaderGroup: 1 - maxHp: 100 dropItemData: {fileID: 11400000, guid: 953ceca9a25978549a56f0a4ff5d6a2c, type: 2} genericDropPrefab: {fileID: 1253970051563370359, guid: 1d7655b1088c3ea46b8f52f6c6760047, type: 3} shakeDuration: 0.1 shakeMagnitude: 0.1 + darkIntensity: 0.2 --- !u!114 &-8077891763218709486 MonoBehaviour: m_ObjectHideFlags: 0 @@ -177,3 +178,19 @@ MonoBehaviour: precomputeOutline: 0 bakeKeys: [] bakeValues: [] +--- !u!114 &1570038371682152649 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 989066657509100432} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a4b9c07450e6c9c4b8c741b633a2702e, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::HealthComponent + ShowTopMostFoldoutHeaderGroup: 1 + maxHealth: 100 + destroyOnDeath: 1 + destroyDelay: 0 diff --git a/Assets/Resources/Item_Pickaxe.asset b/Assets/Resources/Item_Pickaxe.asset index 1181bb1..36d2289 100644 --- a/Assets/Resources/Item_Pickaxe.asset +++ b/Assets/Resources/Item_Pickaxe.asset @@ -15,11 +15,13 @@ MonoBehaviour: itemID: 1 itemName: "\uACE1\uAD2D\uC774" icon: {fileID: 1410797105229532982, guid: 206db4b680835834bb863402d6ff99f0, type: 3} + description: weight: 0 maxStack: 1 originalBlockPrefab: {fileID: 919132149155446097, guid: ebadcbcc9e8baa1469830ca427684d65, type: 3} - isTool: 1 - toolAction: {fileID: 11400000, guid: 677adf45880ed9e4a80b2f113fff07f8, type: 2} - toolPrefab: {fileID: 919132149155446097, guid: ebadcbcc9e8baa1469830ca427684d65, type: 3} + behavior: {fileID: 11400000, guid: 239d2d4ad6cebe042813b3d938255356, type: 2} + isEquippable: 1 + equipmentPrefab: {fileID: 1272226527344106, guid: 346845f0154817c4297d6af7c695b28a, type: 3} equipPositionOffset: {x: 0, y: 0, z: 0} equipRotationOffset: {x: 0, y: 0, z: 0} + attachmentPointName: handslot.r diff --git a/Assets/Resources/New Mining Behavior.asset b/Assets/Resources/New Mining Behavior.asset new file mode 100644 index 0000000..a157eac --- /dev/null +++ b/Assets/Resources/New Mining Behavior.asset @@ -0,0 +1,23 @@ +%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: c1deeb9de56edff4ca77ddabf9db691a, type: 3} + m_Name: New Mining Behavior + m_EditorClassIdentifier: Assembly-CSharp::MiningBehavior + behaviorName: Mining + duration: 0.5 + animTrigger: Attack + animSpeed: 1 + impactDelay: 0.2 + canRepeat: 1 + useSound: {fileID: 0} + useEffect: {fileID: 0} + damage: 50 diff --git a/Assets/Resources/New Mining Behavior.asset.meta b/Assets/Resources/New Mining Behavior.asset.meta new file mode 100644 index 0000000..1030303 --- /dev/null +++ b/Assets/Resources/New Mining Behavior.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 239d2d4ad6cebe042813b3d938255356 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scenes/DefenceScene.unity b/Assets/Scenes/DefenceScene.unity index c9aec7b..a300c1e 100644 --- a/Assets/Scenes/DefenceScene.unity +++ b/Assets/Scenes/DefenceScene.unity @@ -926,6 +926,22 @@ NavMeshObstacle: m_CarveOnlyStationary: 1 m_Center: {x: 0, y: 0, z: 0} m_TimeToStationary: 0.5 +--- !u!114 &445606028 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 445606021} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a4b9c07450e6c9c4b8c741b633a2702e, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::HealthComponent + ShowTopMostFoldoutHeaderGroup: 1 + maxHealth: 100 + destroyOnDeath: 1 + destroyDelay: 0 --- !u!1001 &497942047 PrefabInstance: m_ObjectHideFlags: 0 @@ -1091,7 +1107,7 @@ MonoBehaviour: m_DisconnectTimeoutMS: 30000 ConnectionData: Address: 127.0.0.1 - Port: 7793 + Port: 7781 ServerListenAddress: 127.0.0.1 ClientBindPort: 0 DebugSimulator: @@ -1724,6 +1740,18 @@ PrefabInstance: propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} + - target: {fileID: 5420652907638236344, guid: 443aa97110814434cb36b26656f1884c, type: 3} + propertyPath: GlobalObjectIdHash + value: 3705943614 + objectReference: {fileID: 0} + - target: {fileID: 5420652907638236344, guid: 443aa97110814434cb36b26656f1884c, type: 3} + propertyPath: SceneMigrationSynchronization + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 5420652907638236344, guid: 443aa97110814434cb36b26656f1884c, type: 3} + propertyPath: InScenePlacedSourceGlobalObjectIdHash + value: 948195259 + objectReference: {fileID: 0} - target: {fileID: 6403733529880835406, guid: 443aa97110814434cb36b26656f1884c, type: 3} propertyPath: m_Name value: Core @@ -1739,6 +1767,9 @@ PrefabInstance: - targetCorrespondingSourceObject: {fileID: 6403733529880835406, guid: 443aa97110814434cb36b26656f1884c, type: 3} insertIndex: -1 addedObject: {fileID: 445606027} + - targetCorrespondingSourceObject: {fileID: 6403733529880835406, guid: 443aa97110814434cb36b26656f1884c, type: 3} + insertIndex: -1 + addedObject: {fileID: 445606028} m_SourcePrefab: {fileID: 100100000, guid: 443aa97110814434cb36b26656f1884c, type: 3} --- !u!1 &1044242050 GameObject: @@ -1864,6 +1895,18 @@ PrefabInstance: propertyPath: 'm_Materials.Array.data[0]' value: objectReference: {fileID: 2100000, guid: 2425c03ff18262a4eaa45371f0fe6dcf, type: 2} + - target: {fileID: 1850162772760692556, guid: 2bb7e098e271eb44a873c856dbf59c7c, type: 3} + propertyPath: GlobalObjectIdHash + value: 4230179970 + objectReference: {fileID: 0} + - target: {fileID: 1850162772760692556, guid: 2bb7e098e271eb44a873c856dbf59c7c, type: 3} + propertyPath: SceneMigrationSynchronization + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1850162772760692556, guid: 2bb7e098e271eb44a873c856dbf59c7c, type: 3} + propertyPath: InScenePlacedSourceGlobalObjectIdHash + value: 2008860137 + objectReference: {fileID: 0} - target: {fileID: 3321405240327640087, guid: 2bb7e098e271eb44a873c856dbf59c7c, type: 3} propertyPath: m_Name value: Gate @@ -1919,8 +1962,32 @@ PrefabInstance: m_RemovedComponents: [] m_RemovedGameObjects: [] m_AddedGameObjects: [] - m_AddedComponents: [] + m_AddedComponents: + - targetCorrespondingSourceObject: {fileID: 3321405240327640087, guid: 2bb7e098e271eb44a873c856dbf59c7c, type: 3} + insertIndex: -1 + addedObject: {fileID: 1075139052} m_SourcePrefab: {fileID: 100100000, guid: 2bb7e098e271eb44a873c856dbf59c7c, type: 3} +--- !u!1 &1075139051 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 3321405240327640087, guid: 2bb7e098e271eb44a873c856dbf59c7c, type: 3} + m_PrefabInstance: {fileID: 1075139050} + m_PrefabAsset: {fileID: 0} +--- !u!114 &1075139052 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1075139051} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a4b9c07450e6c9c4b8c741b633a2702e, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::HealthComponent + ShowTopMostFoldoutHeaderGroup: 1 + maxHealth: 100 + destroyOnDeath: 0 + destroyDelay: 0 --- !u!1 &1095978102 GameObject: m_ObjectHideFlags: 0 @@ -3049,6 +3116,136 @@ Camera: m_CorrespondingSourceObject: {fileID: 5650099317679730308, guid: 2b08dd32e48ef5e4aa65a6122099152e, type: 3} m_PrefabInstance: {fileID: 3690888448170635710} m_PrefabAsset: {fileID: 0} +--- !u!1 &1782529040 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1782529044} + - component: {fileID: 1782529043} + - component: {fileID: 1782529042} + - component: {fileID: 1782529041} + - component: {fileID: 1782529045} + m_Layer: 0 + m_Name: FogOverlay + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!64 &1782529041 +MeshCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1782529040} + 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: 5 + m_Convex: 0 + m_CookingOptions: 30 + m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0} +--- !u!23 &1782529042 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1782529040} + 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_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 3deb4a29e039554439a14651844edec2, 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_GlobalIlluminationMeshLod: 0 + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_MaskInteraction: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &1782529043 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1782529040} + m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &1782529044 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1782529040} + serializedVersion: 2 + m_LocalRotation: {x: -0.7071068, y: 0, z: 0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 0, z: 10} + m_LocalScale: {x: 1000, y: 1, z: 1000} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: -90, y: 0, z: 0} +--- !u!114 &1782529045 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1782529040} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4867a597119adb047ab17b69beca76be, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::FogOfWarManager + worldSize: 1000 + textureSize: 512 + revealRadius: 3 + groundLevelY: -1 --- !u!1 &1842105545 GameObject: m_ObjectHideFlags: 0 @@ -3739,3 +3936,4 @@ SceneRoots: - {fileID: 556982644} - {fileID: 2067098344} - {fileID: 1384281111} + - {fileID: 1782529044} diff --git a/Assets/Scripts/Enemy/EnemyHealth.cs b/Assets/Scripts/Enemy/EnemyHealth.cs index 76f723e..a8d1e9c 100644 --- a/Assets/Scripts/Enemy/EnemyHealth.cs +++ b/Assets/Scripts/Enemy/EnemyHealth.cs @@ -1,33 +1,66 @@ using UnityEngine; +using System; -public class EnemyHealth : MonoBehaviour, IDamageable +/// +/// Enemy health handler. +/// Uses HealthComponent for health management. +/// +[RequireComponent(typeof(HealthComponent))] +public class EnemyHealth : MonoBehaviour { - public float maxHealth = 50f; - private float _currentHealth; + private HealthComponent _health; - void Start() + /// + /// Event fired when this enemy dies. + /// + public event Action OnEnemyDeath; + + void Awake() { - _currentHealth = maxHealth; + _health = GetComponent(); + + // Subscribe to health component events + _health.OnDamaged += HandleDamaged; + _health.OnDeath += HandleDeath; } - public void TakeDamage(float damage) + void OnDestroy() { - _currentHealth -= damage; - Debug.Log($"{gameObject.name} 남은 체력: {_currentHealth}"); - - // 데미지 입었을 때 반짝이는 효과를 여기서 호출해도 좋습니다. - // 예: GetComponent().StartFlash(); - - if (_currentHealth <= 0) + if (_health != null) { - Die(); + _health.OnDamaged -= HandleDamaged; + _health.OnDeath -= HandleDeath; } } - private void Die() + private void HandleDamaged(DamageInfo info) { - Debug.Log($"{gameObject.name} 사망!"); - // 여기서 파티클 생성이나 점수 추가 등을 처리합니다. + Debug.Log($"{gameObject.name} took {info.Amount} damage. Remaining: {_health.CurrentHealth}"); + } + + private void HandleDeath() + { + Debug.Log($"{gameObject.name} died!"); + + // Fire event for external listeners (score system, spawner, etc.) + OnEnemyDeath?.Invoke(); + + // Destroy the enemy Destroy(gameObject); } -} \ No newline at end of file + + /// + /// Get the health component for direct access if needed. + /// + public HealthComponent Health => _health; + + /// + /// Get the current health (convenience property). + /// + public float CurrentHealth => _health != null ? _health.CurrentHealth : 0f; + + /// + /// Get the max health (convenience property). + /// + public float MaxHealth => _health != null ? _health.MaxHealth : 0f; +} diff --git a/Assets/Scripts/GameBase/Core.cs b/Assets/Scripts/GameBase/Core.cs index 6098b74..9378e07 100644 --- a/Assets/Scripts/GameBase/Core.cs +++ b/Assets/Scripts/GameBase/Core.cs @@ -1,24 +1,21 @@ using UnityEngine; -using System; -public class Core : MonoBehaviour, IDamageable +/// +/// Core structure that players must defend. +/// Uses HealthComponent for health management. +/// +[RequireComponent(typeof(HealthComponent))] +public class Core : MonoBehaviour { - [SerializeField] private float maxHealth = 100f; - [SerializeField] private float currentHealth = 100f; - private float CurrentHealth; + private HealthComponent _health; - // 체력이 변경될 때 UI 등에 알리기 위한 이벤트 (Observer 패턴) - public static event Action OnHealthChanged; - public static event Action OnCoreDestroyed; - - void Awake() => currentHealth = maxHealth; - - public void TakeDamage(float amount) + void Awake() { - currentHealth -= amount; - OnHealthChanged?.Invoke(currentHealth / maxHealth); - - if (currentHealth <= 0) - OnCoreDestroyed?.Invoke(); + _health = GetComponent(); } -} \ No newline at end of file + + /// + /// Get the health component for direct access. + /// + public HealthComponent Health => _health; +} diff --git a/Assets/Scripts/GameBase/GameManager.cs b/Assets/Scripts/GameBase/GameManager.cs index f08e601..cc8942d 100644 --- a/Assets/Scripts/GameBase/GameManager.cs +++ b/Assets/Scripts/GameBase/GameManager.cs @@ -1,19 +1,42 @@ using UnityEngine; -using UnityEngine.SceneManagement; // 씬 재시작용 +using UnityEngine.SceneManagement; +/// +/// Manages game state and responds to core destruction. +/// public class GameManager : MonoBehaviour { - private bool _isGameOver = false; + [SerializeField] private Core coreReference; - private void OnEnable() + private bool _isGameOver = false; + private HealthComponent _coreHealth; + + private void Start() { - // Core의 파괴 이벤트를 구독 - Core.OnCoreDestroyed += GameOver; + // Find Core if not assigned + if (coreReference == null) + { + coreReference = FindFirstObjectByType(); + } + + // Subscribe to core's health component + if (coreReference != null) + { + _coreHealth = coreReference.Health; + if (_coreHealth != null) + { + _coreHealth.OnDeath += GameOver; + } + } } - private void OnDisable() + private void OnDestroy() { - Core.OnCoreDestroyed -= GameOver; + // Unsubscribe to prevent memory leaks + if (_coreHealth != null) + { + _coreHealth.OnDeath -= GameOver; + } } private void GameOver() @@ -23,14 +46,13 @@ public class GameManager : MonoBehaviour _isGameOver = true; Debug.Log("Game Over! Core has been destroyed."); - // 여기에 패배 UI 표시 로직 등을 넣습니다. - // 예: 3초 후 게임 재시작 + // Show defeat UI here + // Example: restart game after 3 seconds Invoke(nameof(RestartGame), 3f); } private void RestartGame() { - // 현재 활성화된 씬을 다시 로드 SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex); } -} \ No newline at end of file +} diff --git a/Assets/Scripts/GameBase/Gate.cs b/Assets/Scripts/GameBase/Gate.cs index e311a33..d53d2be 100644 --- a/Assets/Scripts/GameBase/Gate.cs +++ b/Assets/Scripts/GameBase/Gate.cs @@ -1,34 +1,51 @@ using UnityEngine; -using System; +using UnityEngine.AI; -public class Gate : MonoBehaviour, IDamageable +/// +/// Gate structure that can be destroyed. +/// Uses HealthComponent for health management. +/// +[RequireComponent(typeof(HealthComponent))] +public class Gate : MonoBehaviour { - [SerializeField] private float maxHealth = 50f; - [SerializeField] private float currentHealth = 50f; - private float CurrentHealth; + private HealthComponent _health; + private NavMeshObstacle _obstacle; - // 체력이 변경될 때 UI 등에 알리기 위한 이벤트 (Observer 패턴) - public static event Action OnHealthChanged; - public static event Action OnGateDestroyed; - - void Awake() => currentHealth = maxHealth; - - public void TakeDamage(float amount) + void Awake() { - currentHealth -= amount; - OnHealthChanged?.Invoke(currentHealth / maxHealth); + _health = GetComponent(); + _obstacle = GetComponent(); - if (currentHealth <= 0) + // Subscribe to health component events + _health.OnDeath += HandleDeath; + } + + void OnDestroy() + { + if (_health != null) { - gameObject.SetActive(false); - var obstacle = GetComponent(); - if(obstacle != null) - { - obstacle.carving = false; - obstacle.enabled = false; - } - OnGateDestroyed?.Invoke(); - Destroy(gameObject, 0.1f); + _health.OnDeath -= HandleDeath; } } -} \ No newline at end of file + + private void HandleDeath() + { + // Disable the gate visually + gameObject.SetActive(false); + + // Disable NavMesh carving so enemies can pass through + if (_obstacle != null) + { + _obstacle.carving = false; + _obstacle.enabled = false; + } + + // Destroy the object after a short delay + Destroy(gameObject, 0.1f); + } + + /// + /// Get the health component for direct access. + /// + public HealthComponent Health => _health; +} diff --git a/Assets/Scripts/GameBase/HealthComponent.cs b/Assets/Scripts/GameBase/HealthComponent.cs new file mode 100644 index 0000000..51f2278 --- /dev/null +++ b/Assets/Scripts/GameBase/HealthComponent.cs @@ -0,0 +1,384 @@ +using System; +using UnityEngine; +using Unity.Netcode; + +/// +/// Reusable health component that can be attached to any entity. +/// Supports both networked (multiplayer) and local-only usage. +/// Implements IDamageable for compatibility with existing damage systems. +/// +public class HealthComponent : NetworkBehaviour, IDamageable +{ + [Header("Health Settings")] + [SerializeField] private float maxHealth = 100f; + [SerializeField] private bool destroyOnDeath = false; + [SerializeField] private float destroyDelay = 0f; + + // Network-synced health for multiplayer + private NetworkVariable _networkHealth = new NetworkVariable( + readPerm: NetworkVariableReadPermission.Everyone, + writePerm: NetworkVariableWritePermission.Server); + + // Local health for non-networked entities or single-player + private float _localHealth; + + // Track if we're in a networked context + private bool _isNetworked; + + #region Events + + /// + /// Fired when health changes. Parameters: (currentHealth, maxHealth) + /// + public event Action OnHealthChanged; + + /// + /// Fired when damage is taken. Parameter: DamageInfo + /// + public event Action OnDamaged; + + /// + /// Fired when healed. Parameter: healAmount + /// + public event Action OnHealed; + + /// + /// Fired when health reaches zero. + /// + public event Action OnDeath; + + /// + /// Fired when health is restored from zero (revived). + /// + public event Action OnRevived; + + #endregion + + #region IDamageable Implementation + + public float CurrentHealth => _isNetworked ? _networkHealth.Value : _localHealth; + public float MaxHealth => maxHealth; + public bool IsAlive => CurrentHealth > 0; + + #endregion + + #region Unity Lifecycle + + private void Awake() + { + _localHealth = maxHealth; + } + + public override void OnNetworkSpawn() + { + _isNetworked = true; + + if (IsServer) + { + _networkHealth.Value = maxHealth; + } + + _networkHealth.OnValueChanged += HandleNetworkHealthChanged; + } + + public override void OnNetworkDespawn() + { + _networkHealth.OnValueChanged -= HandleNetworkHealthChanged; + } + + #endregion + + #region Damage Methods + + /// + /// Simple damage method (backwards compatible). + /// + public void TakeDamage(float amount) + { + TakeDamage(new DamageInfo(amount)); + } + + /// + /// Enhanced damage method with full context. + /// + public void TakeDamage(DamageInfo damageInfo) + { + if (!IsAlive) return; + if (damageInfo.Amount <= 0) return; + + if (_isNetworked) + { + // In networked mode, request damage through server + ApplyDamageServerRpc(damageInfo.Amount, (int)damageInfo.Type); + } + else + { + // Local mode - apply directly + ApplyDamageLocal(damageInfo); + } + } + + [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] + private void ApplyDamageServerRpc(float amount, int damageType) + { + if (!IsAlive) return; + + float newHealth = Mathf.Max(0, _networkHealth.Value - amount); + _networkHealth.Value = newHealth; + + // Notify all clients about the damage + NotifyDamageClientRpc(amount, damageType); + + if (newHealth <= 0) + { + HandleDeathServer(); + } + } + + [ClientRpc] + private void NotifyDamageClientRpc(float amount, int damageType) + { + OnDamaged?.Invoke(new DamageInfo(amount, (DamageType)damageType)); + } + + private void ApplyDamageLocal(DamageInfo info) + { + float previousHealth = _localHealth; + _localHealth = Mathf.Max(0, _localHealth - info.Amount); + + OnHealthChanged?.Invoke(_localHealth, maxHealth); + OnDamaged?.Invoke(info); + + if (_localHealth <= 0 && previousHealth > 0) + { + HandleDeathLocal(); + } + } + + #endregion + + #region Healing Methods + + /// + /// Heal the entity by a specified amount. + /// + public void Heal(float amount) + { + if (amount <= 0) return; + + if (_isNetworked) + { + HealServerRpc(amount); + } + else + { + HealLocal(amount); + } + } + + [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] + private void HealServerRpc(float amount) + { + bool wasAlive = _networkHealth.Value > 0; + float newHealth = Mathf.Min(maxHealth, _networkHealth.Value + amount); + _networkHealth.Value = newHealth; + + NotifyHealClientRpc(amount, !wasAlive && newHealth > 0); + } + + [ClientRpc] + private void NotifyHealClientRpc(float amount, bool revived) + { + OnHealed?.Invoke(amount); + if (revived) + { + OnRevived?.Invoke(); + } + } + + private void HealLocal(float amount) + { + bool wasAlive = _localHealth > 0; + _localHealth = Mathf.Min(maxHealth, _localHealth + amount); + + OnHealthChanged?.Invoke(_localHealth, maxHealth); + OnHealed?.Invoke(amount); + + if (!wasAlive && _localHealth > 0) + { + OnRevived?.Invoke(); + } + } + + /// + /// Fully restore health to maximum. + /// + public void HealToFull() + { + Heal(maxHealth - CurrentHealth); + } + + #endregion + + #region Health Modification + + /// + /// Set the maximum health value. + /// + /// New maximum health + /// If true, also sets current health to the new max + public void SetMaxHealth(float newMax, bool healToMax = false) + { + maxHealth = Mathf.Max(1, newMax); + + if (healToMax) + { + if (_isNetworked && IsServer) + { + _networkHealth.Value = maxHealth; + } + else if (!_isNetworked) + { + _localHealth = maxHealth; + OnHealthChanged?.Invoke(_localHealth, maxHealth); + } + } + else + { + // Clamp current health to new max + if (_isNetworked && IsServer) + { + _networkHealth.Value = Mathf.Min(_networkHealth.Value, maxHealth); + } + else if (!_isNetworked) + { + _localHealth = Mathf.Min(_localHealth, maxHealth); + OnHealthChanged?.Invoke(_localHealth, maxHealth); + } + } + } + + /// + /// Directly set the current health (use with caution). + /// + public void SetHealth(float health) + { + float newHealth = Mathf.Clamp(health, 0, maxHealth); + + if (_isNetworked && IsServer) + { + _networkHealth.Value = newHealth; + } + else if (!_isNetworked) + { + _localHealth = newHealth; + OnHealthChanged?.Invoke(_localHealth, maxHealth); + } + } + + #endregion + + #region Death Handling + + private void HandleDeathServer() + { + NotifyDeathClientRpc(); + + if (destroyOnDeath) + { + if (destroyDelay > 0) + { + // Schedule despawn + Invoke(nameof(DespawnObject), destroyDelay); + } + else + { + DespawnObject(); + } + } + } + + private void DespawnObject() + { + if (TryGetComponent(out var netObj)) + { + netObj.Despawn(); + } + } + + [ClientRpc] + private void NotifyDeathClientRpc() + { + OnDeath?.Invoke(); + } + + private void HandleDeathLocal() + { + OnDeath?.Invoke(); + + if (destroyOnDeath) + { + if (destroyDelay > 0) + { + Destroy(gameObject, destroyDelay); + } + else + { + Destroy(gameObject); + } + } + } + + #endregion + + #region Network Health Change Handler + + private void HandleNetworkHealthChanged(float previousValue, float newValue) + { + OnHealthChanged?.Invoke(newValue, maxHealth); + + // Check for death on clients (server handles it in ApplyDamageServerRpc) + if (previousValue > 0 && newValue <= 0 && !IsServer) + { + // Death event already sent via ClientRpc, just update local state + } + } + + #endregion + + #region Utility Methods + + /// + /// Get health as a normalized value (0-1). + /// + public float GetHealthNormalized() + { + return maxHealth > 0 ? CurrentHealth / maxHealth : 0f; + } + + /// + /// Check if health is below a certain percentage. + /// + public bool IsHealthBelow(float percentage) + { + return GetHealthNormalized() < percentage; + } + + /// + /// Check if at full health. + /// + public bool IsAtFullHealth() + { + return CurrentHealth >= maxHealth; + } + + /// + /// Kill the entity instantly. + /// + public void Kill() + { + TakeDamage(new DamageInfo(CurrentHealth + 1, DamageType.True)); + } + + #endregion +} diff --git a/Assets/Scripts/GameBase/HealthComponent.cs.meta b/Assets/Scripts/GameBase/HealthComponent.cs.meta new file mode 100644 index 0000000..24889a6 --- /dev/null +++ b/Assets/Scripts/GameBase/HealthComponent.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a4b9c07450e6c9c4b8c741b633a2702e \ No newline at end of file diff --git a/Assets/Scripts/GameBase/IDamageable.cs b/Assets/Scripts/GameBase/IDamageable.cs index 2ba6b47..ec97be6 100644 --- a/Assets/Scripts/GameBase/IDamageable.cs +++ b/Assets/Scripts/GameBase/IDamageable.cs @@ -1,5 +1,141 @@ -// IDamageable.cs +using UnityEngine; + +/// +/// Types of damage that can be dealt. +/// Used for damage resistance/vulnerability systems. +/// +public enum DamageType +{ + Physical, + Magical, + Mining, + Environmental, + True // Ignores resistances +} + +/// +/// Contains all information about a damage event. +/// +[System.Serializable] +public struct DamageInfo +{ + /// + /// Amount of damage to deal. + /// + public float Amount; + + /// + /// Type of damage being dealt. + /// + public DamageType Type; + + /// + /// The GameObject that caused the damage (can be null). + /// + public GameObject Source; + + /// + /// World position where the damage was applied. + /// + public Vector3 HitPoint; + + /// + /// Direction the damage came from (for knockback, effects, etc.). + /// + public Vector3 HitDirection; + + /// + /// Create a simple damage info with just an amount. + /// + public DamageInfo(float amount) + { + Amount = amount; + Type = DamageType.Physical; + Source = null; + HitPoint = Vector3.zero; + HitDirection = Vector3.zero; + } + + /// + /// Create damage info with amount and type. + /// + public DamageInfo(float amount, DamageType type) + { + Amount = amount; + Type = type; + Source = null; + HitPoint = Vector3.zero; + HitDirection = Vector3.zero; + } + + /// + /// Create full damage info with all parameters. + /// + public DamageInfo(float amount, DamageType type, GameObject source, + Vector3 hitPoint = default, Vector3 hitDirection = default) + { + Amount = amount; + Type = type; + Source = source; + HitPoint = hitPoint; + HitDirection = hitDirection; + } + + /// + /// Create mining damage. + /// + public static DamageInfo Mining(float amount, GameObject source = null) + { + return new DamageInfo(amount, DamageType.Mining, source); + } + + /// + /// Create physical damage with source and direction. + /// + public static DamageInfo Physical(float amount, GameObject source, Vector3 hitPoint, Vector3 direction) + { + return new DamageInfo(amount, DamageType.Physical, source, hitPoint, direction); + } +} + +/// +/// Interface for any object that can take damage. +/// public interface IDamageable { + /// + /// Simple damage method for backwards compatibility. + /// + /// Amount of damage to deal void TakeDamage(float amount); -} \ No newline at end of file + + /// + /// Enhanced damage method with full context. + /// Default implementation calls the simple TakeDamage for backwards compatibility. + /// + /// Full damage information + void TakeDamage(DamageInfo damageInfo) + { + TakeDamage(damageInfo.Amount); + } + + /// + /// Current health value. + /// + float CurrentHealth { get; } + + /// + /// Maximum health value. + /// + float MaxHealth { get; } + + /// + /// Whether this object is still alive. + /// + bool IsAlive { get; } + + /// + /// Health as a percentage (0-1). + /// + float HealthPercent => MaxHealth > 0 ? CurrentHealth / MaxHealth : 0f; +} diff --git a/Assets/Scripts/GameBase/ItemData.cs b/Assets/Scripts/GameBase/ItemData.cs index 0bdfbc0..80347f4 100644 --- a/Assets/Scripts/GameBase/ItemData.cs +++ b/Assets/Scripts/GameBase/ItemData.cs @@ -1,23 +1,118 @@ using UnityEngine; +/// +/// ScriptableObject defining item properties. +/// Implements IUsableItem and IEquippableItem for extensibility. +/// [CreateAssetMenu(fileName = "New Item", menuName = "Inventory/Item")] -public class ItemData : ScriptableObject +public class ItemData : ScriptableObject, IUsableItem, IEquippableItem { + [Header("Basic Info")] public int itemID; public string itemName; - public Sprite icon; - public float weight; // 아이템 개당 무게 - public int maxStack = 99; // 최대 중첩 개수 + public Sprite icon; + [TextArea] public string description; + + [Header("Stack & Weight")] + public float weight = 1f; + public int maxStack = 99; [Header("Visual Source")] - public GameObject originalBlockPrefab; // 이제 이것만 있으면 됩니다! + [Tooltip("Original prefab for dropped item visuals")] + public GameObject originalBlockPrefab; - [Header("Tool Settings")] - public bool isTool; // 도구 여부 - public PlayerActionData toolAction; // 이 도구를 들었을 때 나갈 액션 (예: MiningActionData) + [Header("Item Behavior")] + [Tooltip("Defines what happens when the item is used")] + public ItemBehavior behavior; - [Header("Visual Settings")] - public GameObject toolPrefab; // 캐릭터 손에 스폰될 3D 프리팹 - public Vector3 equipPositionOffset; // 손 위치 미세 조정 - public Vector3 equipRotationOffset; // 손 회전 미세 조정 -} \ No newline at end of file + [Header("Equipment Settings")] + [Tooltip("Whether this item can be equipped (shows in hand)")] + public bool isEquippable; + [Tooltip("Prefab spawned in player's hand when equipped")] + public GameObject equipmentPrefab; + public Vector3 equipPositionOffset; + public Vector3 equipRotationOffset; + [Tooltip("Name of the transform to attach equipment to")] + public string attachmentPointName = "ToolAnchor"; + + #region IUsableItem Implementation + + public bool IsConsumable => behavior != null && behavior.IsConsumable; + + public bool CanUse(GameObject user, GameObject target) + { + return behavior != null && behavior.CanUse(user, target); + } + + public void Use(GameObject user, GameObject target) + { + if (behavior != null) + { + behavior.Use(user, target); + } + } + + public ActionDescriptor GetUseAction() + { + return behavior?.GetActionDescriptor(); + } + + #endregion + + #region IEquippableItem Implementation + + public GameObject GetEquipmentPrefab() => equipmentPrefab; + public Vector3 GetPositionOffset() => equipPositionOffset; + public Vector3 GetRotationOffset() => equipRotationOffset; + public string GetAttachmentPointName() => attachmentPointName; + + public Transform FindAttachmentPoint(GameObject user) + { + if (string.IsNullOrEmpty(attachmentPointName)) + return user.transform; + + var transforms = user.GetComponentsInChildren(); + foreach (var t in transforms) + { + if (t.name == attachmentPointName) + return t; + } + + return user.transform; + } + + #endregion + + #region Utility Properties + + /// + /// Check if this item has any usable behavior. + /// + public bool HasBehavior => behavior != null; + + /// + /// Check if this item can be equipped. + /// + public bool CanBeEquipped => isEquippable && equipmentPrefab != null; + + #endregion + + #region Utility Methods + + /// + /// Get display name for UI. + /// + public string GetDisplayName() => string.IsNullOrEmpty(itemName) ? name : itemName; + + /// + /// Get description for UI. + /// + public string GetDescription() => description; + + /// + /// Calculate total weight for a stack. + /// + public float GetStackWeight(int count) => weight * count; + + #endregion +} diff --git a/Assets/Scripts/IInteractable.cs b/Assets/Scripts/IInteractable.cs index 3225608..00faf37 100644 --- a/Assets/Scripts/IInteractable.cs +++ b/Assets/Scripts/IInteractable.cs @@ -1,6 +1,172 @@ using UnityEngine; +/// +/// Types of interactions available in the game. +/// +public enum InteractionType +{ + Generic, + Pickup, + Use, + Talk, + Enter, + Build, + Open, + Activate +} + +/// +/// Contains preview information about an interaction for UI display. +/// +[System.Serializable] +public struct InteractionPreview +{ + /// + /// The action verb (e.g., "Pick up", "Enter", "Use"). + /// + public string ActionVerb; + + /// + /// The name of the target object (e.g., "Iron Ore", "Tunnel"). + /// + public string TargetName; + + /// + /// Optional icon to display in UI. + /// + public Sprite Icon; + + /// + /// The type of interaction. + /// + public InteractionType Type; + + /// + /// Whether this interaction requires holding the button. + /// + public bool RequiresHold; + + /// + /// Duration to hold if RequiresHold is true. + /// + public float HoldDuration; + + /// + /// Input hint to display (e.g., "[F]", "[E]"). + /// + public string InputHint; + + /// + /// Create a simple interaction preview. + /// + public static InteractionPreview Simple(string actionVerb, string targetName, + InteractionType type = InteractionType.Generic) + { + return new InteractionPreview + { + ActionVerb = actionVerb, + TargetName = targetName, + Type = type, + RequiresHold = false, + HoldDuration = 0f, + InputHint = "[F]" + }; + } + + /// + /// Create a pickup interaction preview. + /// + public static InteractionPreview Pickup(string itemName, Sprite icon = null) + { + return new InteractionPreview + { + ActionVerb = "Pick up", + TargetName = itemName, + Icon = icon, + Type = InteractionType.Pickup, + RequiresHold = false, + InputHint = "[F]" + }; + } + + /// + /// Create an enter/use interaction preview. + /// + public static InteractionPreview Enter(string targetName) + { + return new InteractionPreview + { + ActionVerb = "Enter", + TargetName = targetName, + Type = InteractionType.Enter, + RequiresHold = false, + InputHint = "[F]" + }; + } + + /// + /// Create a hold-to-interact preview. + /// + public static InteractionPreview Hold(string actionVerb, string targetName, float duration, + InteractionType type = InteractionType.Use) + { + return new InteractionPreview + { + ActionVerb = actionVerb, + TargetName = targetName, + Type = type, + RequiresHold = true, + HoldDuration = duration, + InputHint = "[Hold F]" + }; + } + + /// + /// Get the full display string (e.g., "[F] Pick up Iron Ore"). + /// + public string GetDisplayString() + { + return $"{InputHint} {ActionVerb} {TargetName}"; + } +} + +/// +/// Interface for objects that can be interacted with by the player. +/// public interface IInteractable { + /// + /// Perform the interaction. + /// + /// The GameObject performing the interaction (usually the player) void Interact(GameObject interactor); -} \ No newline at end of file + + /// + /// Get preview information about this interaction for UI display. + /// Default implementation returns a generic preview. + /// + InteractionPreview GetInteractionPreview() + { + return InteractionPreview.Simple("Interact", "Object"); + } + + /// + /// Check if this object can currently be interacted with. + /// Default implementation always returns true. + /// + /// The GameObject attempting to interact + /// True if interaction is possible + bool CanInteract(GameObject interactor) + { + return true; + } + + /// + /// Get the world position of this interactable for distance calculations. + /// Default implementation returns zero vector (override in implementation). + /// + Vector3 GetInteractionPoint() + { + return Vector3.zero; + } +} diff --git a/Assets/Scripts/Items.meta b/Assets/Scripts/Items.meta new file mode 100644 index 0000000..bf03d96 --- /dev/null +++ b/Assets/Scripts/Items.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: afa213ad718d66646ba597d72845441d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Items/IUsableItem.cs b/Assets/Scripts/Items/IUsableItem.cs new file mode 100644 index 0000000..396e4f4 --- /dev/null +++ b/Assets/Scripts/Items/IUsableItem.cs @@ -0,0 +1,63 @@ +using UnityEngine; + +/// +/// Interface for items that can be used/activated by the player. +/// +public interface IUsableItem +{ + /// + /// Check if the item can be used. + /// + /// The GameObject using the item (player) + /// Optional target for the use action + /// True if the item can be used + bool CanUse(GameObject user, GameObject target); + + /// + /// Use the item. + /// + /// The GameObject using the item (player) + /// Optional target for the use action + void Use(GameObject user, GameObject target); + + /// + /// Get the action descriptor for using this item. + /// + ActionDescriptor GetUseAction(); + + /// + /// Whether using this item consumes it (reduces stack count). + /// + bool IsConsumable { get; } +} + +/// +/// Interface for items that can be equipped on the player. +/// +public interface IEquippableItem +{ + /// + /// Get the equipment prefab to spawn. + /// + GameObject GetEquipmentPrefab(); + + /// + /// Get position offset for equipment placement. + /// + Vector3 GetPositionOffset(); + + /// + /// Get rotation offset for equipment placement. + /// + Vector3 GetRotationOffset(); + + /// + /// Get the name of the attachment point on the character. + /// + string GetAttachmentPointName(); + + /// + /// Find the attachment point transform on a user. + /// + Transform FindAttachmentPoint(GameObject user); +} diff --git a/Assets/Scripts/Items/IUsableItem.cs.meta b/Assets/Scripts/Items/IUsableItem.cs.meta new file mode 100644 index 0000000..288f69d --- /dev/null +++ b/Assets/Scripts/Items/IUsableItem.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 686008e086e9d2247b33d5828e0efa5f \ No newline at end of file diff --git a/Assets/Scripts/Items/ItemBehavior.cs b/Assets/Scripts/Items/ItemBehavior.cs new file mode 100644 index 0000000..e4ecd75 --- /dev/null +++ b/Assets/Scripts/Items/ItemBehavior.cs @@ -0,0 +1,67 @@ +using UnityEngine; + +/// +/// Base class for item behaviors. Allows different items to have +/// completely different use effects without modifying ItemData. +/// This is the Strategy pattern for item actions. +/// +public abstract class ItemBehavior : ScriptableObject +{ + [Header("Basic Settings")] + [SerializeField] protected string behaviorName = "Use"; + [SerializeField] protected float duration = 0.5f; + [SerializeField] protected string animTrigger = "Use"; + [SerializeField] protected float animSpeed = 1f; + [SerializeField] protected float impactDelay = 0.2f; + [SerializeField] protected bool canRepeat = false; + + [Header("Effects")] + [SerializeField] protected AudioClip useSound; + [SerializeField] protected GameObject useEffect; + + /// + /// Whether this behavior consumes the item when used. + /// + public virtual bool IsConsumable => false; + + /// + /// Check if this behavior can be used with the given user and target. + /// + /// The player/entity using the item + /// Optional target of the use + /// True if the behavior can be executed + public abstract bool CanUse(GameObject user, GameObject target); + + /// + /// Execute the behavior's effect. + /// + /// The player/entity using the item + /// Optional target of the use + public abstract void Use(GameObject user, GameObject target); + + /// + /// Get the action descriptor for this behavior. + /// + public virtual ActionDescriptor GetActionDescriptor() + { + return new ActionDescriptor + { + ActionName = behaviorName, + Duration = duration, + AnimTrigger = animTrigger, + AnimSpeed = animSpeed, + ImpactDelay = impactDelay, + CanRepeat = canRepeat, + SoundEffect = useSound, + ParticleEffect = useEffect + }; + } + + /// + /// Get a description of why the behavior cannot be used. + /// + public virtual string GetBlockedReason(GameObject user, GameObject target) + { + return null; + } +} \ No newline at end of file diff --git a/Assets/Scripts/Items/ItemBehavior.cs.meta b/Assets/Scripts/Items/ItemBehavior.cs.meta new file mode 100644 index 0000000..3c429ac --- /dev/null +++ b/Assets/Scripts/Items/ItemBehavior.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 73a8d5e271a199f4598ae20f5b20a466 \ No newline at end of file diff --git a/Assets/Scripts/MineableBlock.cs b/Assets/Scripts/MineableBlock.cs index 75668ae..86b78c4 100644 --- a/Assets/Scripts/MineableBlock.cs +++ b/Assets/Scripts/MineableBlock.cs @@ -1,98 +1,131 @@ -using NUnit.Framework.Interfaces; using System.Collections; using Unity.Netcode; using UnityEngine; -public class MineableBlock : NetworkBehaviour +/// +/// A block that can be mined by players. +/// Uses HealthComponent for health management and implements IDamageable. +/// +[RequireComponent(typeof(HealthComponent))] +public class MineableBlock : NetworkBehaviour, IDamageable { - [Header("Block Stats")] - [SerializeField] private int maxHp = 100; - // [동기화] 모든 플레이어가 동일한 블록 체력을 보게 함 - private NetworkVariable _currentHp = new NetworkVariable(); - + [Header("Drop Settings")] [SerializeField] private ItemData dropItemData; - [SerializeField] private GameObject genericDropPrefab; // 여기에 위에서 만든 'GenericDroppedItem' 프리팹을 넣으세요. + [SerializeField] private GameObject genericDropPrefab; [Header("Visuals")] private Outline _outline; private Vector3 _originalPos; [Header("Shake Settings")] - [SerializeField] private float shakeDuration = 0.15f; // 흔들리는 시간 - [SerializeField] private float shakeMagnitude = 0.1f; // 흔들리는 강도 + [SerializeField] private float shakeDuration = 0.15f; + [SerializeField] private float shakeMagnitude = 0.1f; private Coroutine _shakeCoroutine; - private Color _originalColor; // 본래의 색상을 저장할 변수 + private Color _originalColor; private float _lastVisibleTime; private const float VisibilityThreshold = 0.25f; [Header("Fog Settings")] [Range(0f, 1f)] - [SerializeField] private float darkIntensity = 0.2f; // 안개 속에서 얼마나 어두워질지 (0: 완전 검정, 1: 원본) + [SerializeField] private float darkIntensity = 0.2f; private MaterialPropertyBlock _propBlock; private NetworkVariable isDiscovered = new NetworkVariable(false); private MeshRenderer _renderer; + private HealthComponent _health; + + #region IDamageable Implementation + + public float CurrentHealth => _health != null ? _health.CurrentHealth : 0f; + public float MaxHealth => _health != null ? _health.MaxHealth : 0f; + public bool IsAlive => _health != null && _health.IsAlive; + + public void TakeDamage(float amount) + { + TakeDamage(new DamageInfo(amount, DamageType.Mining)); + } + + public void TakeDamage(DamageInfo damageInfo) + { + if (_health != null) + { + _health.TakeDamage(damageInfo); + } + } + + #endregion void Awake() { + _health = GetComponent(); _renderer = GetComponentInChildren(); _propBlock = new MaterialPropertyBlock(); - // 시작 시에는 보이지 않게 설정 + // Start hidden if (_renderer != null) _renderer.enabled = false; - _originalColor = _renderer.sharedMaterial.HasProperty("_BaseColor") + if (_renderer != null && _renderer.sharedMaterial != null) + { + _originalColor = _renderer.sharedMaterial.HasProperty("_BaseColor") ? _renderer.sharedMaterial.GetColor("_BaseColor") : _renderer.sharedMaterial.GetColor("_Color"); + } - _renderer.enabled = false; - - // 해당 오브젝트 혹은 자식에게서 Outline 컴포넌트를 찾습니다. + // Find outline component _outline = GetComponentInChildren(); - _originalPos = transform.localPosition; // 로컬 위치 저장 + _originalPos = transform.localPosition; if (_outline != null) { - // 게임 시작 시 하이라이트는 꺼둡니다. _outline.enabled = false; } - else + + // Subscribe to health events + if (_health != null) { - Debug.LogWarning($"{gameObject.name}: QuickOutline 에셋의 Outline 컴포넌트를 찾을 수 없습니다."); + _health.OnDamaged += HandleDamaged; + _health.OnDeath += HandleDeath; + } + } + + void OnDestroy() + { + if (_health != null) + { + _health.OnDamaged -= HandleDamaged; + _health.OnDeath -= HandleDeath; } } public override void OnNetworkSpawn() { - if (IsServer) - { - _currentHp.Value = maxHp; - } - - // 데이터가 동기화될 때 비주얼 업데이트 + // Update visuals when discovered state syncs UpdateVisuals(isDiscovered.Value); - isDiscovered.OnValueChanged += (oldVal, newVal) => { + isDiscovered.OnValueChanged += (oldVal, newVal) => + { if (newVal) UpdateState(); }; } void Update() { - // 1. 이미 발견된 블록인지는 서버 변수(isDiscovered)로 확인 - // 2. 현재 내 위치가 안개에서 벗어났는지 확인 (매우 단순화된 로직) - if (!isDiscovered.Value) + // Check if block should be discovered based on player distance + if (!isDiscovered.Value && NetworkManager.Singleton != null && + NetworkManager.Singleton.LocalClient != null && + NetworkManager.Singleton.LocalClient.PlayerObject != null) { - float dist = Vector3.Distance(transform.position, NetworkManager.Singleton.LocalClient.PlayerObject.transform.position); - if (dist < FogOfWarManager.Instance.revealRadius) + float dist = Vector3.Distance(transform.position, + NetworkManager.Singleton.LocalClient.PlayerObject.transform.position); + + if (FogOfWarManager.Instance != null && dist < FogOfWarManager.Instance.revealRadius) { - // 서버에 "나 발견됐어!"라고 보고 RequestRevealServerRpc(); } } - // 3. 비주얼 업데이트: 발견된 적이 있을 때만 렌더러를 켬 + // Update renderer visibility if (_renderer != null) { _renderer.enabled = isDiscovered.Value; @@ -109,64 +142,64 @@ public class MineableBlock : NetworkBehaviour private void UpdateState() { - if (_renderer == null) return; + if (_renderer == null || !_renderer.enabled) return; - // 2. 현재 시야 안에 있는지 판단합니다. bool isCurrentlyVisible = (Time.time - _lastVisibleTime) < VisibilityThreshold; - // 3. 상태에 따라 색상과 렌더러 상태를 결정합니다. - if (_renderer.enabled == false) return; - _renderer.GetPropertyBlock(_propBlock); - // 2. 시야 내에 있으면 원본 색상(_originalColor), 멀어지면 어둡게 만든 색상을 적용합니다. Color targetColor = isCurrentlyVisible ? _originalColor : _originalColor * darkIntensity; _propBlock.SetColor("_BaseColor", targetColor); - _renderer.SetPropertyBlock(_propBlock); } - public void RevealBlock() // 서버에서 호출 + /// + /// Reveal this block (called by server). + /// + public void RevealBlock() { - if (IsServer && !isDiscovered.Value) isDiscovered.Value = true; + if (IsServer && !isDiscovered.Value) + { + isDiscovered.Value = true; + } } - // 플레이어가 주변을 훑을 때 호출해줄 함수 + /// + /// Update local visibility for fog of war. + /// public void UpdateLocalVisibility() { _lastVisibleTime = Time.time; } - // 서버에서만 대미지를 처리하도록 제한 - [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] - public void TakeDamageRpc(int damageAmount) + private void HandleDamaged(DamageInfo info) { - if (_currentHp.Value <= 0) return; + // Play hit effect on all clients + PlayHitEffectClientRpc(); + } - _currentHp.Value -= damageAmount; - - if (_currentHp.Value <= 0) + private void HandleDeath() + { + if (IsServer) { - DestroyBlock(); + DropItem(); + GetComponent().Despawn(); } } - private void DestroyBlock() - { - DropItem(); - // 2. 서버에서 네트워크 오브젝트 제거 (모든 클라이언트에서 사라짐) - GetComponent().Despawn(); - } - - // 하이라이트 상태를 설정하는 공개 메서드 + /// + /// Set highlight state for targeting feedback. + /// public void SetHighlight(bool isOn) { - if (_outline == null) return; - - // 외곽선 컴포넌트 활성화/비활성화 - _outline.enabled = isOn; + if (_outline != null) + { + _outline.enabled = isOn; + } } - // 서버에서 호출하여 모든 클라이언트에게 흔들림 지시 + /// + /// Play hit visual effect on all clients. + /// [ClientRpc] public void PlayHitEffectClientRpc() { @@ -181,14 +214,9 @@ public class MineableBlock : NetworkBehaviour { Vector3 randomOffset = Random.insideUnitSphere * shakeMagnitude; transform.localPosition = _originalPos + randomOffset; - - // 좌표가 실제로 바뀌고 있는지 로그 출력 - // Debug.Log($"현재 좌표: {transform.localPosition}"); - elapsed += Time.deltaTime; yield return null; } - transform.localPosition = _originalPos; } @@ -196,13 +224,11 @@ public class MineableBlock : NetworkBehaviour { if (!IsServer || dropItemData == null || genericDropPrefab == null) return; - // 원본 블록이 아니라 '범용 컨테이너'를 소환합니다. GameObject dropObj = Instantiate(genericDropPrefab, transform.position + Vector3.up * 0.5f, Quaternion.identity); NetworkObject netObj = dropObj.GetComponent(); netObj.Spawn(); - // 소환된 컨테이너에 "너는 어떤 아이템의 모양을 따라해야 해"라고 알려줍니다. if (dropObj.TryGetComponent(out var droppedItem)) { droppedItem.Initialize(dropItemData.itemID); @@ -212,7 +238,11 @@ public class MineableBlock : NetworkBehaviour private void UpdateVisuals(bool discovered) { if (_renderer != null) _renderer.enabled = discovered; - // 발견되지 않은 블록은 아웃라인도 표시되지 않아야 함 if (!discovered && _outline != null) _outline.enabled = false; } -} \ No newline at end of file + + /// + /// Get the health component for direct access if needed. + /// + public HealthComponent Health => _health; +} diff --git a/Assets/Scripts/Player/BehaviorActionData.cs b/Assets/Scripts/Player/BehaviorActionData.cs new file mode 100644 index 0000000..d45a36e --- /dev/null +++ b/Assets/Scripts/Player/BehaviorActionData.cs @@ -0,0 +1,20 @@ +using UnityEngine; + +/// +/// Bridge class that wraps ItemBehavior for use with PlayerActionHandler. +/// This allows the new behavior system to work with the existing action handler. +/// +[CreateAssetMenu(menuName = "Actions/Behavior Action")] +public class BehaviorActionData : PlayerActionData +{ + [HideInInspector] + public ItemBehavior behavior; + + public override void ExecuteEffect(GameObject performer, GameObject target) + { + if (behavior != null) + { + behavior.Use(performer, target); + } + } +} diff --git a/Assets/Scripts/Player/BehaviorActionData.cs.meta b/Assets/Scripts/Player/BehaviorActionData.cs.meta new file mode 100644 index 0000000..0f9f88b --- /dev/null +++ b/Assets/Scripts/Player/BehaviorActionData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4a373ecb07ad66848923d4a455b6d236 \ No newline at end of file diff --git a/Assets/Scripts/Player/ConsumableBehavior.cs b/Assets/Scripts/Player/ConsumableBehavior.cs new file mode 100644 index 0000000..21e31a5 --- /dev/null +++ b/Assets/Scripts/Player/ConsumableBehavior.cs @@ -0,0 +1,50 @@ +using UnityEngine; + +/// +/// Consumable behavior for healing items, food, etc. +/// +[CreateAssetMenu(menuName = "Items/Behaviors/Consumable Behavior")] +public class ConsumableBehavior : ItemBehavior +{ + [Header("Consumable Settings")] + [SerializeField] private float healAmount = 20f; + [SerializeField] private float staminaRestore = 0f; + + public override bool IsConsumable => true; + + private void OnEnable() + { + if (string.IsNullOrEmpty(behaviorName)) behaviorName = "Consume"; + if (string.IsNullOrEmpty(animTrigger)) animTrigger = "Consume"; + canRepeat = false; + } + + public override bool CanUse(GameObject user, GameObject target) + { + // Can use if user has a health component that isn't at full health + var health = user.GetComponent(); + if (health == null) return false; + + // Can use if not at full health (healing) or if it restores stamina + return !health.IsAtFullHealth() || staminaRestore > 0; + } + + public override void Use(GameObject user, GameObject target) + { + var health = user.GetComponent(); + if (health != null && healAmount > 0) + { + health.Heal(healAmount); + } + + // Stamina restoration would go here when stamina system is implemented + } + + public override string GetBlockedReason(GameObject user, GameObject target) + { + var health = user.GetComponent(); + if (health == null) return "Cannot use this item"; + if (health.IsAtFullHealth() && staminaRestore <= 0) return "Already at full health"; + return null; + } +} \ No newline at end of file diff --git a/Assets/Scripts/Player/ConsumableBehavior.cs.meta b/Assets/Scripts/Player/ConsumableBehavior.cs.meta new file mode 100644 index 0000000..75bda16 --- /dev/null +++ b/Assets/Scripts/Player/ConsumableBehavior.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e9c4b3ac4b03db34fa97481232baadfe \ No newline at end of file diff --git a/Assets/Scripts/Player/EquipmentSlot.cs b/Assets/Scripts/Player/EquipmentSlot.cs new file mode 100644 index 0000000..2bc70ea --- /dev/null +++ b/Assets/Scripts/Player/EquipmentSlot.cs @@ -0,0 +1,230 @@ +using System; +using UnityEngine; + +/// +/// Types of equipment slots available. +/// +public enum EquipmentSlotType +{ + MainHand, + OffHand, + Head, + Body, + Back, + Accessory +} + +/// +/// Represents a single equipment slot that can hold an equipped item. +/// +[Serializable] +public class EquipmentSlot +{ + /// + /// Type of this equipment slot. + /// + public EquipmentSlotType SlotType; + + /// + /// Transform where equipment is attached. + /// + public Transform AttachPoint; + + /// + /// Currently spawned equipment instance. + /// + [NonSerialized] + public GameObject CurrentEquipment; + + /// + /// Currently equipped item data. + /// + [NonSerialized] + public ItemData EquippedItem; + + /// + /// Event fired when equipment changes. + /// + public event Action OnEquipmentChanged; + + /// + /// Equip an item to this slot. + /// + /// Item to equip (or null to unequip) + public void Equip(ItemData item) + { + // Remove current equipment + Unequip(); + + if (item == null) return; + + // Check if item can be equipped + if (!item.CanBeEquipped) return; + + EquippedItem = item; + + // Spawn equipment visual + var prefab = item.GetEquipmentPrefab(); + if (prefab != null && AttachPoint != null) + { + CurrentEquipment = UnityEngine.Object.Instantiate(prefab, AttachPoint); + CurrentEquipment.transform.localPosition = item.GetPositionOffset(); + CurrentEquipment.transform.localRotation = Quaternion.Euler(item.GetRotationOffset()); + } + + OnEquipmentChanged?.Invoke(this, item); + } + + /// + /// Equip using IEquippableItem interface (more generic). + /// + /// Equippable item + /// The user equipping the item + public void Equip(IEquippableItem equippable, GameObject user) + { + Unequip(); + + if (equippable == null) return; + + var prefab = equippable.GetEquipmentPrefab(); + if (prefab == null) return; + + // Find or use the configured attach point + Transform attachTo = AttachPoint; + if (attachTo == null && user != null) + { + attachTo = equippable.FindAttachmentPoint(user); + } + + if (attachTo != null) + { + CurrentEquipment = UnityEngine.Object.Instantiate(prefab, attachTo); + CurrentEquipment.transform.localPosition = equippable.GetPositionOffset(); + CurrentEquipment.transform.localRotation = Quaternion.Euler(equippable.GetRotationOffset()); + } + + OnEquipmentChanged?.Invoke(this, EquippedItem); + } + + /// + /// Unequip the current item. + /// + public void Unequip() + { + if (CurrentEquipment != null) + { + UnityEngine.Object.Destroy(CurrentEquipment); + CurrentEquipment = null; + } + + var previousItem = EquippedItem; + EquippedItem = null; + + if (previousItem != null) + { + OnEquipmentChanged?.Invoke(this, null); + } + } + + /// + /// Check if this slot has equipment. + /// + public bool HasEquipment => CurrentEquipment != null || EquippedItem != null; + + /// + /// Check if a specific item can be equipped in this slot. + /// + public bool CanEquip(ItemData item) + { + if (item == null) return true; // Can always "unequip" + return item.CanBeEquipped; + } +} + +/// +/// Manages multiple equipment slots for a character. +/// +[Serializable] +public class EquipmentManager +{ + [SerializeField] + private EquipmentSlot[] _slots; + + /// + /// All equipment slots. + /// + public EquipmentSlot[] Slots => _slots; + + /// + /// Get a slot by type. + /// + public EquipmentSlot GetSlot(EquipmentSlotType type) + { + if (_slots == null) return null; + + foreach (var slot in _slots) + { + if (slot.SlotType == type) + return slot; + } + return null; + } + + /// + /// Equip an item to the appropriate slot. + /// + public bool TryEquip(ItemData item, EquipmentSlotType preferredSlot = EquipmentSlotType.MainHand) + { + var slot = GetSlot(preferredSlot); + if (slot != null && slot.CanEquip(item)) + { + slot.Equip(item); + return true; + } + return false; + } + + /// + /// Unequip all slots. + /// + public void UnequipAll() + { + if (_slots == null) return; + + foreach (var slot in _slots) + { + slot.Unequip(); + } + } + + /// + /// Initialize slots with attach points found on the character. + /// + public void Initialize(GameObject character, params (EquipmentSlotType type, string attachPointName)[] slotConfigs) + { + _slots = new EquipmentSlot[slotConfigs.Length]; + + for (int i = 0; i < slotConfigs.Length; i++) + { + var config = slotConfigs[i]; + Transform attachPoint = null; + + // Find attach point + var transforms = character.GetComponentsInChildren(); + foreach (var t in transforms) + { + if (t.name == config.attachPointName) + { + attachPoint = t; + break; + } + } + + _slots[i] = new EquipmentSlot + { + SlotType = config.type, + AttachPoint = attachPoint + }; + } + } +} diff --git a/Assets/Scripts/Player/EquipmentSlot.cs.meta b/Assets/Scripts/Player/EquipmentSlot.cs.meta new file mode 100644 index 0000000..682b023 --- /dev/null +++ b/Assets/Scripts/Player/EquipmentSlot.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b2bee3e86fbe00446b94cf38066b8a81 \ No newline at end of file diff --git a/Assets/Scripts/Player/IActionProvider.cs b/Assets/Scripts/Player/IActionProvider.cs index 316c0e3..5cf47b0 100644 --- a/Assets/Scripts/Player/IActionProvider.cs +++ b/Assets/Scripts/Player/IActionProvider.cs @@ -1,13 +1,171 @@ +using System.Collections.Generic; +using UnityEngine; + +/// +/// Requirement for performing an action (item cost, etc.). +/// +[System.Serializable] +public struct ActionRequirement +{ + /// + /// Item ID required (use -1 if no item required). + /// + public int ItemID; + + /// + /// Amount of the item required. + /// + public int Amount; + + /// + /// Whether the item is consumed when the action is performed. + /// + public bool ConsumeOnUse; + + public ActionRequirement(int itemID, int amount, bool consumeOnUse = true) + { + ItemID = itemID; + Amount = amount; + ConsumeOnUse = consumeOnUse; + } + + /// + /// No requirement. + /// + public static ActionRequirement None => new ActionRequirement(-1, 0, false); +} + +/// +/// Describes an action that can be performed. +/// [System.Serializable] public class ActionDescriptor { - public float duration = 0.5f; - public string animTrigger = "Interact"; - // 필요하다면 여기에 사운드 이펙트나 파티클 정보를 추가할 수 있습니다. + /// + /// Display name of the action. + /// + public string ActionName = "Action"; + + /// + /// Total duration of the action in seconds. + /// + public float Duration = 0.5f; + + /// + /// Animation trigger name. + /// + public string AnimTrigger = "Interact"; + + /// + /// Animation playback speed multiplier. + /// + public float AnimSpeed = 1f; + + /// + /// Time within the animation when the effect occurs (for syncing hit with animation). + /// + public float ImpactDelay = 0f; + + /// + /// Sound effect to play. + /// + public AudioClip SoundEffect; + + /// + /// Particle effect prefab to spawn. + /// + public GameObject ParticleEffect; + + /// + /// Stamina cost to perform this action. + /// + public float StaminaCost = 0f; + + /// + /// Item requirements for this action. + /// + public ActionRequirement[] ItemRequirements; + + /// + /// Whether this action can be repeated by holding the button. + /// + public bool CanRepeat = false; + + /// + /// Cooldown time before this action can be performed again. + /// + public float Cooldown = 0f; + + /// + /// Create a simple action descriptor. + /// + public static ActionDescriptor Simple(string name, float duration, string animTrigger = "Interact") + { + return new ActionDescriptor + { + ActionName = name, + Duration = duration, + AnimTrigger = animTrigger + }; + } + + /// + /// Create an action descriptor for repeatable actions (like mining). + /// + public static ActionDescriptor Repeatable(string name, float duration, string animTrigger, + float impactDelay, float animSpeed = 1f) + { + return new ActionDescriptor + { + ActionName = name, + Duration = duration, + AnimTrigger = animTrigger, + AnimSpeed = animSpeed, + ImpactDelay = impactDelay, + CanRepeat = true + }; + } } -// 명세를 제공하는 인터페이스 +/// +/// Interface for objects that can provide action descriptors. +/// Implement this to define what actions can be performed on or with an object. +/// public interface IActionProvider { + /// + /// Get the primary action descriptor for this provider. + /// ActionDescriptor GetActionDescriptor(); -} \ No newline at end of file + + /// + /// Get all available actions from this provider. + /// Default implementation returns only the primary action. + /// + IEnumerable GetAvailableActions() + { + yield return GetActionDescriptor(); + } + + /// + /// Check if a specific action can be performed. + /// + /// The GameObject attempting the action + /// The action to check + /// True if the action can be performed + bool CanPerformAction(GameObject performer, ActionDescriptor action) + { + return true; + } + + /// + /// Get the reason why an action cannot be performed. + /// + /// The GameObject attempting the action + /// The action to check + /// Reason string, or null if action can be performed + string GetActionBlockedReason(GameObject performer, ActionDescriptor action) + { + return null; + } +} diff --git a/Assets/Scripts/Player/MiningActionData.cs b/Assets/Scripts/Player/MiningActionData.cs index a8b102f..bdc3e5e 100644 --- a/Assets/Scripts/Player/MiningActionData.cs +++ b/Assets/Scripts/Player/MiningActionData.cs @@ -7,13 +7,12 @@ public class MiningActionData : PlayerActionData public override void ExecuteEffect(GameObject performer, GameObject target) { - if(target == null) return; + if (target == null) return; - if (target.TryGetComponent(out var block)) + // Use IDamageable interface for all damageable objects + if (target.TryGetComponent(out var damageable)) { - // 서버 RPC 호출은 블록 내부의 로직을 그대로 사용합니다. - block.TakeDamageRpc(damage); - block.PlayHitEffectClientRpc(); + damageable.TakeDamage(new DamageInfo(damage, DamageType.Mining, performer)); } } -} \ No newline at end of file +} diff --git a/Assets/Scripts/Player/MiningBehavior.cs b/Assets/Scripts/Player/MiningBehavior.cs new file mode 100644 index 0000000..7503cd0 --- /dev/null +++ b/Assets/Scripts/Player/MiningBehavior.cs @@ -0,0 +1,44 @@ +using UnityEngine; + +/// +/// Mining behavior for pickaxes and similar tools. +/// +[CreateAssetMenu(menuName = "Items/Behaviors/Mining Behavior")] +public class MiningBehavior : ItemBehavior +{ + [Header("Mining Settings")] + [SerializeField] private int damage = 50; + + private void OnEnable() + { + // Set default mining values + if (string.IsNullOrEmpty(behaviorName)) behaviorName = "Mine"; + if (string.IsNullOrEmpty(animTrigger)) animTrigger = "Attack"; + canRepeat = true; + } + + public override bool CanUse(GameObject user, GameObject target) + { + // Can always swing, but only deals damage if hitting a mineable block + return true; + } + + public override void Use(GameObject user, GameObject target) + { + if (target == null) return; + + // Use IDamageable interface for all damageable objects + if (target.TryGetComponent(out var damageable)) + { + damageable.TakeDamage(new DamageInfo(damage, DamageType.Mining, user)); + } + } + + public override string GetBlockedReason(GameObject user, GameObject target) + { + if (target == null) return "No target"; + if (!target.TryGetComponent(out _)) + return "Cannot mine this object"; + return null; + } +} diff --git a/Assets/Scripts/Player/MiningBehavior.cs.meta b/Assets/Scripts/Player/MiningBehavior.cs.meta new file mode 100644 index 0000000..454b9d1 --- /dev/null +++ b/Assets/Scripts/Player/MiningBehavior.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c1deeb9de56edff4ca77ddabf9db691a \ No newline at end of file diff --git a/Assets/Scripts/Player/PlaceableBehavior.cs b/Assets/Scripts/Player/PlaceableBehavior.cs new file mode 100644 index 0000000..cd262cd --- /dev/null +++ b/Assets/Scripts/Player/PlaceableBehavior.cs @@ -0,0 +1,41 @@ +using UnityEngine; + +/// +/// Placeable behavior for building/placing items. +/// +[CreateAssetMenu(menuName = "Items/Behaviors/Placeable Behavior")] +public class PlaceableBehavior : ItemBehavior +{ + [Header("Placement Settings")] + [SerializeField] private GameObject placeablePrefab; + [SerializeField] private bool requiresGround = true; + [SerializeField] private float placementRange = 5f; + + public override bool IsConsumable => true; + + private void OnEnable() + { + if (string.IsNullOrEmpty(behaviorName)) behaviorName = "Place"; + if (string.IsNullOrEmpty(animTrigger)) animTrigger = "Place"; + canRepeat = false; + } + + public override bool CanUse(GameObject user, GameObject target) + { + // Would integrate with BuildManager for placement validation + return placeablePrefab != null; + } + + public override void Use(GameObject user, GameObject target) + { + // Actual placement would be handled by BuildManager + // This is a placeholder for the behavior pattern + Debug.Log($"[PlaceableBehavior] Would place {placeablePrefab?.name}"); + } + + public override string GetBlockedReason(GameObject user, GameObject target) + { + if (placeablePrefab == null) return "Invalid placement item"; + return null; + } +} \ No newline at end of file diff --git a/Assets/Scripts/Player/PlaceableBehavior.cs.meta b/Assets/Scripts/Player/PlaceableBehavior.cs.meta new file mode 100644 index 0000000..af21653 --- /dev/null +++ b/Assets/Scripts/Player/PlaceableBehavior.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b4962abe690c6ef47b7ea654ce747200 \ No newline at end of file diff --git a/Assets/Scripts/Player/PlayerActionHandler.cs b/Assets/Scripts/Player/PlayerActionHandler.cs index 35ef0ad..28495fd 100644 --- a/Assets/Scripts/Player/PlayerActionHandler.cs +++ b/Assets/Scripts/Player/PlayerActionHandler.cs @@ -33,11 +33,11 @@ public class PlayerActionHandler : NetworkBehaviour private IEnumerator InteractionRoutine(ActionDescriptor desc, IInteractable target) { _isBusy = true; - if (desc != null) _animator.SetTrigger(desc.animTrigger); + if (desc != null) _animator.SetTrigger(desc.AnimTrigger); target.Interact(gameObject); // 로직 실행 - yield return new WaitForSeconds(desc?.duration ?? 0.1f); + yield return new WaitForSeconds(desc?.Duration ?? 0.1f); _isBusy = false; } diff --git a/Assets/Scripts/Player/PlayerEquipmentHandler.cs b/Assets/Scripts/Player/PlayerEquipmentHandler.cs index 41b0361..3cf95f1 100644 --- a/Assets/Scripts/Player/PlayerEquipmentHandler.cs +++ b/Assets/Scripts/Player/PlayerEquipmentHandler.cs @@ -1,25 +1,63 @@ using Unity.Netcode; using UnityEngine; +/// +/// Handles equipment visuals for the player. +/// Uses the new EquipmentSlot system while maintaining backwards compatibility. +/// public class PlayerEquipmentHandler : NetworkBehaviour { - [SerializeField] private Transform toolAnchor; // 캐릭터 손의 소켓 위치 + [Header("Equipment Settings")] + [SerializeField] private Transform mainHandAnchor; + [SerializeField] private Transform offHandAnchor; + + [Header("Slot Configuration")] + [SerializeField] private EquipmentSlot mainHandSlot; + private PlayerInventory _inventory; - private GameObject _currentToolInstance; // 현재 생성된 도구 모델 + private GameObject _currentToolInstance; void Awake() { _inventory = GetComponent(); + + // Initialize main hand slot if not configured in inspector + if (mainHandSlot == null) + { + mainHandSlot = new EquipmentSlot + { + SlotType = EquipmentSlotType.MainHand, + AttachPoint = mainHandAnchor + }; + } + else if (mainHandSlot.AttachPoint == null) + { + mainHandSlot.AttachPoint = mainHandAnchor; + } } public override void OnNetworkSpawn() { - // 인벤토리의 슬롯 변경 이벤트 구독 - // OnSlotChanged는 (이전 값, 새 값) 두 개의 인자를 전달합니다. - _inventory.OnSlotChanged += HandleSlotChanged; + // Subscribe to inventory slot changes + if (_inventory != null) + { + _inventory.OnSlotChanged += HandleSlotChanged; - // 게임 시작 시 처음에 들고 있는 아이템 모델 생성 - UpdateEquippedModel(_inventory.SelectedSlotIndex); + // Initialize with current slot + UpdateEquippedModel(_inventory.SelectedSlotIndex); + } + } + + public override void OnNetworkDespawn() + { + // Unsubscribe to prevent memory leaks + if (_inventory != null) + { + _inventory.OnSlotChanged -= HandleSlotChanged; + } + + // Clean up equipment + mainHandSlot?.Unequip(); } private void HandleSlotChanged(int previousValue, int newValue) @@ -29,30 +67,58 @@ public class PlayerEquipmentHandler : NetworkBehaviour private void UpdateEquippedModel(int slotIndex) { - // 1. 기존 도구가 있다면 파괴 - if (_currentToolInstance != null) + // Get item data for the selected slot + ItemData data = _inventory?.GetItemDataInSlot(slotIndex); + + // Use new equipment slot system + if (data != null && data.CanBeEquipped) { - Destroy(_currentToolInstance); + // Use IEquippableItem interface + mainHandSlot.Equip(data); + } + else + { + mainHandSlot.Unequip(); } - // 2. 현재 선택된 슬롯의 데이터 확인 - ItemData data = _inventory.GetItemDataInSlot(slotIndex); - - // 3. 도구인 경우에만 모델 생성 - if (data != null && data.isTool && data.toolPrefab != null) - { - _currentToolInstance = Instantiate(data.toolPrefab, toolAnchor); - - // ItemData에 설정된 오프셋 적용 - _currentToolInstance.transform.localPosition = data.equipPositionOffset; - _currentToolInstance.transform.localRotation = Quaternion.Euler(data.equipRotationOffset); - } + // Update legacy reference for any code that might check it + _currentToolInstance = mainHandSlot.CurrentEquipment; } - public override void OnNetworkDespawn() + /// + /// Get the currently equipped item data. + /// + public ItemData GetEquippedItem() { - // 이벤트 구독 해제 (메모리 누수 방지) - if (_inventory != null) - _inventory.OnSlotChanged -= HandleSlotChanged; + return mainHandSlot?.EquippedItem; } -} \ No newline at end of file + + /// + /// Get the currently equipped tool instance. + /// + public GameObject GetCurrentToolInstance() + { + return mainHandSlot?.CurrentEquipment ?? _currentToolInstance; + } + + /// + /// Check if player has equipment in main hand. + /// + public bool HasMainHandEquipment => mainHandSlot?.HasEquipment ?? false; + + /// + /// Force refresh the equipped model. + /// + public void RefreshEquipment() + { + if (_inventory != null) + { + UpdateEquippedModel(_inventory.SelectedSlotIndex); + } + } + + /// + /// Get the main hand equipment slot for advanced usage. + /// + public EquipmentSlot MainHandSlot => mainHandSlot; +} diff --git a/Assets/Scripts/Player/PlayerNetworkController.cs b/Assets/Scripts/Player/PlayerNetworkController.cs index 86747eb..d6bc06c 100644 --- a/Assets/Scripts/Player/PlayerNetworkController.cs +++ b/Assets/Scripts/Player/PlayerNetworkController.cs @@ -204,35 +204,50 @@ public class PlayerNetworkController : NetworkBehaviour if (_isGrounded) _velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity); } - // 1. 액션 (좌클릭) - 대상이 없어도 나감 - // PlayerNetworkController.cs 중 일부 + // 1. Action (Left Click) - executes even without target private void OnActionInput() { if (!IsOwner || _actionHandler.IsBusy) return; ItemData selectedItem = _inventory.GetSelectedItemData(); + if (selectedItem == null) return; - // 로그 1: 아이템 확인 - if (selectedItem == null) { Debug.Log("선택된 아이템이 없음"); return; } - - // 로그 2: 도구 여부 및 액션 데이터 확인 - Debug.Log($"현재 아이템: {selectedItem.itemName}, 도구여부: {selectedItem.isTool}, 액션데이터: {selectedItem.toolAction != null}"); - - if (selectedItem.isTool && selectedItem.toolAction != null) + // Check if item has behavior (new system) + if (selectedItem.behavior != null) { - if (_lastHighlightedBlock != null) + GameObject target = _lastHighlightedBlock != null ? _lastHighlightedBlock.gameObject : null; + + // Use the new behavior system + if (selectedItem.CanUse(gameObject, target)) { - Debug.Log($"채광 시작: {_lastHighlightedBlock.name}"); - _actionHandler.PerformAction(selectedItem.toolAction, _lastHighlightedBlock.gameObject); - } - else - { - Debug.Log("조준된 블록이 없음 (하이라이트 확인 필요)"); - _actionHandler.PerformAction(selectedItem.toolAction, null); + // Get action descriptor and perform action + var actionDesc = selectedItem.GetUseAction(); + if (actionDesc != null) + { + _actionHandler.PerformAction( + CreateActionDataFromDescriptor(actionDesc, selectedItem.behavior), + target + ); + } } } } + // Helper to bridge between new ActionDescriptor and legacy PlayerActionData + private PlayerActionData CreateActionDataFromDescriptor(ActionDescriptor desc, ItemBehavior behavior) + { + // Create a temporary runtime action data + var actionData = ScriptableObject.CreateInstance(); + actionData.actionName = desc.ActionName; + actionData.duration = desc.Duration; + actionData.animTrigger = desc.AnimTrigger; + actionData.impactDelay = desc.ImpactDelay; + actionData.baseSpeed = desc.AnimSpeed; + actionData.canRepeat = desc.CanRepeat; + actionData.behavior = behavior; + return actionData; + } + // 2. 인터랙션 (F키) - 대상이 없으면 아예 시작 안 함 private void OnInteractTap() { @@ -250,13 +265,13 @@ public class PlayerNetworkController : NetworkBehaviour { if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(targetId, out var target)) { - if (target.TryGetComponent(out var block)) + // Use IDamageable interface instead of MineableBlock directly + if (target.TryGetComponent(out var damageable)) { - // 서버에서 최종 거리 검증 후 대미지 적용 + // Server-side distance validation before applying damage if (Vector3.Distance(transform.position, target.transform.position) <= attackRange + 1.0f) { - block.TakeDamageRpc(miningDamage); - block.PlayHitEffectClientRpc(); + damageable.TakeDamage(new DamageInfo(miningDamage, DamageType.Mining, gameObject)); } } } @@ -487,10 +502,11 @@ public class PlayerNetworkController : NetworkBehaviour private void HandleContinuousAction() { ItemData selectedItem = _inventory.GetSelectedItemData(); - if (selectedItem == null || !selectedItem.isTool || selectedItem.toolAction == null) return; + if (selectedItem == null || selectedItem.behavior == null) return; - // [핵심] 반복 가능한 액션일 때만 Update에서 재실행 - if (selectedItem.toolAction.canRepeat) + // Only repeat if action supports it + var actionDesc = selectedItem.GetUseAction(); + if (actionDesc != null && actionDesc.CanRepeat) { TryExecuteAction(); } @@ -501,15 +517,23 @@ public class PlayerNetworkController : NetworkBehaviour if (_actionHandler.IsBusy) return; ItemData selectedItem = _inventory.GetSelectedItemData(); - if (selectedItem != null && selectedItem.isTool && selectedItem.toolAction != null) + if (selectedItem == null || selectedItem.behavior == null) return; + + var actionDesc = selectedItem.GetUseAction(); + if (actionDesc == null) return; + + // Skip if non-repeatable action already executed once + if (!actionDesc.CanRepeat && _hasExecutedOnce) return; + + GameObject target = _lastHighlightedBlock != null ? _lastHighlightedBlock.gameObject : null; + + if (selectedItem.CanUse(gameObject, target)) { - // 단발성 액션인데 이미 한 번 실행했다면 스킵 - if (!selectedItem.toolAction.canRepeat && _hasExecutedOnce) return; - - GameObject target = _lastHighlightedBlock != null ? _lastHighlightedBlock.gameObject : null; - _actionHandler.PerformAction(selectedItem.toolAction, target); - - _hasExecutedOnce = true; // 실행 기록 저장 + _actionHandler.PerformAction( + CreateActionDataFromDescriptor(actionDesc, selectedItem.behavior), + target + ); + _hasExecutedOnce = true; } } diff --git a/Assets/Scripts/Utilities.meta b/Assets/Scripts/Utilities.meta new file mode 100644 index 0000000..610ebfd --- /dev/null +++ b/Assets/Scripts/Utilities.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d5b5e4be41b488d4cb306af7c3175e94 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Utilities/ActionExecutor.cs b/Assets/Scripts/Utilities/ActionExecutor.cs new file mode 100644 index 0000000..562178e --- /dev/null +++ b/Assets/Scripts/Utilities/ActionExecutor.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections; +using UnityEngine; + +/// +/// Reusable action execution system with busy state management. +/// Replaces repeated coroutine busy-state patterns across the codebase. +/// +public class ActionExecutor : MonoBehaviour +{ + public bool IsBusy { get; private set; } + + private Coroutine _currentAction; + + /// + /// Event fired when an action starts. + /// + public event Action OnActionStarted; + + /// + /// Event fired when an action completes. + /// + public event Action OnActionCompleted; + + /// + /// Event fired when an action is cancelled. + /// + public event Action OnActionCancelled; + + /// + /// Try to execute an action. Returns false if already busy. + /// + /// The action request to execute + /// True if action started, false if busy + public bool TryExecute(ActionRequest request) + { + if (IsBusy) return false; + + _currentAction = StartCoroutine(ExecuteRoutine(request)); + return true; + } + + /// + /// Cancel the current action if one is running. + /// + public void Cancel() + { + if (_currentAction != null) + { + StopCoroutine(_currentAction); + _currentAction = null; + IsBusy = false; + OnActionCancelled?.Invoke(); + } + } + + /// + /// Force reset the busy state. Use with caution. + /// + public void ForceReset() + { + if (_currentAction != null) + { + StopCoroutine(_currentAction); + _currentAction = null; + } + IsBusy = false; + } + + private IEnumerator ExecuteRoutine(ActionRequest request) + { + IsBusy = true; + OnActionStarted?.Invoke(); + + // Pre-action callback + request.OnStart?.Invoke(); + + // Animation trigger + if (request.Animator != null && !string.IsNullOrEmpty(request.AnimTrigger)) + { + if (request.AnimSpeed > 0) + { + request.Animator.SetFloat("ActionSpeed", request.AnimSpeed); + } + request.Animator.SetTrigger(request.AnimTrigger); + } + + // Wait for impact point + if (request.ImpactDelay > 0) + { + float adjustedDelay = request.AnimSpeed > 0 + ? request.ImpactDelay / request.AnimSpeed + : request.ImpactDelay; + yield return new WaitForSeconds(adjustedDelay); + } + + // Execute main effect + request.OnImpact?.Invoke(); + + // Wait for remaining duration + float remainingTime = request.TotalDuration - request.ImpactDelay; + if (request.AnimSpeed > 0) + { + remainingTime /= request.AnimSpeed; + } + + if (remainingTime > 0) + { + yield return new WaitForSeconds(remainingTime); + } + + // Completion callback + request.OnComplete?.Invoke(); + + IsBusy = false; + _currentAction = null; + OnActionCompleted?.Invoke(); + } +} + +/// +/// Configuration for an action to be executed by ActionExecutor. +/// +[Serializable] +public struct ActionRequest +{ + /// + /// Animator to trigger animations on. + /// + public Animator Animator; + + /// + /// Animation trigger name. + /// + public string AnimTrigger; + + /// + /// Animation playback speed multiplier. + /// + public float AnimSpeed; + + /// + /// Time delay before the impact/effect happens (for syncing with animation). + /// + public float ImpactDelay; + + /// + /// Total duration of the action. + /// + public float TotalDuration; + + /// + /// Callback invoked when action starts. + /// + public Action OnStart; + + /// + /// Callback invoked at the impact moment. + /// + public Action OnImpact; + + /// + /// Callback invoked when action completes. + /// + public Action OnComplete; + + /// + /// Create a simple action request with just timing. + /// + public static ActionRequest Simple(float duration, Action onComplete) + { + return new ActionRequest + { + TotalDuration = duration, + AnimSpeed = 1f, + OnComplete = onComplete + }; + } + + /// + /// Create an animated action request. + /// + public static ActionRequest Animated(Animator animator, string trigger, float duration, + float impactDelay, Action onImpact, float speed = 1f) + { + return new ActionRequest + { + Animator = animator, + AnimTrigger = trigger, + AnimSpeed = speed, + ImpactDelay = impactDelay, + TotalDuration = duration, + OnImpact = onImpact + }; + } + + /// + /// Create a full action request with all callbacks. + /// + public static ActionRequest Full(Animator animator, string trigger, float duration, + float impactDelay, float speed, Action onStart, Action onImpact, Action onComplete) + { + return new ActionRequest + { + Animator = animator, + AnimTrigger = trigger, + AnimSpeed = speed, + ImpactDelay = impactDelay, + TotalDuration = duration, + OnStart = onStart, + OnImpact = onImpact, + OnComplete = onComplete + }; + } +} diff --git a/Assets/Scripts/Utilities/ActionExecutor.cs.meta b/Assets/Scripts/Utilities/ActionExecutor.cs.meta new file mode 100644 index 0000000..e6d3fad --- /dev/null +++ b/Assets/Scripts/Utilities/ActionExecutor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 27e07922480ecf44faf5135ad4872531 \ No newline at end of file diff --git a/Assets/Scripts/Utilities/PhysicsQueryUtility.cs b/Assets/Scripts/Utilities/PhysicsQueryUtility.cs new file mode 100644 index 0000000..e4cbb9d --- /dev/null +++ b/Assets/Scripts/Utilities/PhysicsQueryUtility.cs @@ -0,0 +1,228 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +/// +/// Utility class for common physics queries. +/// Eliminates repeated OverlapSphere patterns across the codebase. +/// +public static class PhysicsQueryUtility +{ + // Reusable buffer to avoid allocations (32 should be enough for most cases) + private static readonly Collider[] OverlapBuffer = new Collider[32]; + + /// + /// Find the closest object implementing interface T within radius. + /// + /// Interface or component type to search for + /// Center point of the search + /// Search radius + /// Layer mask to filter objects + /// Closest object of type T, or null if none found + public static T FindClosest(Vector3 origin, float radius, LayerMask layerMask) where T : class + { + int count = Physics.OverlapSphereNonAlloc(origin, radius, OverlapBuffer, layerMask); + + T closest = null; + float minDistSqr = float.MaxValue; + + for (int i = 0; i < count; i++) + { + T component = OverlapBuffer[i].GetComponentInParent(); + if (component != null) + { + float distSqr = (origin - OverlapBuffer[i].transform.position).sqrMagnitude; + if (distSqr < minDistSqr) + { + minDistSqr = distSqr; + closest = component; + } + } + } + + return closest; + } + + /// + /// Find the closest object implementing interface T within radius, also returning distance. + /// + public static T FindClosest(Vector3 origin, float radius, LayerMask layerMask, out float distance) where T : class + { + int count = Physics.OverlapSphereNonAlloc(origin, radius, OverlapBuffer, layerMask); + + T closest = null; + float minDistSqr = float.MaxValue; + + for (int i = 0; i < count; i++) + { + T component = OverlapBuffer[i].GetComponentInParent(); + if (component != null) + { + float distSqr = (origin - OverlapBuffer[i].transform.position).sqrMagnitude; + if (distSqr < minDistSqr) + { + minDistSqr = distSqr; + closest = component; + } + } + } + + distance = closest != null ? Mathf.Sqrt(minDistSqr) : 0f; + return closest; + } + + /// + /// Execute action on all objects of type T within radius. + /// + /// Interface or component type to search for + /// Center point of the search + /// Search radius + /// Layer mask to filter objects + /// Action to execute on each found object + public static void ForEachInRadius(Vector3 origin, float radius, LayerMask layerMask, Action action) where T : class + { + int count = Physics.OverlapSphereNonAlloc(origin, radius, OverlapBuffer, layerMask); + + // Track processed objects to avoid duplicates (same object hit by multiple colliders) + var processed = new HashSet(); + + for (int i = 0; i < count; i++) + { + T component = OverlapBuffer[i].GetComponentInParent(); + if (component != null && !processed.Contains(component)) + { + processed.Add(component); + action(component); + } + } + } + + /// + /// Execute action on all objects of type T within radius, with additional context. + /// + public static void ForEachInRadius(Vector3 origin, float radius, LayerMask layerMask, + Action action) where T : class + { + int count = Physics.OverlapSphereNonAlloc(origin, radius, OverlapBuffer, layerMask); + + var processed = new HashSet(); + + for (int i = 0; i < count; i++) + { + T component = OverlapBuffer[i].GetComponentInParent(); + if (component != null && !processed.Contains(component)) + { + processed.Add(component); + action(component, OverlapBuffer[i]); + } + } + } + + /// + /// Get all objects of type T within radius. + /// + /// Interface or component type to search for + /// Center point of the search + /// Search radius + /// Layer mask to filter objects + /// List of all objects of type T within radius + public static List FindAllInRadius(Vector3 origin, float radius, LayerMask layerMask) where T : class + { + var results = new List(); + int count = Physics.OverlapSphereNonAlloc(origin, radius, OverlapBuffer, layerMask); + + for (int i = 0; i < count; i++) + { + T component = OverlapBuffer[i].GetComponentInParent(); + if (component != null && !results.Contains(component)) + { + results.Add(component); + } + } + + return results; + } + + /// + /// Find all objects of type T within radius, sorted by distance (closest first). + /// + public static List FindAllInRadiusSorted(Vector3 origin, float radius, LayerMask layerMask) where T : class + { + int count = Physics.OverlapSphereNonAlloc(origin, radius, OverlapBuffer, layerMask); + + var resultsWithDistance = new List<(T component, float distSqr)>(); + + for (int i = 0; i < count; i++) + { + T component = OverlapBuffer[i].GetComponentInParent(); + if (component != null) + { + bool alreadyAdded = false; + foreach (var item in resultsWithDistance) + { + if (ReferenceEquals(item.component, component)) + { + alreadyAdded = true; + break; + } + } + + if (!alreadyAdded) + { + float distSqr = (origin - OverlapBuffer[i].transform.position).sqrMagnitude; + resultsWithDistance.Add((component, distSqr)); + } + } + } + + // Sort by distance + resultsWithDistance.Sort((a, b) => a.distSqr.CompareTo(b.distSqr)); + + var results = new List(resultsWithDistance.Count); + foreach (var item in resultsWithDistance) + { + results.Add(item.component); + } + + return results; + } + + /// + /// Check if any object of type T exists within radius. + /// + public static bool AnyInRadius(Vector3 origin, float radius, LayerMask layerMask) where T : class + { + int count = Physics.OverlapSphereNonAlloc(origin, radius, OverlapBuffer, layerMask); + + for (int i = 0; i < count; i++) + { + if (OverlapBuffer[i].GetComponentInParent() != null) + { + return true; + } + } + + return false; + } + + /// + /// Count objects of type T within radius. + /// + public static int CountInRadius(Vector3 origin, float radius, LayerMask layerMask) where T : class + { + int count = Physics.OverlapSphereNonAlloc(origin, radius, OverlapBuffer, layerMask); + + var processed = new HashSet(); + + for (int i = 0; i < count; i++) + { + T component = OverlapBuffer[i].GetComponentInParent(); + if (component != null) + { + processed.Add(component); + } + } + + return processed.Count; + } +} diff --git a/Assets/Scripts/Utilities/PhysicsQueryUtility.cs.meta b/Assets/Scripts/Utilities/PhysicsQueryUtility.cs.meta new file mode 100644 index 0000000..d5b5a0b --- /dev/null +++ b/Assets/Scripts/Utilities/PhysicsQueryUtility.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e1262d37bb47b7d4ea78d6426675ae9d \ No newline at end of file