diff --git a/Assets/DefaultNetworkPrefabs.asset b/Assets/DefaultNetworkPrefabs.asset index c2c2ae0..54dfd26 100644 --- a/Assets/DefaultNetworkPrefabs.asset +++ b/Assets/DefaultNetworkPrefabs.asset @@ -99,3 +99,13 @@ MonoBehaviour: SourcePrefabToOverride: {fileID: 0} SourceHashToOverride: 0 OverridingTargetPrefab: {fileID: 0} + - Override: 0 + Prefab: {fileID: 5063655529130579611, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3} + SourcePrefabToOverride: {fileID: 0} + SourceHashToOverride: 0 + OverridingTargetPrefab: {fileID: 0} + - Override: 0 + Prefab: {fileID: 5156816586410934540, guid: 29ad9c03b79f43f42859005ce707dff2, type: 3} + SourcePrefabToOverride: {fileID: 0} + SourceHashToOverride: 0 + OverridingTargetPrefab: {fileID: 0} diff --git a/Assets/Prefabs/MineableChunk.prefab b/Assets/Prefabs/MineableChunk.prefab new file mode 100644 index 0000000..a760d6f --- /dev/null +++ b/Assets/Prefabs/MineableChunk.prefab @@ -0,0 +1,167 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &5063655529130579611 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3520563273759020846} + - component: {fileID: 7358912368512722974} + - component: {fileID: 3942884241032403340} + - component: {fileID: 6314136871395425781} + - component: {fileID: 7711355090271435980} + - component: {fileID: 5287829808763682365} + m_Layer: 12 + m_Name: MineableChunk + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &3520563273759020846 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5063655529130579611} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: -7.62841, y: 3.2298, z: -4} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!33 &7358912368512722974 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5063655529130579611} + m_Mesh: {fileID: 0} +--- !u!23 &3942884241032403340 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5063655529130579611} + 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: 07059e320ce76b044bf5140668021f21, type: 2} + - {fileID: 2100000, guid: 4930c6ffa5b9b9f4098249a43abf8506, 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!64 &6314136871395425781 +MeshCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5063655529130579611} + 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: 0} +--- !u!114 &7711355090271435980 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5063655529130579611} + 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: 2335500207 + InScenePlacedSourceGlobalObjectIdHash: 2335500207 + DeferredDespawnTick: 0 + Ownership: 1 + AlwaysReplicateAsRoot: 0 + SynchronizeTransform: 1 + ActiveSceneSynchronization: 0 + SceneMigrationSynchronization: 0 + SpawnWithObservers: 1 + DontDestroyWithOwner: 0 + AutoObjectParentSync: 1 + SyncOwnerTransformWhenParented: 1 + AllowOwnerToParent: 0 +--- !u!114 &5287829808763682365 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5063655529130579611} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 53bb340e9008b024ba035f6ee8fa21a4, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::MineableChunk + ShowTopMostFoldoutHeaderGroup: 1 + normalBlockHealth: 100 + resourceBlockHealth: 150 + normalDropItem: {fileID: 0} + resourceDropItem: {fileID: 11400000, guid: 953ceca9a25978549a56f0a4ff5d6a2c, type: 2} + genericDropPrefab: {fileID: 1253970051563370359, guid: 1d7655b1088c3ea46b8f52f6c6760047, type: 3} + normalBlockMaterial: {fileID: 2100000, guid: 8aa7a33ec074d5f429b993c5c857614b, type: 2} + resourceBlockMaterial: {fileID: 2100000, guid: 4930c6ffa5b9b9f4098249a43abf8506, type: 2} + meshRebuildDelay: 0.1 + highlightMaterial: {fileID: 0} + visibilityTimeout: 0.5 + discoveredTint: {r: 0.3, g: 0.3, b: 0.3, a: 1} diff --git a/Assets/Prefabs/MineableChunk.prefab.meta b/Assets/Prefabs/MineableChunk.prefab.meta new file mode 100644 index 0000000..0b4d9ad --- /dev/null +++ b/Assets/Prefabs/MineableChunk.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: fa605604c7558cd41abc2fc25fc28e8f +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Prefabs/Player.prefab b/Assets/Prefabs/Player.prefab index 5976fd5..8123dfb 100644 --- a/Assets/Prefabs/Player.prefab +++ b/Assets/Prefabs/Player.prefab @@ -223,11 +223,6 @@ PrefabInstance: insertIndex: -1 addedObject: {fileID: 3912054216608015251} m_SourcePrefab: {fileID: 100100000, guid: ffaf1ddb2ff58d2448ccfdd357387f63, type: 3} ---- !u!4 &2152733048352974824 stripped -Transform: - m_CorrespondingSourceObject: {fileID: -5515783359193845756, guid: ffaf1ddb2ff58d2448ccfdd357387f63, type: 3} - m_PrefabInstance: {fileID: 3356319783404427244} - m_PrefabAsset: {fileID: 0} --- !u!1 &2473992278589500093 stripped GameObject: m_CorrespondingSourceObject: {fileID: 919132149155446097, guid: ffaf1ddb2ff58d2448ccfdd357387f63, type: 3} @@ -341,7 +336,7 @@ MonoBehaviour: m_Bits: 25600 constructionLayer: serializedVersion: 2 - m_Bits: 256 + m_Bits: 384 buildSpeedMultiplier: 2 itemLayer: serializedVersion: 2 @@ -359,7 +354,7 @@ MonoBehaviour: crosshairUI: {fileID: 0} idleCrosshair: {fileID: 2628378444897590106, guid: 174f7cb20aaa6d4409b788a700a925ad, type: 3} targetCrosshair: {fileID: -5662625722731528258, guid: 7652364ca249c3144813de7eb3d1b129, type: 3} - visionRadius: 2 + visionRadius: 5 --- !u!114 &106528027568436521 MonoBehaviour: m_ObjectHideFlags: 0 @@ -506,4 +501,8 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: Assembly-CSharp::PlayerEquipmentHandler ShowTopMostFoldoutHeaderGroup: 1 - toolAnchor: {fileID: 2152733048352974824} + mainHandAnchor: {fileID: 0} + offHandAnchor: {fileID: 0} + mainHandSlot: + SlotType: 0 + AttachPoint: {fileID: 0} diff --git a/Assets/Prefabs/UndergroundGenerator.prefab b/Assets/Prefabs/UndergroundGenerator.prefab new file mode 100644 index 0000000..85b064a --- /dev/null +++ b/Assets/Prefabs/UndergroundGenerator.prefab @@ -0,0 +1,82 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &5156816586410934540 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 69091081815009223} + - component: {fileID: 5648469769714403381} + - component: {fileID: 1129818542040768727} + m_Layer: 0 + m_Name: UndergroundGenerator + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &69091081815009223 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5156816586410934540} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: -14, y: -6, z: -3} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &5648469769714403381 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5156816586410934540} + 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: 1220698054 + InScenePlacedSourceGlobalObjectIdHash: 0 + DeferredDespawnTick: 0 + Ownership: 1 + AlwaysReplicateAsRoot: 0 + SynchronizeTransform: 1 + ActiveSceneSynchronization: 0 + SceneMigrationSynchronization: 0 + SpawnWithObservers: 1 + DontDestroyWithOwner: 0 + AutoObjectParentSync: 1 + SyncOwnerTransformWhenParented: 1 + AllowOwnerToParent: 0 +--- !u!114 &1129818542040768727 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5156816586410934540} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 7fc80ebb61fb70d4fa694d3a1f81d2ab, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::UndergroundGenerator + ShowTopMostFoldoutHeaderGroup: 1 + generationRange: {x: 50, y: 30, z: 6} + noiseScale: 0.14 + hollowThreshold: 0.299 + baseResourceThreshold: 0.563 + increaseResourceWithDepth: 1 + depthFactor: 0.005 + normalBlockPrefab: {fileID: 989066657509100432, guid: dbb1cfcb3d9e3844e8d9cdf09b0a1660, type: 3} + resourceBlockPrefab: {fileID: 989066657509100432, guid: 17532917e1ada23469c573abf64905f0, type: 3} + containerName: UndergroundBlocks diff --git a/Assets/Prefabs/UndergroundGenerator.prefab.meta b/Assets/Prefabs/UndergroundGenerator.prefab.meta new file mode 100644 index 0000000..3fe3bd4 --- /dev/null +++ b/Assets/Prefabs/UndergroundGenerator.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 29ad9c03b79f43f42859005ce707dff2 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scenes/DefenceScene.unity b/Assets/Scenes/DefenceScene.unity index a300c1e..7eb9db1 100644 --- a/Assets/Scenes/DefenceScene.unity +++ b/Assets/Scenes/DefenceScene.unity @@ -1771,6 +1771,87 @@ PrefabInstance: insertIndex: -1 addedObject: {fileID: 445606028} m_SourcePrefab: {fileID: 100100000, guid: 443aa97110814434cb36b26656f1884c, type: 3} +--- !u!1 &1026559330 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1026559333} + - component: {fileID: 1026559332} + - component: {fileID: 1026559331} + m_Layer: 0 + m_Name: ChunkedUndergroundGenerator + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1026559331 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1026559330} + 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: 964628114 + InScenePlacedSourceGlobalObjectIdHash: 0 + DeferredDespawnTick: 0 + Ownership: 1 + AlwaysReplicateAsRoot: 0 + SynchronizeTransform: 1 + ActiveSceneSynchronization: 0 + SceneMigrationSynchronization: 0 + SpawnWithObservers: 1 + DontDestroyWithOwner: 0 + AutoObjectParentSync: 1 + SyncOwnerTransformWhenParented: 1 + AllowOwnerToParent: 0 +--- !u!114 &1026559332 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1026559330} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3d90f6724f14d49489261782c9672f11, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::ChunkedUndergroundGenerator + ShowTopMostFoldoutHeaderGroup: 1 + generationRange: {x: 20, y: 30, z: 10} + noiseScale: 0.12 + hollowThreshold: 0.35 + baseResourceThreshold: 0.8 + increaseResourceWithDepth: 1 + depthFactor: 0.005 + normalBlockHealth: 100 + resourceBlockHealth: 150 + chunkPrefab: {fileID: 0} + containerName: UndergroundChunks +--- !u!4 &1026559333 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1026559330} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: -7.62834, y: 3.07751, z: -4} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1044242050 GameObject: m_ObjectHideFlags: 0 @@ -3031,86 +3112,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1600458457} m_CullTransparentMesh: 1 ---- !u!1 &1634635642 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1634635645} - - component: {fileID: 1634635643} - - component: {fileID: 1634635644} - m_Layer: 0 - m_Name: UndergroundGenerator - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!114 &1634635643 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1634635642} - 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: 1220698054 - InScenePlacedSourceGlobalObjectIdHash: 0 - DeferredDespawnTick: 0 - Ownership: 1 - AlwaysReplicateAsRoot: 0 - SynchronizeTransform: 1 - ActiveSceneSynchronization: 0 - SceneMigrationSynchronization: 0 - SpawnWithObservers: 1 - DontDestroyWithOwner: 0 - AutoObjectParentSync: 1 - SyncOwnerTransformWhenParented: 1 - AllowOwnerToParent: 0 ---- !u!114 &1634635644 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1634635642} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 7fc80ebb61fb70d4fa694d3a1f81d2ab, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::UndergroundGenerator - ShowTopMostFoldoutHeaderGroup: 1 - generationRange: {x: 20, y: 30, z: 6} - noiseScale: 0.14 - hollowThreshold: 0.3 - baseResourceThreshold: 0.63 - increaseResourceWithDepth: 1 - depthFactor: 0.005 - normalBlockPrefab: {fileID: 989066657509100432, guid: dbb1cfcb3d9e3844e8d9cdf09b0a1660, type: 3} - resourceBlockPrefab: {fileID: 989066657509100432, guid: 17532917e1ada23469c573abf64905f0, type: 3} - containerName: UndergroundBlocks ---- !u!4 &1634635645 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1634635642} - serializedVersion: 2 - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: -14, y: -6, z: -3} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 0} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!20 &1648146738 stripped Camera: m_CorrespondingSourceObject: {fileID: 5650099317679730308, guid: 2b08dd32e48ef5e4aa65a6122099152e, type: 3} @@ -3809,6 +3810,71 @@ Transform: m_CorrespondingSourceObject: {fileID: 2338240775821095493, guid: 1955bdf7dd2940f44aa117fbcf6eb626, type: 3} m_PrefabInstance: {fileID: 497942047} m_PrefabAsset: {fileID: 0} +--- !u!1001 &320979913694462859 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: 3520563273759020846, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3} + propertyPath: m_LocalPosition.x + value: -7.62841 + objectReference: {fileID: 0} + - target: {fileID: 3520563273759020846, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3} + propertyPath: m_LocalPosition.y + value: 3.2298 + objectReference: {fileID: 0} + - target: {fileID: 3520563273759020846, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3} + propertyPath: m_LocalPosition.z + value: -4 + objectReference: {fileID: 0} + - target: {fileID: 3520563273759020846, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 3520563273759020846, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3520563273759020846, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3520563273759020846, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3520563273759020846, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3520563273759020846, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3520563273759020846, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 5063655529130579611, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3} + propertyPath: m_Name + value: MineableChunk + objectReference: {fileID: 0} + - target: {fileID: 7711355090271435980, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3} + propertyPath: GlobalObjectIdHash + value: 1536139134 + objectReference: {fileID: 0} + - target: {fileID: 7711355090271435980, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3} + propertyPath: InScenePlacedSourceGlobalObjectIdHash + value: 2335500207 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3} --- !u!1001 &3690888448170635710 PrefabInstance: m_ObjectHideFlags: 0 @@ -3930,10 +3996,11 @@ SceneRoots: - {fileID: 743367988} - {fileID: 670724422} - {fileID: 1409253547} - - {fileID: 1634635645} - {fileID: 437093202} - {fileID: 1489230404} - {fileID: 556982644} - {fileID: 2067098344} - {fileID: 1384281111} - {fileID: 1782529044} + - {fileID: 320979913694462859} + - {fileID: 1026559333} diff --git a/Assets/Scripts/GameBase/BuildManager.cs b/Assets/Scripts/GameBase/BuildManager.cs index dd10c3d..1559995 100644 --- a/Assets/Scripts/GameBase/BuildManager.cs +++ b/Assets/Scripts/GameBase/BuildManager.cs @@ -2,6 +2,7 @@ using UnityEngine; using Unity.Netcode; using UnityEngine.InputSystem; using UnityEngine.EventSystems; +using UnityEngine.UI; using System.Collections.Generic; public class BuildManager : NetworkBehaviour @@ -36,6 +37,9 @@ public class BuildManager : NetworkBehaviour private Vector3Int _currentGridPos; private PlayerInputActions _inputActions; + // Public property to check if currently in build mode + public bool IsBuildMode => _isBuildMode; + private Dictionary _tunnelRegistry = new Dictionary(); private HashSet _occupiedNodes = new HashSet(); @@ -139,10 +143,59 @@ public class BuildManager : NetworkBehaviour return _tunnelRegistry.GetValueOrDefault(pos); } + // Helper method to properly check if pointer is over UI with New Input System + private bool IsPointerOverUI() + { + if (EventSystem.current == null) return false; + + // Use the new input system's pointer position + Vector2 pointerPosition = Mouse.current.position.ReadValue(); + + PointerEventData eventData = new PointerEventData(EventSystem.current) + { + position = pointerPosition + }; + + List results = new List(); + EventSystem.current.RaycastAll(eventData, results); + + // Filter out non-interactive UI elements (crosshair, HUD, etc.) + foreach (var result in results) + { + GameObject uiObject = result.gameObject; + + // Ignore non-interactive UI elements by name + if (uiObject.name == "Crosshair" || + uiObject.name.Contains("HUD") || + uiObject.name.Contains("Display")) + { + continue; + } + + // Check if the UI element is actually interactive (has a Selectable component) + UnityEngine.UI.Selectable selectable = uiObject.GetComponent(); + if (selectable != null && selectable.interactable) + { + return true; + } + + // Also check parent objects for Selectable components (in case we hit a child element) + selectable = uiObject.GetComponentInParent(); + if (selectable != null && selectable.interactable) + { + return true; + } + } + + // No interactive UI elements found + return false; + } + // 1. 건설 요청 시 실제 계산된 worldPos를 넘겨줍니다. private void OnBuildRequested() { - if (!_isBuildMode || EventSystem.current.IsPointerOverGameObject()) return; + if (!_isBuildMode) return; + if (IsPointerOverUI()) return; // 고스트가 현재 위치한 '그 좌표'를 그대로 보냅니다. RequestBuildRpc(_selectedTurretIndex, _currentGridPos, _ghostInstance.transform.position); @@ -153,12 +206,30 @@ public class BuildManager : NetworkBehaviour [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] private void RequestBuildRpc(int index, Vector3Int gridPos, Vector3 worldPos) { + if (constructionSitePrefab == null) + { + Debug.LogError("[BuildManager] Construction site prefab is null!"); + return; + } + // GridToWorld를 다시 계산하지 않고 전달받은 worldPos를 그대로 사용합니다. GameObject siteObj = Instantiate(constructionSitePrefab, worldPos, Quaternion.identity); - siteObj.GetComponent().Spawn(); + + NetworkObject netObj = siteObj.GetComponent(); + if (netObj == null) + { + Debug.LogError("[BuildManager] Construction site has no NetworkObject component!"); + Destroy(siteObj); + return; + } + + netObj.Spawn(); ConstructionSite site = siteObj.GetComponent(); - if (site != null) site.Initialize(index, gridPos); + if (site != null) + { + site.Initialize(index, gridPos); + } } public void SelectTurret(int index) diff --git a/Assets/Scripts/Player/MiningBehavior.cs b/Assets/Scripts/Player/MiningBehavior.cs index 7503cd0..da5957e 100644 --- a/Assets/Scripts/Player/MiningBehavior.cs +++ b/Assets/Scripts/Player/MiningBehavior.cs @@ -2,6 +2,7 @@ /// /// Mining behavior for pickaxes and similar tools. +/// Supports both legacy MineableBlock and new chunk-based MineableChunk. /// [CreateAssetMenu(menuName = "Items/Behaviors/Mining Behavior")] public class MiningBehavior : ItemBehavior @@ -27,7 +28,25 @@ public class MiningBehavior : ItemBehavior { if (target == null) return; - // Use IDamageable interface for all damageable objects + // Try chunk-based mining first (new system) + if (target.TryGetComponent(out var chunk)) + { + // Get the specific block index from PlayerNetworkController + var playerController = user.GetComponent(); + if (playerController != null) + { + var chunkTarget = playerController.GetCurrentChunkTarget(); + if (chunkTarget.hasHit && chunkTarget.chunk == chunk) + { + // Damage the specific block within the chunk + chunk.DamageBlockServerRpc(chunkTarget.blockIndex, (byte)Mathf.Min(255, damage)); + return; + } + } + return; + } + + // Fallback to legacy IDamageable interface if (target.TryGetComponent(out var damageable)) { damageable.TakeDamage(new DamageInfo(damage, DamageType.Mining, user)); @@ -37,8 +56,15 @@ public class MiningBehavior : ItemBehavior public override string GetBlockedReason(GameObject user, GameObject target) { if (target == null) return "No target"; + + // Check for chunk + if (target.TryGetComponent(out _)) + return null; // Chunks are always mineable + + // Check for legacy damageable if (!target.TryGetComponent(out _)) return "Cannot mine this object"; + return null; } } diff --git a/Assets/Scripts/Player/PlayerNetworkController.cs b/Assets/Scripts/Player/PlayerNetworkController.cs index d6bc06c..156fc69 100644 --- a/Assets/Scripts/Player/PlayerNetworkController.cs +++ b/Assets/Scripts/Player/PlayerNetworkController.cs @@ -44,8 +44,13 @@ public class PlayerNetworkController : NetworkBehaviour private PlayerActionHandler _actionHandler; private RectTransform _crosshairRect; - private MineableBlock _currentTargetBlock; // 현재 강조 중인 블록 저장 - private MineableBlock _lastHighlightedBlock; + private MineableBlock _currentTargetBlock; // 현재 강조 중인 블록 저장 (legacy) + private MineableBlock _lastHighlightedBlock; // legacy block targeting + + // Chunk-based targeting (new system) + private MineableChunk _lastHighlightedChunk; + private int _lastHighlightedChunkBlockIndex = -1; + private ChunkInteractionHandler.ChunkHitResult _currentChunkTarget; private CharacterController _controller; private PlayerInputActions _inputActions; @@ -209,13 +214,17 @@ public class PlayerNetworkController : NetworkBehaviour { if (!IsOwner || _actionHandler.IsBusy) return; + // Don't perform actions when in build mode + if (BuildManager.Instance != null && BuildManager.Instance.IsBuildMode) return; + ItemData selectedItem = _inventory.GetSelectedItemData(); if (selectedItem == null) return; // Check if item has behavior (new system) if (selectedItem.behavior != null) { - GameObject target = _lastHighlightedBlock != null ? _lastHighlightedBlock.gameObject : null; + // Get target - prioritize chunk system over legacy blocks + GameObject target = GetCurrentMiningTarget(); // Use the new behavior system if (selectedItem.CanUse(gameObject, target)) @@ -334,51 +343,87 @@ public class PlayerNetworkController : NetworkBehaviour { if (!IsOwner || _crosshairRect == null) return; - // 1. 카메라 레이로 조준점 계산 (플레이어 몸통 무시) + // Use direct raycast from camera through crosshair position + // Use longer range (100m) from camera to catch all distances Ray aimRay = Camera.main.ScreenPointToRay(_crosshairRect.position); - Vector3 worldAimPoint; - if (Physics.Raycast(aimRay, out RaycastHit mouseHit, 100f, ~ignoreDuringAim)) - worldAimPoint = mouseHit.point; - else - worldAimPoint = aimRay.GetPoint(50f); - // 2. 캐릭터 가슴에서 조준점을 향하는 방향 계산 - Vector3 origin = transform.position + Vector3.up * 1.2f; - Vector3 direction = (worldAimPoint - origin).normalized; - - // 자기 자신 충돌 방지용 오프셋 - Vector3 rayStart = origin + direction * 0.4f; - - // 3. [중요] 실제 공격과 동일한 SphereCast 실행 RaycastHit blockHit; - bool hasTarget = Physics.SphereCast(rayStart, aimRadius, direction, out blockHit, attackRange - 0.4f, mineableLayer); + bool hasTarget = Physics.SphereCast(aimRay, aimRadius, out blockHit, 100f, mineableLayer); - // 4. 하이라이트 대상 업데이트 - MineableBlock currentTarget = null; + // Filter by actual attack range from player if (hasTarget) { - currentTarget = blockHit.collider.GetComponentInParent(); + Vector3 playerPos = transform.position + Vector3.up * 1.2f; + float distanceFromPlayer = Vector3.Distance(playerPos, blockHit.point); + + if (distanceFromPlayer > attackRange) + { + hasTarget = false; // Too far from player to interact with + } } - // 대상이 바뀌었을 때만 아웃라인 갱신 (최적화) - if (_lastHighlightedBlock != currentTarget) + // 4. 하이라이트 대상 업데이트 - 청크 시스템과 레거시 블록 모두 지원 + MineableBlock currentLegacyTarget = null; + MineableChunk currentChunk = null; + int currentChunkBlockIndex = -1; + + if (hasTarget) + { + // Try chunk first (new system) + var chunkHit = ChunkInteractionHandler.GetChunkHit(blockHit); + if (chunkHit.hasHit) + { + currentChunk = chunkHit.chunk; + currentChunkBlockIndex = chunkHit.blockIndex; + _currentChunkTarget = chunkHit; + } + else + { + // Fallback to legacy MineableBlock + currentLegacyTarget = blockHit.collider.GetComponentInParent(); + _currentChunkTarget = ChunkInteractionHandler.ChunkHitResult.None; + } + } + else + { + _currentChunkTarget = ChunkInteractionHandler.ChunkHitResult.None; + } + + // Update chunk highlight + bool chunkTargetChanged = (currentChunk != _lastHighlightedChunk) || + (currentChunkBlockIndex != _lastHighlightedChunkBlockIndex); + if (chunkTargetChanged) + { + if (_lastHighlightedChunk != null) + _lastHighlightedChunk.SetHighlight(false); + if (currentChunk != null) + currentChunk.SetHighlight(true, currentChunkBlockIndex); + + _lastHighlightedChunk = currentChunk; + _lastHighlightedChunkBlockIndex = currentChunkBlockIndex; + } + + // Update legacy block highlight + if (_lastHighlightedBlock != currentLegacyTarget) { if (_lastHighlightedBlock != null) _lastHighlightedBlock.SetHighlight(false); - if (currentTarget != null) currentTarget.SetHighlight(true); - _lastHighlightedBlock = currentTarget; + if (currentLegacyTarget != null) currentLegacyTarget.SetHighlight(true); + _lastHighlightedBlock = currentLegacyTarget; } // 기즈모 디버그 데이터 동기화 - _debugOrigin = rayStart; - _debugDir = direction; - _debugHit = hasTarget; - _debugDist = hasTarget ? blockHit.distance : (attackRange - 0.4f); + Ray debugRay = Camera.main.ScreenPointToRay(_crosshairRect.position); + _debugOrigin = debugRay.origin; + _debugDir = debugRay.direction; + _debugHit = hasTarget && (currentChunk != null || currentLegacyTarget != null); + _debugDist = hasTarget ? blockHit.distance : attackRange; // 크로스헤어 이미지 교체 + bool hasValidTarget = currentChunk != null || currentLegacyTarget != null; if (crosshairUI != null) { - crosshairUI.sprite = hasTarget ? targetCrosshair : idleCrosshair; - crosshairUI.color = hasTarget ? Color.green : Color.white; + crosshairUI.sprite = hasValidTarget ? targetCrosshair : idleCrosshair; + crosshairUI.color = hasValidTarget ? Color.green : Color.white; } } @@ -420,11 +465,33 @@ public class PlayerNetworkController : NetworkBehaviour private void RevealSurroundings() { + // Use FogOfWarManager's revealRadius if available, fallback to visionRadius + float currentRevealRadius = visionRadius; + if (FogOfWarManager.Instance != null) + { + currentRevealRadius = FogOfWarManager.Instance.revealRadius; + } + // 시야 반경 내의 블록 감지 - Collider[] hitBlocks = Physics.OverlapSphere(transform.position, visionRadius, mineableLayer); + Collider[] hitBlocks = Physics.OverlapSphere(transform.position, currentRevealRadius, mineableLayer); foreach (var col in hitBlocks) { + // Try chunk-based reveal first (new system) + if (col.TryGetComponent(out var chunk)) + { + // Update local visibility (for fog of war visual states) + chunk.UpdateLocalVisibility(transform.position, currentRevealRadius); + + // Request server to mark blocks as discovered (permanent) + if (IsOwner) + { + RequestChunkRevealServerRpc(chunk.GetComponent().NetworkObjectId, transform.position, currentRevealRadius); + } + continue; + } + + // Fallback to legacy MineableBlock if (col.TryGetComponent(out var block)) { // 1. [로컬] 내 화면에서 이 블록을 보이게 함 (실시간 시야) @@ -451,6 +518,18 @@ public class PlayerNetworkController : NetworkBehaviour } } + [ServerRpc] + private void RequestChunkRevealServerRpc(ulong chunkNetId, Vector3 playerPos, float radius) + { + if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(chunkNetId, out var netObj)) + { + if (netObj.TryGetComponent(out var chunk)) + { + chunk.RevealBlocksInRadius(playerPos, radius); + } + } + } + private IEnumerator ActionRoutine(float duration, string animTrigger, Action actionLogic) { // 1. 상태 잠금 @@ -516,6 +595,9 @@ public class PlayerNetworkController : NetworkBehaviour { if (_actionHandler.IsBusy) return; + // Don't perform actions when in build mode + if (BuildManager.Instance != null && BuildManager.Instance.IsBuildMode) return; + ItemData selectedItem = _inventory.GetSelectedItemData(); if (selectedItem == null || selectedItem.behavior == null) return; @@ -525,7 +607,8 @@ public class PlayerNetworkController : NetworkBehaviour // Skip if non-repeatable action already executed once if (!actionDesc.CanRepeat && _hasExecutedOnce) return; - GameObject target = _lastHighlightedBlock != null ? _lastHighlightedBlock.gameObject : null; + // Get target - prioritize chunk system over legacy blocks + GameObject target = GetCurrentMiningTarget(); if (selectedItem.CanUse(gameObject, target)) { @@ -537,6 +620,29 @@ public class PlayerNetworkController : NetworkBehaviour } } + /// + /// Get the current mining target (chunk or legacy block) + /// + private GameObject GetCurrentMiningTarget() + { + // Prioritize chunk target + if (_currentChunkTarget.hasHit && _currentChunkTarget.chunk != null) + { + return _currentChunkTarget.chunk.gameObject; + } + + // Fallback to legacy block + return _lastHighlightedBlock != null ? _lastHighlightedBlock.gameObject : null; + } + + /// + /// Get the current chunk target info (for MiningBehavior) + /// + public ChunkInteractionHandler.ChunkHitResult GetCurrentChunkTarget() + { + return _currentChunkTarget; + } + private void OnDrawGizmos() { if (!Application.isPlaying || !IsOwner) return; diff --git a/Assets/Scripts/Underground.meta b/Assets/Scripts/Underground.meta new file mode 100644 index 0000000..dde65aa --- /dev/null +++ b/Assets/Scripts/Underground.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f0bcafb0d0408ab4f893e9c51c9e60e5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Underground/BlockData.cs b/Assets/Scripts/Underground/BlockData.cs new file mode 100644 index 0000000..d183d62 --- /dev/null +++ b/Assets/Scripts/Underground/BlockData.cs @@ -0,0 +1,193 @@ +using Unity.Netcode; + +/// +/// Compact block state struct for chunk-based storage. +/// 3 bytes per block: type (1), health (1), flags (1) +/// +public struct BlockData : INetworkSerializable +{ + /// + /// Block type: 0=empty/air, 1=normal stone, 2=resource ore + /// + public byte blockType; + + /// + /// Block health: 0-255 (0 = destroyed) + /// + public byte health; + + /// + /// Block flags: bit 0 = isDiscovered (fog of war) + /// + public byte flags; + + // Block type constants + public const byte TYPE_EMPTY = 0; + public const byte TYPE_NORMAL = 1; + public const byte TYPE_RESOURCE = 2; + + // Flag bit masks + public const byte FLAG_DISCOVERED = 1 << 0; // Has been seen at least once (networked/permanent) + + /// + /// Whether this block is empty (air or destroyed) + /// + public bool IsEmpty => blockType == TYPE_EMPTY || health == 0; + + /// + /// Whether this block has been discovered by players + /// + public bool IsDiscovered + { + get => (flags & FLAG_DISCOVERED) != 0; + set + { + if (value) + flags |= FLAG_DISCOVERED; + else + flags &= unchecked((byte)~FLAG_DISCOVERED); + } + } + + /// + /// Whether this block is a resource block + /// + public bool IsResource => blockType == TYPE_RESOURCE; + + /// + /// Create an empty block + /// + public static BlockData Empty => new BlockData + { + blockType = TYPE_EMPTY, + health = 0, + flags = 0 + }; + + /// + /// Create a normal stone block with full health + /// + public static BlockData Normal(byte maxHealth = 100) => new BlockData + { + blockType = TYPE_NORMAL, + health = maxHealth, + flags = 0 + }; + + /// + /// Create a resource ore block with full health + /// + public static BlockData Resource(byte maxHealth = 150) => new BlockData + { + blockType = TYPE_RESOURCE, + health = maxHealth, + flags = 0 + }; + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + serializer.SerializeValue(ref blockType); + serializer.SerializeValue(ref health); + serializer.SerializeValue(ref flags); + } +} + +/// +/// Network-serializable container for entire chunk state. +/// Used for initial sync when clients connect. +/// +public struct ChunkState : INetworkSerializable +{ + public BlockData[] blocks; + + public const int CHUNK_SIZE = 4; + public const int BLOCKS_PER_CHUNK = CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE; // 64 + + public ChunkState(int size) + { + blocks = new BlockData[size]; + } + + /// + /// Ensure blocks array is initialized + /// + private void EnsureInitialized() + { + if (blocks == null) + { + blocks = new BlockData[BLOCKS_PER_CHUNK]; + } + } + + /// + /// Get block at local coordinates within the chunk + /// + public BlockData GetBlock(int x, int y, int z) + { + if (blocks == null) return BlockData.Empty; + int index = x + y * CHUNK_SIZE + z * CHUNK_SIZE * CHUNK_SIZE; + if (index < 0 || index >= blocks.Length) return BlockData.Empty; + return blocks[index]; + } + + /// + /// Set block at local coordinates within the chunk + /// + public void SetBlock(int x, int y, int z, BlockData block) + { + EnsureInitialized(); + int index = x + y * CHUNK_SIZE + z * CHUNK_SIZE * CHUNK_SIZE; + if (index >= 0 && index < blocks.Length) + { + blocks[index] = block; + } + } + + /// + /// Convert local index to local 3D coordinates + /// + public static (int x, int y, int z) IndexToLocal(int index) + { + int x = index % CHUNK_SIZE; + int y = (index / CHUNK_SIZE) % CHUNK_SIZE; + int z = index / (CHUNK_SIZE * CHUNK_SIZE); + return (x, y, z); + } + + /// + /// Convert local 3D coordinates to index + /// + public static int LocalToIndex(int x, int y, int z) + { + return x + y * CHUNK_SIZE + z * CHUNK_SIZE * CHUNK_SIZE; + } + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + // Serialize array length first + int length = BLOCKS_PER_CHUNK; + serializer.SerializeValue(ref length); + + if (serializer.IsReader) + { + blocks = new BlockData[length]; + } + else if (blocks == null) + { + blocks = new BlockData[length]; + } + + for (int i = 0; i < length; i++) + { + blocks[i].NetworkSerialize(serializer); + } + } + + /// + /// Create a default initialized ChunkState + /// + public static ChunkState CreateEmpty() + { + return new ChunkState(BLOCKS_PER_CHUNK); + } +} diff --git a/Assets/Scripts/Underground/BlockData.cs.meta b/Assets/Scripts/Underground/BlockData.cs.meta new file mode 100644 index 0000000..059319b --- /dev/null +++ b/Assets/Scripts/Underground/BlockData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d2b004a2f2115024ea956fdccbb62cf1 \ No newline at end of file diff --git a/Assets/Scripts/Underground/ChunkCoord.cs b/Assets/Scripts/Underground/ChunkCoord.cs new file mode 100644 index 0000000..f35267f --- /dev/null +++ b/Assets/Scripts/Underground/ChunkCoord.cs @@ -0,0 +1,215 @@ +using UnityEngine; + +/// +/// Utility struct for chunk coordinate conversions. +/// Handles conversions between world, grid, chunk, and local block coordinates. +/// +public struct ChunkCoord +{ + public const int CHUNK_SIZE = 4; + + /// + /// Chunk position in chunk coordinates (not world or grid) + /// + public Vector3Int chunkPos; + + public ChunkCoord(Vector3Int pos) + { + chunkPos = pos; + } + + public ChunkCoord(int x, int y, int z) + { + chunkPos = new Vector3Int(x, y, z); + } + + /// + /// Get the grid position of the chunk's origin (corner with smallest coordinates) + /// + public Vector3Int GridOrigin => new Vector3Int( + chunkPos.x * CHUNK_SIZE, + chunkPos.y * CHUNK_SIZE, + chunkPos.z * CHUNK_SIZE + ); + + /// + /// Get world position of chunk origin using BuildManager's grid system + /// + public Vector3 WorldOrigin + { + get + { + if (BuildManager.Instance != null) + { + return BuildManager.Instance.GridToWorld(GridOrigin); + } + // Fallback if BuildManager not available + return new Vector3( + GridOrigin.x + 0.5f, + GridOrigin.y + 0.5f, + GridOrigin.z + 0.5f + ); + } + } + + /// + /// Convert grid coordinates to chunk coordinates + /// + public static ChunkCoord FromGridPos(Vector3Int gridPos) + { + return new ChunkCoord( + Mathf.FloorToInt((float)gridPos.x / CHUNK_SIZE), + Mathf.FloorToInt((float)gridPos.y / CHUNK_SIZE), + Mathf.FloorToInt((float)gridPos.z / CHUNK_SIZE) + ); + } + + /// + /// Convert world position to chunk coordinates + /// + public static ChunkCoord FromWorldPos(Vector3 worldPos) + { + Vector3Int gridPos; + if (BuildManager.Instance != null) + { + gridPos = BuildManager.Instance.WorldToGrid3D(worldPos); + } + else + { + // Fallback + gridPos = new Vector3Int( + Mathf.RoundToInt(worldPos.x - 0.5f), + Mathf.RoundToInt(worldPos.y - 0.5f), + Mathf.RoundToInt(worldPos.z - 0.5f) + ); + } + return FromGridPos(gridPos); + } + + /// + /// Get local block coordinates within chunk from grid position + /// + public static Vector3Int GridToLocal(Vector3Int gridPos) + { + // Use modulo to get local position, handling negative coords + int x = ((gridPos.x % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE; + int y = ((gridPos.y % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE; + int z = ((gridPos.z % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE; + return new Vector3Int(x, y, z); + } + + /// + /// Convert local block coordinates to grid position + /// + public Vector3Int LocalToGrid(Vector3Int localPos) + { + return GridOrigin + localPos; + } + + /// + /// Convert local block coordinates to world position + /// + public Vector3 LocalToWorld(Vector3Int localPos) + { + Vector3Int gridPos = LocalToGrid(localPos); + if (BuildManager.Instance != null) + { + return BuildManager.Instance.GridToWorld(gridPos); + } + return new Vector3(gridPos.x + 0.5f, gridPos.y + 0.5f, gridPos.z + 0.5f); + } + + /// + /// Convert local index to local 3D coordinates + /// + public static Vector3Int IndexToLocal(int index) + { + int x = index % CHUNK_SIZE; + int y = (index / CHUNK_SIZE) % CHUNK_SIZE; + int z = index / (CHUNK_SIZE * CHUNK_SIZE); + return new Vector3Int(x, y, z); + } + + /// + /// Convert local 3D coordinates to index + /// + public static int LocalToIndex(Vector3Int localPos) + { + return localPos.x + localPos.y * CHUNK_SIZE + localPos.z * CHUNK_SIZE * CHUNK_SIZE; + } + + /// + /// Convert local coordinates (int) to index + /// + public static int LocalToIndex(int x, int y, int z) + { + return x + y * CHUNK_SIZE + z * CHUNK_SIZE * CHUNK_SIZE; + } + + /// + /// Check if local coordinates are within valid range + /// + public static bool IsValidLocal(int x, int y, int z) + { + return x >= 0 && x < CHUNK_SIZE && + y >= 0 && y < CHUNK_SIZE && + z >= 0 && z < CHUNK_SIZE; + } + + /// + /// Check if local position is within valid range + /// + public static bool IsValidLocal(Vector3Int local) + { + return IsValidLocal(local.x, local.y, local.z); + } + + /// + /// Convert world hit point to local block index within chunk + /// + public int WorldPointToLocalIndex(Vector3 worldPoint) + { + // Get chunk origin in world space + Vector3 chunkWorldOrigin = WorldOrigin; + + // Calculate offset from chunk origin (in grid units, assuming 1 unit per block) + float cellSize = BuildManager.Instance != null ? BuildManager.Instance.cellSize : 1f; + Vector3 offset = (worldPoint - chunkWorldOrigin) / cellSize; + + // Convert to local coordinates (add small epsilon to handle edge cases) + int lx = Mathf.Clamp(Mathf.FloorToInt(offset.x + 0.5f), 0, CHUNK_SIZE - 1); + int ly = Mathf.Clamp(Mathf.FloorToInt(offset.y + 0.5f), 0, CHUNK_SIZE - 1); + int lz = Mathf.Clamp(Mathf.FloorToInt(offset.z + 0.5f), 0, CHUNK_SIZE - 1); + + return LocalToIndex(lx, ly, lz); + } + + public override bool Equals(object obj) + { + if (obj is ChunkCoord other) + { + return chunkPos == other.chunkPos; + } + return false; + } + + public override int GetHashCode() + { + return chunkPos.GetHashCode(); + } + + public static bool operator ==(ChunkCoord a, ChunkCoord b) + { + return a.chunkPos == b.chunkPos; + } + + public static bool operator !=(ChunkCoord a, ChunkCoord b) + { + return a.chunkPos != b.chunkPos; + } + + public override string ToString() + { + return $"ChunkCoord({chunkPos.x}, {chunkPos.y}, {chunkPos.z})"; + } +} diff --git a/Assets/Scripts/Underground/ChunkCoord.cs.meta b/Assets/Scripts/Underground/ChunkCoord.cs.meta new file mode 100644 index 0000000..1d023b8 --- /dev/null +++ b/Assets/Scripts/Underground/ChunkCoord.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 08589d2f166b446418cc8b61105e6d01 \ No newline at end of file diff --git a/Assets/Scripts/Underground/ChunkInteractionHandler.cs b/Assets/Scripts/Underground/ChunkInteractionHandler.cs new file mode 100644 index 0000000..bc8d727 --- /dev/null +++ b/Assets/Scripts/Underground/ChunkInteractionHandler.cs @@ -0,0 +1,126 @@ +using UnityEngine; + +/// +/// Handles interaction between players and chunk-based blocks. +/// Converts raycast hits to block indices and manages damage/reveal requests. +/// +public static class ChunkInteractionHandler +{ + /// + /// Result of a chunk interaction query + /// + public struct ChunkHitResult + { + public bool hasHit; + public MineableChunk chunk; + public int blockIndex; + public Vector3 hitPoint; + public Vector3 blockWorldPosition; + public BlockData blockData; + + public static ChunkHitResult None => new ChunkHitResult { hasHit = false }; + } + + /// + /// Try to get chunk and block info from a raycast hit + /// + public static ChunkHitResult GetChunkHit(RaycastHit hit) + { + // Try to get MineableChunk from hit collider + MineableChunk chunk = hit.collider.GetComponentInParent(); + if (chunk == null) + { + return ChunkHitResult.None; + } + + // Convert hit point to block index + // Push slightly into the surface using the normal to get the correct block + Vector3 insidePoint = hit.point - hit.normal * 0.01f; + int blockIndex = chunk.WorldPointToBlockIndex(insidePoint); + BlockData blockData = chunk.GetBlock(blockIndex); + + // Skip if block is empty + if (blockData.IsEmpty) + { + return ChunkHitResult.None; + } + + return new ChunkHitResult + { + hasHit = true, + chunk = chunk, + blockIndex = blockIndex, + hitPoint = hit.point, + blockWorldPosition = chunk.GetBlockWorldPosition(blockIndex), + blockData = blockData + }; + } + + /// + /// Try to damage a block at a raycast hit point + /// + public static bool TryDamageAtPoint(RaycastHit hit, float damage) + { + ChunkHitResult result = GetChunkHit(hit); + if (!result.hasHit) return false; + + // Request damage on server + result.chunk.DamageBlockServerRpc(result.blockIndex, (byte)Mathf.Min(255, damage)); + return true; + } + + /// + /// Try to damage a block directly on a known chunk + /// + public static bool TryDamageBlock(MineableChunk chunk, int blockIndex, float damage) + { + if (chunk == null) return false; + + BlockData block = chunk.GetBlock(blockIndex); + if (block.IsEmpty) return false; + + chunk.DamageBlockServerRpc(blockIndex, (byte)Mathf.Min(255, damage)); + return true; + } + + /// + /// Try to reveal a block at a raycast hit point + /// + public static bool TryRevealAtPoint(RaycastHit hit) + { + MineableChunk chunk = hit.collider.GetComponentInParent(); + if (chunk == null) return false; + + int blockIndex = chunk.WorldPointToBlockIndex(hit.point - hit.normal * 0.1f); + BlockData blockData = chunk.GetBlock(blockIndex); + + if (blockData.IsEmpty || blockData.IsDiscovered) return false; + + chunk.RevealBlockServerRpc(blockIndex); + return true; + } + + /// + /// Perform a sphere cast to find a targetable block in a chunk + /// + public static ChunkHitResult SphereCastForBlock(Vector3 origin, Vector3 direction, float radius, float maxDistance, LayerMask chunkLayer) + { + if (Physics.SphereCast(origin, radius, direction, out RaycastHit hit, maxDistance, chunkLayer)) + { + return GetChunkHit(hit); + } + return ChunkHitResult.None; + } + + /// + /// Perform a raycast to find a targetable block in a chunk + /// + public static ChunkHitResult RaycastForBlock(Vector3 origin, Vector3 direction, float maxDistance, LayerMask chunkLayer) + { + if (Physics.Raycast(origin, direction, out RaycastHit hit, maxDistance, chunkLayer)) + { + return GetChunkHit(hit); + } + return ChunkHitResult.None; + } +} diff --git a/Assets/Scripts/Underground/ChunkInteractionHandler.cs.meta b/Assets/Scripts/Underground/ChunkInteractionHandler.cs.meta new file mode 100644 index 0000000..5835456 --- /dev/null +++ b/Assets/Scripts/Underground/ChunkInteractionHandler.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3fc7fb23f18204f4eafbf4395b12aba5 \ No newline at end of file diff --git a/Assets/Scripts/Underground/ChunkMeshBuilder.cs b/Assets/Scripts/Underground/ChunkMeshBuilder.cs new file mode 100644 index 0000000..1236224 --- /dev/null +++ b/Assets/Scripts/Underground/ChunkMeshBuilder.cs @@ -0,0 +1,330 @@ +using System.Collections.Generic; +using UnityEngine; + +/// +/// Builds optimized meshes for chunks with face culling. +/// Only renders faces that are exposed (adjacent to empty blocks or chunk boundaries). +/// Uses submeshes for different block types (normal/resource). +/// +public static class ChunkMeshBuilder +{ + private const int CHUNK_SIZE = ChunkCoord.CHUNK_SIZE; + + // Face directions + private static readonly Vector3Int[] FaceDirections = new Vector3Int[] + { + Vector3Int.right, // +X + Vector3Int.left, // -X + Vector3Int.up, // +Y + Vector3Int.down, // -Y + new Vector3Int(0, 0, 1), // +Z + new Vector3Int(0, 0, -1) // -Z + }; + + // Face normals matching directions + private static readonly Vector3[] FaceNormals = new Vector3[] + { + Vector3.right, + Vector3.left, + Vector3.up, + Vector3.down, + Vector3.forward, + Vector3.back + }; + + // Vertices for each face (relative to block center) + private static readonly Vector3[][] FaceVertices = new Vector3[][] + { + // +X face + new Vector3[] { + new Vector3(0.5f, -0.5f, -0.5f), + new Vector3(0.5f, 0.5f, -0.5f), + new Vector3(0.5f, 0.5f, 0.5f), + new Vector3(0.5f, -0.5f, 0.5f) + }, + // -X face + new Vector3[] { + new Vector3(-0.5f, -0.5f, 0.5f), + new Vector3(-0.5f, 0.5f, 0.5f), + new Vector3(-0.5f, 0.5f, -0.5f), + new Vector3(-0.5f, -0.5f, -0.5f) + }, + // +Y face + new Vector3[] { + new Vector3(-0.5f, 0.5f, -0.5f), + new Vector3(-0.5f, 0.5f, 0.5f), + new Vector3( 0.5f, 0.5f, 0.5f), + new Vector3( 0.5f, 0.5f, -0.5f) + }, + // -Y face + new Vector3[] { + new Vector3(-0.5f, -0.5f, 0.5f), + new Vector3(-0.5f, -0.5f, -0.5f), + new Vector3( 0.5f, -0.5f, -0.5f), + new Vector3( 0.5f, -0.5f, 0.5f) + }, + // +Z face + new Vector3[] { + new Vector3(-0.5f, -0.5f, 0.5f), + new Vector3( 0.5f, -0.5f, 0.5f), + new Vector3( 0.5f, 0.5f, 0.5f), + new Vector3(-0.5f, 0.5f, 0.5f) + }, + // -Z face + new Vector3[] { + new Vector3( 0.5f, -0.5f, -0.5f), + new Vector3(-0.5f, -0.5f, -0.5f), + new Vector3(-0.5f, 0.5f, -0.5f), + new Vector3( 0.5f, 0.5f, -0.5f) + } + }; + + // UV coordinates for each face + private static readonly Vector2[] FaceUVs = new Vector2[] + { + new Vector2(0, 0), + new Vector2(1, 0), + new Vector2(1, 1), + new Vector2(0, 1) + }; + + /// + /// Interface for querying neighboring chunks for cross-chunk face culling + /// + public interface INeighborProvider + { + /// + /// Check if block at world grid position is solid (for face culling) + /// + bool IsBlockSolid(Vector3Int gridPos); + } + + /// + /// Visibility check delegate for fog of war + /// + public delegate bool VisibilityChecker(int blockIndex); + + /// + /// Build mesh for a chunk with face culling. + /// Returns a mesh with two submeshes: 0=normal blocks, 1=resource blocks + /// + public static Mesh BuildMesh(ChunkState state, ChunkCoord coord, INeighborProvider neighborProvider = null, bool onlyDiscovered = true) + { + return BuildMeshWithVisibility(state, coord, neighborProvider, onlyDiscovered, null); + } + + /// + /// Build mesh for a chunk with face culling and fog of war support. + /// Returns a mesh with 4 submeshes: 0=normal-visible, 1=resource-visible, 2=normal-dark, 3=resource-dark + /// + public static Mesh BuildMeshWithVisibility(ChunkState state, ChunkCoord coord, INeighborProvider neighborProvider, bool onlyDiscovered, VisibilityChecker isVisible) + { + // Lists for 4 submeshes: normal-visible, resource-visible, normal-dark, resource-dark + var meshData = new MeshData[4]; + for (int i = 0; i < 4; i++) + { + meshData[i] = new MeshData(); + } + + // Handle null blocks array + if (state.blocks == null) + { + return CreateEmptyMesh(coord, 4); + } + + // Process each block + for (int i = 0; i < ChunkState.BLOCKS_PER_CHUNK; i++) + { + BlockData block = state.blocks[i]; + + // Skip empty blocks + if (block.IsEmpty) continue; + + // Skip undiscovered blocks if onlyDiscovered is true + if (onlyDiscovered && !block.IsDiscovered) continue; + + Vector3Int localPos = ChunkCoord.IndexToLocal(i); + Vector3 blockOffset = new Vector3(localPos.x, localPos.y, localPos.z); + + // Determine which submesh to use based on block type and visibility + bool currentlyVisible = isVisible != null ? isVisible(i) : true; + int submeshIndex; + if (block.IsResource) + { + submeshIndex = currentlyVisible ? 1 : 3; // resource-visible or resource-dark + } + else + { + submeshIndex = currentlyVisible ? 0 : 2; // normal-visible or normal-dark + } + + var data = meshData[submeshIndex]; + + // Check each face + for (int f = 0; f < 6; f++) + { + Vector3Int neighborLocal = localPos + FaceDirections[f]; + + bool shouldRenderFace = false; + + if (ChunkCoord.IsValidLocal(neighborLocal)) + { + int neighborIndex = ChunkCoord.LocalToIndex(neighborLocal); + BlockData neighborBlock = state.blocks[neighborIndex]; + shouldRenderFace = neighborBlock.IsEmpty || (onlyDiscovered && !neighborBlock.IsDiscovered); + } + else + { + if (neighborProvider != null) + { + Vector3Int neighborGridPos = coord.LocalToGrid(neighborLocal); + shouldRenderFace = !neighborProvider.IsBlockSolid(neighborGridPos); + } + else + { + shouldRenderFace = true; + } + } + + if (shouldRenderFace) + { + AddFace(data.vertices, data.normals, data.uvs, data.triangles, blockOffset, f); + } + } + } + + // Create the mesh + Mesh mesh = new Mesh(); + mesh.name = $"Chunk_{coord.chunkPos.x}_{coord.chunkPos.y}_{coord.chunkPos.z}"; + + // Combine all vertices from all submeshes + int totalVerts = 0; + int[] vertOffsets = new int[4]; + for (int i = 0; i < 4; i++) + { + vertOffsets[i] = totalVerts; + totalVerts += meshData[i].vertices.Count; + } + + var allVertices = new Vector3[totalVerts]; + var allNormals = new Vector3[totalVerts]; + var allUVs = new Vector2[totalVerts]; + + for (int i = 0; i < 4; i++) + { + meshData[i].vertices.CopyTo(allVertices, vertOffsets[i]); + meshData[i].normals.CopyTo(allNormals, vertOffsets[i]); + meshData[i].uvs.CopyTo(allUVs, vertOffsets[i]); + } + + mesh.vertices = allVertices; + mesh.normals = allNormals; + mesh.uv = allUVs; + + // Set submeshes with adjusted triangle indices + mesh.subMeshCount = 4; + for (int i = 0; i < 4; i++) + { + var triangles = meshData[i].triangles; + for (int t = 0; t < triangles.Count; t++) + { + triangles[t] += vertOffsets[i]; + } + mesh.SetTriangles(triangles, i); + } + + mesh.RecalculateBounds(); + return mesh; + } + + /// + /// Helper class to hold mesh data for each submesh + /// + private class MeshData + { + public List vertices = new List(); + public List normals = new List(); + public List uvs = new List(); + public List triangles = new List(); + } + + /// + /// Create an empty mesh with proper submesh configuration + /// + private static Mesh CreateEmptyMesh(ChunkCoord coord, int submeshCount = 4) + { + Mesh mesh = new Mesh(); + mesh.name = $"Chunk_{coord.chunkPos.x}_{coord.chunkPos.y}_{coord.chunkPos.z}_Empty"; + mesh.subMeshCount = submeshCount; + for (int i = 0; i < submeshCount; i++) + { + mesh.SetTriangles(new int[0], i); + } + return mesh; + } + + /// + /// Add a face to the mesh data + /// + private static void AddFace(List vertices, List normals, List uvs, List triangles, Vector3 blockOffset, int faceIndex) + { + int startVertex = vertices.Count; + + // Add vertices + for (int i = 0; i < 4; i++) + { + vertices.Add(FaceVertices[faceIndex][i] + blockOffset); + normals.Add(FaceNormals[faceIndex]); + uvs.Add(FaceUVs[i]); + } + + // Add triangles (two triangles per face) + triangles.Add(startVertex); + triangles.Add(startVertex + 1); + triangles.Add(startVertex + 2); + + triangles.Add(startVertex); + triangles.Add(startVertex + 2); + triangles.Add(startVertex + 3); + } + + /// + /// Count visible faces in a chunk (for debugging/stats) + /// + public static int CountVisibleFaces(ChunkState state, bool onlyDiscovered = true) + { + if (state.blocks == null) return 0; + + int faceCount = 0; + + for (int i = 0; i < ChunkState.BLOCKS_PER_CHUNK; i++) + { + BlockData block = state.blocks[i]; + if (block.IsEmpty) continue; + if (onlyDiscovered && !block.IsDiscovered) continue; + + Vector3Int localPos = ChunkCoord.IndexToLocal(i); + + for (int f = 0; f < 6; f++) + { + Vector3Int neighborLocal = localPos + FaceDirections[f]; + + if (ChunkCoord.IsValidLocal(neighborLocal)) + { + int neighborIndex = ChunkCoord.LocalToIndex(neighborLocal); + BlockData neighborBlock = state.blocks[neighborIndex]; + if (neighborBlock.IsEmpty || (onlyDiscovered && !neighborBlock.IsDiscovered)) + { + faceCount++; + } + } + else + { + faceCount++; + } + } + } + + return faceCount; + } +} diff --git a/Assets/Scripts/Underground/ChunkMeshBuilder.cs.meta b/Assets/Scripts/Underground/ChunkMeshBuilder.cs.meta new file mode 100644 index 0000000..98cf06a --- /dev/null +++ b/Assets/Scripts/Underground/ChunkMeshBuilder.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 02008c1a2b561f049ae0f8b78dd9d22c \ No newline at end of file diff --git a/Assets/Scripts/Underground/ChunkedUndergroundGenerator.cs b/Assets/Scripts/Underground/ChunkedUndergroundGenerator.cs new file mode 100644 index 0000000..7e8abb5 --- /dev/null +++ b/Assets/Scripts/Underground/ChunkedUndergroundGenerator.cs @@ -0,0 +1,389 @@ +using System.Collections.Generic; +using Unity.Netcode; +using UnityEngine; + +/// +/// Generates underground terrain using chunk-based system. +/// Replaces individual MineableBlock NetworkObjects with MineableChunk NetworkObjects. +/// +public class ChunkedUndergroundGenerator : NetworkBehaviour +{ + public static ChunkedUndergroundGenerator Instance { get; private set; } + + [Header("Generation Range (in blocks)")] + [SerializeField] private Vector3Int generationRange = new Vector3Int(20, 30, 10); + [SerializeField] private float noiseScale = 0.12f; + + [Header("Thresholds (0 to 1)")] + [SerializeField, Range(0, 1)] private float hollowThreshold = 0.35f; + [SerializeField, Range(0, 1)] private float baseResourceThreshold = 0.85f; + + [Header("Resource Distribution")] + [SerializeField] private float resourceNoiseScale = 0.25f; // Larger = more spread out + [SerializeField, Range(0, 1)] private float resourceSpawnChance = 0.3f; // Additional random chance + + [Header("Depth Settings")] + [SerializeField] private bool increaseResourceWithDepth = true; + [SerializeField] private float depthFactor = 0.003f; + + [Header("Block Health")] + [SerializeField] private byte normalBlockHealth = 100; + [SerializeField] private byte resourceBlockHealth = 150; + + [Header("Chunk Prefab")] + [SerializeField] private GameObject chunkPrefab; + + [Header("Organization")] + [SerializeField] private string containerName = "UndergroundChunks"; + private Transform _chunkContainer; + + // Chunk registry for neighbor queries + private Dictionary _chunkRegistry = new Dictionary(); + + // Noise seeds + private float _seedX, _seedY, _seedZ; + private float _resourceSeedX, _resourceSeedY, _resourceSeedZ; + + private void Awake() + { + if (Instance == null) + { + Instance = this; + } + else + { + Destroy(gameObject); + return; + } + + _seedX = Random.Range(0f, 99999f); + _seedY = Random.Range(0f, 99999f); + _seedZ = Random.Range(0f, 99999f); + + // Separate seeds for resource distribution + _resourceSeedX = Random.Range(0f, 99999f); + _resourceSeedY = Random.Range(0f, 99999f); + _resourceSeedZ = Random.Range(0f, 99999f); + + // Create container for hierarchy organization + _chunkContainer = new GameObject(containerName).transform; + } + + public override void OnNetworkSpawn() + { + // Register with MineableChunk for neighbor queries + MineableChunk.SetChunkManager(this); + + if (IsServer) + { + GenerateChunks(); + } + } + + /// + /// Generate all chunks for the underground area + /// + private void GenerateChunks() + { + Vector3Int originGrid = BuildManager.Instance.WorldToGrid3D(transform.position); + + // Calculate chunk range + int chunkSizeX = Mathf.CeilToInt((float)generationRange.x / ChunkCoord.CHUNK_SIZE); + int chunkSizeY = Mathf.CeilToInt((float)generationRange.y / ChunkCoord.CHUNK_SIZE); + int chunkSizeZ = Mathf.CeilToInt((float)generationRange.z / ChunkCoord.CHUNK_SIZE); + + Debug.Log($"[ChunkedGenerator] Generating {chunkSizeX}x{chunkSizeY}x{chunkSizeZ} chunks = {chunkSizeX * chunkSizeY * chunkSizeZ} total"); + + int chunksGenerated = 0; + int chunksSkipped = 0; + + // Generate chunks + for (int cx = 0; cx < chunkSizeX; cx++) + { + for (int cy = 0; cy < chunkSizeY; cy++) + { + for (int cz = 0; cz < chunkSizeZ; cz++) + { + // Calculate chunk grid origin (going downward for Y) + Vector3Int chunkGridOrigin = originGrid + new Vector3Int( + cx * ChunkCoord.CHUNK_SIZE, + -cy * ChunkCoord.CHUNK_SIZE, // Negative Y (underground) + cz * ChunkCoord.CHUNK_SIZE + ); + + // Generate block data for this chunk + ChunkState state = GenerateChunkState(chunkGridOrigin); + + // Check if chunk has any blocks + bool hasBlocks = false; + for (int i = 0; i < ChunkState.BLOCKS_PER_CHUNK; i++) + { + if (!state.blocks[i].IsEmpty) + { + hasBlocks = true; + break; + } + } + + if (!hasBlocks) + { + chunksSkipped++; + continue; + } + + // Spawn chunk + SpawnChunk(chunkGridOrigin, state); + chunksGenerated++; + } + } + } + + Debug.Log($"[ChunkedGenerator] Generated {chunksGenerated} chunks, skipped {chunksSkipped} empty chunks"); + } + + /// + /// Generate block state for a chunk + /// + private ChunkState GenerateChunkState(Vector3Int chunkGridOrigin) + { + ChunkState state = ChunkState.CreateEmpty(); + + for (int lx = 0; lx < ChunkCoord.CHUNK_SIZE; lx++) + { + for (int ly = 0; ly < ChunkCoord.CHUNK_SIZE; ly++) + { + for (int lz = 0; lz < ChunkCoord.CHUNK_SIZE; lz++) + { + Vector3Int gridPos = chunkGridOrigin + new Vector3Int(lx, ly, lz); + int index = ChunkCoord.LocalToIndex(lx, ly, lz); + + // Check if within generation range + Vector3Int originGrid = BuildManager.Instance.WorldToGrid3D(transform.position); + Vector3Int offset = gridPos - originGrid; + + if (offset.x < 0 || offset.x >= generationRange.x || + offset.y > 0 || offset.y <= -generationRange.y || + offset.z < 0 || offset.z >= generationRange.z) + { + state.blocks[index] = BlockData.Empty; + continue; + } + + // Sample noise + float noise = Get3DNoise(gridPos.x, gridPos.y, gridPos.z); + + // Check hollow threshold + if (noise < hollowThreshold) + { + state.blocks[index] = BlockData.Empty; + continue; + } + + // Determine block type using separate resource noise for better distribution + float resourceNoise = GetResourceNoise(gridPos.x, gridPos.y, gridPos.z); + + float currentThreshold = baseResourceThreshold; + if (increaseResourceWithDepth) + { + currentThreshold += gridPos.y * depthFactor; // Lower threshold = more resources at depth + } + + // Resource spawns only if: resource noise > threshold AND random chance passes + bool isResource = resourceNoise > currentThreshold && + Random.value < resourceSpawnChance; + + if (isResource) + { + state.blocks[index] = BlockData.Resource(resourceBlockHealth); + } + else + { + state.blocks[index] = BlockData.Normal(normalBlockHealth); + } + } + } + } + + return state; + } + + /// + /// Spawn a chunk NetworkObject + /// + private void SpawnChunk(Vector3Int chunkGridOrigin, ChunkState state) + { + if (chunkPrefab == null) + { + Debug.LogError("[ChunkedGenerator] Chunk prefab not assigned!"); + return; + } + + // Calculate world position for chunk + Vector3 worldPos = BuildManager.Instance.GridToWorld(chunkGridOrigin); + + // Instantiate chunk + GameObject chunkObj = Instantiate(chunkPrefab, worldPos, Quaternion.identity, _chunkContainer); + + // Spawn on network + NetworkObject netObj = chunkObj.GetComponent(); + netObj.Spawn(); + + // Initialize chunk with block data + MineableChunk chunk = chunkObj.GetComponent(); + if (chunk != null) + { + chunk.InitializeBlocks(state); + + // Register in dictionary + ChunkCoord coord = ChunkCoord.FromGridPos(chunkGridOrigin); + _chunkRegistry[coord.chunkPos] = chunk; + } + } + + /// + /// 3D Perlin noise sampling for terrain shape + /// + private float Get3DNoise(int x, int y, int z) + { + float xCoord = (x + _seedX + 10000f) * noiseScale; + float yCoord = (y + _seedY + 10000f) * noiseScale; + float zCoord = (z + _seedZ + 10000f) * noiseScale; + + float ab = Mathf.PerlinNoise(xCoord, yCoord); + float bc = Mathf.PerlinNoise(yCoord, zCoord); + float ac = Mathf.PerlinNoise(xCoord, zCoord); + + return (ab + bc + ac) / 3f; + } + + /// + /// Separate 3D noise for resource distribution (more spread out) + /// + private float GetResourceNoise(int x, int y, int z) + { + float xCoord = (x + _resourceSeedX + 10000f) * resourceNoiseScale; + float yCoord = (y + _resourceSeedY + 10000f) * resourceNoiseScale; + float zCoord = (z + _resourceSeedZ + 10000f) * resourceNoiseScale; + + float ab = Mathf.PerlinNoise(xCoord, yCoord); + float bc = Mathf.PerlinNoise(yCoord, zCoord); + float ac = Mathf.PerlinNoise(xCoord, zCoord); + + return (ab + bc + ac) / 3f; + } + + #region Public Query Methods + + /// + /// Get chunk at grid position + /// + public MineableChunk GetChunkAtGrid(Vector3Int gridPos) + { + ChunkCoord coord = ChunkCoord.FromGridPos(gridPos); + _chunkRegistry.TryGetValue(coord.chunkPos, out MineableChunk chunk); + return chunk; + } + + /// + /// Get chunk at world position + /// + public MineableChunk GetChunkAtWorld(Vector3 worldPos) + { + ChunkCoord coord = ChunkCoord.FromWorldPos(worldPos); + _chunkRegistry.TryGetValue(coord.chunkPos, out MineableChunk chunk); + return chunk; + } + + /// + /// Check if block at grid position is solid (for cross-chunk face culling) + /// + public bool IsBlockSolid(Vector3Int gridPos) + { + MineableChunk chunk = GetChunkAtGrid(gridPos); + if (chunk == null) return false; + + Vector3Int localPos = ChunkCoord.GridToLocal(gridPos); + int index = ChunkCoord.LocalToIndex(localPos); + BlockData block = chunk.GetBlock(index); + + return !block.IsEmpty && block.IsDiscovered; + } + + /// + /// Reveal blocks in radius around a world position (for fog of war) + /// + public void RevealBlocksInRadius(Vector3 worldPos, float radius) + { + if (!IsServer) return; + + // Find all chunks that could be affected + foreach (var chunk in _chunkRegistry.Values) + { + if (chunk == null) continue; + + // Quick bounds check - chunk center distance + float chunkDist = Vector3.Distance(worldPos, chunk.transform.position); + float maxChunkRadius = ChunkCoord.CHUNK_SIZE * 0.866f; // diagonal + + if (chunkDist <= radius + maxChunkRadius) + { + chunk.RevealBlocksInRadius(worldPos, radius); + } + } + } + + /// + /// Get all active chunks + /// + public IEnumerable GetAllChunks() + { + return _chunkRegistry.Values; + } + + #endregion + + #region Gizmos + + private void OnDrawGizmosSelected() + { + BuildManager bm = BuildManager.Instance; + if (bm == null) bm = FindFirstObjectByType(); + if (bm == null) return; + + Vector3Int originGrid = bm.WorldToGrid3D(transform.position); + + // Draw chunk boundaries + int chunkSizeX = Mathf.CeilToInt((float)generationRange.x / ChunkCoord.CHUNK_SIZE); + int chunkSizeY = Mathf.CeilToInt((float)generationRange.y / ChunkCoord.CHUNK_SIZE); + int chunkSizeZ = Mathf.CeilToInt((float)generationRange.z / ChunkCoord.CHUNK_SIZE); + + Gizmos.color = new Color(0, 1, 0, 0.3f); + + for (int cx = 0; cx < chunkSizeX; cx++) + { + for (int cy = 0; cy < chunkSizeY; cy++) + { + for (int cz = 0; cz < chunkSizeZ; cz++) + { + Vector3Int chunkGridOrigin = originGrid + new Vector3Int( + cx * ChunkCoord.CHUNK_SIZE, + -cy * ChunkCoord.CHUNK_SIZE, + cz * ChunkCoord.CHUNK_SIZE + ); + + Vector3 worldPos = bm.GridToWorld(chunkGridOrigin); + // Offset to center of chunk + worldPos += new Vector3( + (ChunkCoord.CHUNK_SIZE - 1) * 0.5f, + (ChunkCoord.CHUNK_SIZE - 1) * 0.5f, + (ChunkCoord.CHUNK_SIZE - 1) * 0.5f + ); + + Gizmos.DrawWireCube(worldPos, Vector3.one * ChunkCoord.CHUNK_SIZE); + } + } + } + } + + #endregion +} diff --git a/Assets/Scripts/Underground/ChunkedUndergroundGenerator.cs.meta b/Assets/Scripts/Underground/ChunkedUndergroundGenerator.cs.meta new file mode 100644 index 0000000..cdfd770 --- /dev/null +++ b/Assets/Scripts/Underground/ChunkedUndergroundGenerator.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3d90f6724f14d49489261782c9672f11 \ No newline at end of file diff --git a/Assets/Scripts/Underground/MineableChunk.cs b/Assets/Scripts/Underground/MineableChunk.cs new file mode 100644 index 0000000..eeeaf11 --- /dev/null +++ b/Assets/Scripts/Underground/MineableChunk.cs @@ -0,0 +1,680 @@ +using System.Collections; +using Unity.Netcode; +using UnityEngine; + +/// +/// NetworkBehaviour for a mineable chunk containing 4x4x4 blocks. +/// Manages block state, mesh generation, damage, and item drops. +/// +[RequireComponent(typeof(MeshFilter))] +[RequireComponent(typeof(MeshRenderer))] +[RequireComponent(typeof(MeshCollider))] +public class MineableChunk : NetworkBehaviour, ChunkMeshBuilder.INeighborProvider +{ + [Header("Block Settings")] + [SerializeField] private byte normalBlockHealth = 100; + [SerializeField] private byte resourceBlockHealth = 150; + + [Header("Drop Settings")] + [SerializeField] private ItemData normalDropItem; + [SerializeField] private ItemData resourceDropItem; + [SerializeField] private GameObject genericDropPrefab; + + [Header("Materials")] + [SerializeField] private Material normalBlockMaterial; + [SerializeField] private Material resourceBlockMaterial; + + [Header("Visual Settings")] + [SerializeField] private float meshRebuildDelay = 0.1f; + + [Header("Highlight Settings")] + [SerializeField] private Material highlightMaterial; + private GameObject _highlightCube; + private MeshRenderer _highlightRenderer; + + [Header("Fog of War")] + [SerializeField] private float visibilityTimeout = 0.5f; // Time before block goes from "visible" to "discovered" + [SerializeField] private Color discoveredTint = new Color(0.3f, 0.3f, 0.3f, 1f); // Darkened color for discovered-but-not-visible + private float[] _blockVisibilityTime; // Last time each block was visible (local only) + private MaterialPropertyBlock _propBlock; + + // Chunk state synced to all clients + private NetworkVariable _networkState = new NetworkVariable( + default, + NetworkVariableReadPermission.Everyone, + NetworkVariableWritePermission.Server + ); + + // Local cache for quick access + private ChunkState _localState; + private ChunkCoord _chunkCoord; + private bool _isInitialized = false; + + // Components + private MeshFilter _meshFilter; + private MeshRenderer _meshRenderer; + private MeshCollider _meshCollider; + private Outline _outline; + + // Mesh rebuild state + private bool _meshDirty = false; + private Coroutine _rebuildCoroutine; + + // Highlight state + private int _highlightedBlockIndex = -1; + + // Reference to chunk manager for neighbor queries + private static ChunkedUndergroundGenerator _chunkManager; + + public ChunkCoord Coord => _chunkCoord; + public ChunkState State => _localState; + + // Visibility check interval + private float _lastVisibilityCheck; + private const float VISIBILITY_CHECK_INTERVAL = 0.3f; + private bool _hasVisibleBlocks = false; + + private void Awake() + { + _meshFilter = GetComponent(); + _meshRenderer = GetComponent(); + _meshCollider = GetComponent(); + _outline = GetComponent(); + + // Initialize local state + _localState = ChunkState.CreateEmpty(); + + // Initialize visibility tracking (local only, not networked) + _blockVisibilityTime = new float[ChunkState.BLOCKS_PER_CHUNK]; + _propBlock = new MaterialPropertyBlock(); + + // Set materials: 0=normal-visible, 1=resource-visible, 2=normal-dark, 3=resource-dark + SetupMaterials(); + + // Create highlight cube for per-block highlighting + CreateHighlightCube(); + } + + private void SetupMaterials() + { + if (normalBlockMaterial != null && resourceBlockMaterial != null) + { + // Create darkened versions of materials for discovered-but-not-visible state + Material normalDark = new Material(normalBlockMaterial); + ApplyDarkTint(normalDark, normalBlockMaterial); + + Material resourceDark = new Material(resourceBlockMaterial); + ApplyDarkTint(resourceDark, resourceBlockMaterial); + + _meshRenderer.materials = new Material[] + { + normalBlockMaterial, // Submesh 0: Normal blocks (visible) + resourceBlockMaterial, // Submesh 1: Resource blocks (visible) + normalDark, // Submesh 2: Normal blocks (discovered/dark) + resourceDark // Submesh 3: Resource blocks (discovered/dark) + }; + } + } + + private void ApplyDarkTint(Material darkMat, Material sourceMat) + { + // Try URP _BaseColor first, then fallback to _Color + if (sourceMat.HasProperty("_BaseColor")) + { + Color baseColor = sourceMat.GetColor("_BaseColor"); + darkMat.SetColor("_BaseColor", baseColor * discoveredTint); + } + else if (sourceMat.HasProperty("_Color")) + { + Color baseColor = sourceMat.GetColor("_Color"); + darkMat.SetColor("_Color", baseColor * discoveredTint); + } + } + + private void CreateHighlightCube() + { + _highlightCube = GameObject.CreatePrimitive(PrimitiveType.Cube); + _highlightCube.name = "BlockHighlight"; + _highlightCube.transform.SetParent(transform); + _highlightCube.transform.localScale = Vector3.one * 1.02f; // Slightly larger than block + + // Remove collider - this is visual only + var collider = _highlightCube.GetComponent(); + if (collider != null) Destroy(collider); + + // Setup renderer + _highlightRenderer = _highlightCube.GetComponent(); + if (highlightMaterial != null) + { + _highlightRenderer.material = highlightMaterial; + } + else + { + // Create default highlight material if none assigned + var mat = new Material(Shader.Find("Standard")); + mat.color = new Color(1f, 1f, 0f, 0.3f); // Yellow transparent + mat.SetFloat("_Mode", 3); // Transparent mode + mat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); + mat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); + mat.SetInt("_ZWrite", 0); + mat.DisableKeyword("_ALPHATEST_ON"); + mat.EnableKeyword("_ALPHABLEND_ON"); + mat.DisableKeyword("_ALPHAPREMULTIPLY_ON"); + mat.renderQueue = 3000; + _highlightRenderer.material = mat; + } + + _highlightCube.SetActive(false); + } + + public override void OnNetworkSpawn() + { + // Calculate chunk coordinates from world position + _chunkCoord = ChunkCoord.FromWorldPos(transform.position); + + // Subscribe to state changes + _networkState.OnValueChanged += OnChunkStateChanged; + + // Initial sync + SyncLocalState(); + RebuildMeshImmediate(); + + // Find chunk manager if not set + if (_chunkManager == null) + { + _chunkManager = FindFirstObjectByType(); + } + } + + private void Update() + { + // Only check visibility timeout periodically and if we had visible blocks + if (!_hasVisibleBlocks) return; + if (Time.time - _lastVisibilityCheck < VISIBILITY_CHECK_INTERVAL) return; + + _lastVisibilityCheck = Time.time; + CheckVisibilityTimeout(); + } + + private void CheckVisibilityTimeout() + { + bool anyStillVisible = false; + bool needsRebuild = false; + + for (int i = 0; i < ChunkState.BLOCKS_PER_CHUNK; i++) + { + if (_blockVisibilityTime[i] <= 0) continue; + + bool wasVisible = (Time.time - _blockVisibilityTime[i]) < visibilityTimeout; + + if (wasVisible) + { + anyStillVisible = true; + } + else if (_blockVisibilityTime[i] > 0) + { + // Block just transitioned from visible to discovered + // Check if it was visible last frame (within timeout + check interval) + float timeSinceVisible = Time.time - _blockVisibilityTime[i]; + if (timeSinceVisible < visibilityTimeout + VISIBILITY_CHECK_INTERVAL * 2) + { + needsRebuild = true; + } + } + } + + _hasVisibleBlocks = anyStillVisible; + + if (needsRebuild) + { + ScheduleMeshRebuild(); + } + } + + public override void OnNetworkDespawn() + { + _networkState.OnValueChanged -= OnChunkStateChanged; + } + + /// + /// Initialize chunk with generated block data (called by generator on server) + /// + public void InitializeBlocks(ChunkState initialState) + { + if (!IsServer) return; + + _networkState.Value = initialState; + SyncLocalState(); + RebuildMeshImmediate(); + } + + /// + /// Get block data at local coordinates + /// + public BlockData GetBlock(int x, int y, int z) + { + if (!ChunkCoord.IsValidLocal(x, y, z)) return BlockData.Empty; + if (_localState.blocks == null) return BlockData.Empty; + int index = ChunkCoord.LocalToIndex(x, y, z); + return _localState.blocks[index]; + } + + /// + /// Get block data at local index + /// + public BlockData GetBlock(int index) + { + if (index < 0 || index >= ChunkState.BLOCKS_PER_CHUNK) return BlockData.Empty; + if (_localState.blocks == null) return BlockData.Empty; + return _localState.blocks[index]; + } + + #region Damage System + + /// + /// Request damage to a specific block (called by clients) + /// + [Rpc(SendTo.Server)] + public void DamageBlockServerRpc(int localIndex, byte damage) + { + if (localIndex < 0 || localIndex >= ChunkState.BLOCKS_PER_CHUNK) return; + + var state = _networkState.Value; + BlockData block = state.blocks[localIndex]; + + if (block.IsEmpty) return; + + // Apply damage + int newHealth = Mathf.Max(0, block.health - damage); + block.health = (byte)newHealth; + + // Update state + state.blocks[localIndex] = block; + _networkState.Value = state; + + // Sync to clients + UpdateBlockClientRpc(localIndex, block); + + // Play hit effect + PlayHitEffectClientRpc(localIndex); + + // Check for destruction + if (newHealth <= 0) + { + DestroyBlock(localIndex, state.blocks[localIndex].blockType); + } + } + + /// + /// Server-side damage application (for direct server calls) + /// + public void DamageBlock(int localIndex, float damageAmount) + { + if (!IsServer) return; + DamageBlockServerRpc(localIndex, (byte)Mathf.Min(255, damageAmount)); + } + + /// + /// Update a single block on all clients + /// + [ClientRpc] + private void UpdateBlockClientRpc(int localIndex, BlockData block) + { + if (localIndex < 0 || localIndex >= ChunkState.BLOCKS_PER_CHUNK) return; + + _localState.blocks[localIndex] = block; + + // Schedule mesh rebuild + ScheduleMeshRebuild(); + } + + /// + /// Play hit visual effect + /// + [ClientRpc] + private void PlayHitEffectClientRpc(int localIndex) + { + // Get block world position for effect + Vector3Int localPos = ChunkCoord.IndexToLocal(localIndex); + Vector3 worldPos = _chunkCoord.LocalToWorld(localPos); + + // TODO: Spawn particle effect at worldPos + // For now, just a debug visualization + } + + /// + /// Handle block destruction (server-side) + /// + private void DestroyBlock(int localIndex, byte blockType) + { + if (!IsServer) return; + + // Spawn dropped item + Vector3Int localPos = ChunkCoord.IndexToLocal(localIndex); + Vector3 worldPos = _chunkCoord.LocalToWorld(localPos); + SpawnDrop(worldPos, blockType); + + // Clear block + var state = _networkState.Value; + state.blocks[localIndex] = BlockData.Empty; + _networkState.Value = state; + + // Full sync and rebuild + SyncStateClientRpc(state); + } + + /// + /// Spawn item drop at position + /// + private void SpawnDrop(Vector3 position, byte blockType) + { + if (genericDropPrefab == null) return; + + ItemData dropItem = blockType == BlockData.TYPE_RESOURCE ? resourceDropItem : normalDropItem; + if (dropItem == null) return; + + GameObject dropObj = Instantiate(genericDropPrefab, position + Vector3.up * 0.5f, Quaternion.identity); + NetworkObject netObj = dropObj.GetComponent(); + netObj.Spawn(); + + if (dropObj.TryGetComponent(out var droppedItem)) + { + droppedItem.Initialize(dropItem.itemID); + } + } + + #endregion + + #region Fog of War + + /// + /// Check if block is currently visible (in player's sight right now) + /// + public bool IsBlockCurrentlyVisible(int localIndex) + { + if (_blockVisibilityTime == null || localIndex < 0 || localIndex >= _blockVisibilityTime.Length) + return false; + return (Time.time - _blockVisibilityTime[localIndex]) < visibilityTimeout; + } + + /// + /// Update local visibility for blocks in range (called by local player) + /// + public void UpdateLocalVisibility(Vector3 playerPos, float radius) + { + bool needsRebuild = false; + bool anyVisible = false; + + for (int i = 0; i < ChunkState.BLOCKS_PER_CHUNK; i++) + { + BlockData block = _localState.blocks[i]; + if (block.IsEmpty || !block.IsDiscovered) continue; + + Vector3 blockWorldPos = GetBlockWorldPosition(i); + bool wasVisible = IsBlockCurrentlyVisible(i); + + if (Vector3.Distance(playerPos, blockWorldPos) <= radius) + { + _blockVisibilityTime[i] = Time.time; + anyVisible = true; + + // If block just became visible, need to rebuild mesh + if (!wasVisible) + { + needsRebuild = true; + } + } + } + + // Track if we have any visible blocks (for timeout checking in Update) + _hasVisibleBlocks = anyVisible || _hasVisibleBlocks; + + if (needsRebuild) + { + ScheduleMeshRebuild(); + } + } + + /// + /// Request reveal of a specific block (called by clients) + /// + [Rpc(SendTo.Server)] + public void RevealBlockServerRpc(int localIndex) + { + if (localIndex < 0 || localIndex >= ChunkState.BLOCKS_PER_CHUNK) return; + + var state = _networkState.Value; + BlockData block = state.blocks[localIndex]; + + if (block.IsEmpty || block.IsDiscovered) return; + + // Mark as discovered + block.IsDiscovered = true; + state.blocks[localIndex] = block; + _networkState.Value = state; + + // Sync to clients + UpdateBlockClientRpc(localIndex, block); + } + + /// + /// Reveal all blocks within radius of a world position + /// + public void RevealBlocksInRadius(Vector3 worldPos, float radius) + { + if (!IsServer) return; + + bool anyRevealed = false; + var state = _networkState.Value; + + for (int i = 0; i < ChunkState.BLOCKS_PER_CHUNK; i++) + { + BlockData block = state.blocks[i]; + if (block.IsEmpty || block.IsDiscovered) continue; + + Vector3Int localPos = ChunkCoord.IndexToLocal(i); + Vector3 blockWorldPos = _chunkCoord.LocalToWorld(localPos); + + if (Vector3.Distance(worldPos, blockWorldPos) <= radius) + { + block.IsDiscovered = true; + state.blocks[i] = block; + anyRevealed = true; + } + } + + if (anyRevealed) + { + _networkState.Value = state; + SyncStateClientRpc(state); + } + } + + /// + /// Check if block at local index is discovered + /// + public bool IsBlockDiscovered(int localIndex) + { + if (localIndex < 0 || localIndex >= ChunkState.BLOCKS_PER_CHUNK) return false; + return _localState.blocks[localIndex].IsDiscovered; + } + + #endregion + + #region Mesh Building + + /// + /// Full state sync from server + /// + [ClientRpc] + private void SyncStateClientRpc(ChunkState state) + { + _localState = state; + RebuildMeshImmediate(); + } + + private void OnChunkStateChanged(ChunkState oldState, ChunkState newState) + { + SyncLocalState(); + ScheduleMeshRebuild(); + } + + private void SyncLocalState() + { + _localState = _networkState.Value; + // Ensure blocks array is initialized + if (_localState.blocks == null) + { + _localState = ChunkState.CreateEmpty(); + } + } + + private void ScheduleMeshRebuild() + { + if (_meshDirty) return; + _meshDirty = true; + + if (_rebuildCoroutine != null) + { + StopCoroutine(_rebuildCoroutine); + } + _rebuildCoroutine = StartCoroutine(DelayedMeshRebuild()); + } + + private IEnumerator DelayedMeshRebuild() + { + yield return new WaitForSeconds(meshRebuildDelay); + RebuildMeshImmediate(); + _meshDirty = false; + _rebuildCoroutine = null; + } + + private void RebuildMeshImmediate() + { + if (_meshFilter == null) return; + + // Build visual mesh with fog of war (4 submeshes: normal-visible, resource-visible, normal-dark, resource-dark) + Mesh visualMesh = ChunkMeshBuilder.BuildMeshWithVisibility( + _localState, + _chunkCoord, + this, + true, // only discovered + IsBlockCurrentlyVisible // visibility checker + ); + _meshFilter.mesh = visualMesh; + + // Build collision mesh (all blocks, including undiscovered, no visibility distinction) + if (_meshCollider != null) + { + Mesh collisionMesh = ChunkMeshBuilder.BuildMesh(_localState, _chunkCoord, this, false); + _meshCollider.sharedMesh = null; // Force refresh + _meshCollider.sharedMesh = collisionMesh; + } + } + + #endregion + + #region INeighborProvider Implementation + + /// + /// Check if block at world grid position is solid (for cross-chunk face culling) + /// + public bool IsBlockSolid(Vector3Int gridPos) + { + // Check if position is in this chunk + ChunkCoord coord = ChunkCoord.FromGridPos(gridPos); + if (coord == _chunkCoord) + { + Vector3Int local = ChunkCoord.GridToLocal(gridPos); + int index = ChunkCoord.LocalToIndex(local); + var block = _localState.blocks[index]; + return !block.IsEmpty && block.IsDiscovered; + } + + // Query chunk manager for other chunks + if (_chunkManager != null) + { + return _chunkManager.IsBlockSolid(gridPos); + } + + // Default: assume not solid at chunk boundaries + return false; + } + + #endregion + + #region Interaction Support + + /// + /// Convert world hit point to local block index + /// + public int WorldPointToBlockIndex(Vector3 worldPoint) + { + // Convert world point to local space relative to chunk transform + Vector3 localPoint = transform.InverseTransformPoint(worldPoint); + + // Each block is 1 unit, centered at integer coordinates + // So block at (0,0,0) spans from -0.5 to 0.5 + int lx = Mathf.Clamp(Mathf.RoundToInt(localPoint.x), 0, ChunkCoord.CHUNK_SIZE - 1); + int ly = Mathf.Clamp(Mathf.RoundToInt(localPoint.y), 0, ChunkCoord.CHUNK_SIZE - 1); + int lz = Mathf.Clamp(Mathf.RoundToInt(localPoint.z), 0, ChunkCoord.CHUNK_SIZE - 1); + + return ChunkCoord.LocalToIndex(lx, ly, lz); + } + + /// + /// Get world position of block at local index + /// + public Vector3 GetBlockWorldPosition(int localIndex) + { + Vector3Int localPos = ChunkCoord.IndexToLocal(localIndex); + // Convert local position to world using chunk transform + return transform.TransformPoint(new Vector3(localPos.x, localPos.y, localPos.z)); + } + + /// + /// Set highlight state for visual feedback + /// + public void SetHighlight(bool isOn, int blockIndex = -1) + { + _highlightedBlockIndex = isOn ? blockIndex : -1; + + // Use per-block highlight cube instead of outline + if (_highlightCube != null) + { + if (isOn && blockIndex >= 0) + { + // Position highlight at specific block + Vector3Int localPos = ChunkCoord.IndexToLocal(blockIndex); + _highlightCube.transform.localPosition = new Vector3(localPos.x, localPos.y, localPos.z); + _highlightCube.SetActive(true); + } + else + { + _highlightCube.SetActive(false); + } + } + + // Disable outline component if present (we use highlight cube instead) + if (_outline != null) + { + _outline.enabled = false; + } + } + + /// + /// Get the currently highlighted block index + /// + public int HighlightedBlockIndex => _highlightedBlockIndex; + + #endregion + + #region Static Registration + + public static void SetChunkManager(ChunkedUndergroundGenerator manager) + { + _chunkManager = manager; + } + + #endregion +} diff --git a/Assets/Scripts/Underground/MineableChunk.cs.meta b/Assets/Scripts/Underground/MineableChunk.cs.meta new file mode 100644 index 0000000..ea56fb0 --- /dev/null +++ b/Assets/Scripts/Underground/MineableChunk.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 53bb340e9008b024ba035f6ee8fa21a4 \ No newline at end of file diff --git a/ProjectSettings/TagManager.asset b/ProjectSettings/TagManager.asset index c0d4079..8a8e94f 100644 --- a/ProjectSettings/TagManager.asset +++ b/ProjectSettings/TagManager.asset @@ -23,7 +23,7 @@ TagManager: - Mineable - TunnelNode - DroppedItem - - + - Wall - - -