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