diff --git a/Assets/DefaultNetworkPrefabs.asset b/Assets/DefaultNetworkPrefabs.asset
index abe922b..7e48bb6 100644
--- a/Assets/DefaultNetworkPrefabs.asset
+++ b/Assets/DefaultNetworkPrefabs.asset
@@ -19,3 +19,13 @@ MonoBehaviour:
SourcePrefabToOverride: {fileID: 0}
SourceHashToOverride: 0
OverridingTargetPrefab: {fileID: 0}
+ - Override: 0
+ Prefab: {fileID: 6482414459531823157, guid: 1511641f145758b469e64376d2a0d434, type: 3}
+ SourcePrefabToOverride: {fileID: 0}
+ SourceHashToOverride: 0
+ OverridingTargetPrefab: {fileID: 0}
+ - Override: 0
+ Prefab: {fileID: 116861493430507844, guid: 3398cc5831880954487717577f61b6d7, type: 3}
+ SourcePrefabToOverride: {fileID: 0}
+ SourceHashToOverride: 0
+ OverridingTargetPrefab: {fileID: 0}
diff --git a/Assets/Resources.meta b/Assets/Resources.meta
new file mode 100644
index 0000000..d3c452a
--- /dev/null
+++ b/Assets/Resources.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 01de85ee5d8a2014594d9910b1a6ff55
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/TowerDefinitions.meta b/Assets/Resources/TowerDefinitions.meta
new file mode 100644
index 0000000..6c55120
--- /dev/null
+++ b/Assets/Resources/TowerDefinitions.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 3b182e413a90b2242a104b915d5b9233
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/TowerDefinitions/BasicTower.asset b/Assets/Resources/TowerDefinitions/BasicTower.asset
new file mode 100644
index 0000000..dd1f95f
--- /dev/null
+++ b/Assets/Resources/TowerDefinitions/BasicTower.asset
@@ -0,0 +1,26 @@
+%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: 7b353a757b6e6774d97e6fb8ba138fcc, type: 3}
+ m_Name: BasicTower
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Towers.TowerDefinition
+ DisplayName: A basic tower for testing.
+ Description:
+ FootprintSize: {x: 2, y: 2}
+ GoldCost: 25
+ BuildTime: 0
+ TowerPrefab: {fileID: 6482414459531823157, guid: 1511641f145758b469e64376d2a0d434, type: 3}
+ Damage: 0
+ Range: 0
+ FireRate: 0
+ SplashRadius: 0
+ SlowFactor: 1
+ ProjectilePrefab: {fileID: 0}
diff --git a/Assets/Resources/TowerDefinitions/BasicTower.asset.meta b/Assets/Resources/TowerDefinitions/BasicTower.asset.meta
new file mode 100644
index 0000000..fc0e53c
--- /dev/null
+++ b/Assets/Resources/TowerDefinitions/BasicTower.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 0f693e29ca953e1439e10cb8f12e4b30
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Art/Materials/M_Circle.mat b/Assets/_Project/Art/Materials/M_Circle.mat
new file mode 100644
index 0000000..5872c9e
--- /dev/null
+++ b/Assets/_Project/Art/Materials/M_Circle.mat
@@ -0,0 +1,149 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &-1452440179449743576
+MonoBehaviour:
+ m_ObjectHideFlags: 11
+ 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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
+ version: 10
+--- !u!21 &2100000
+Material:
+ serializedVersion: 8
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: M_Circle
+ m_Shader: {fileID: -6465566751694194690, guid: 9b4e681081e2b4c469111bb649e2f7ee, type: 3}
+ m_Parent: {fileID: 0}
+ m_ModifiedSerializedProperties: 0
+ m_ValidKeywords: []
+ m_InvalidKeywords: []
+ m_LightmapFlags: 4
+ m_EnableInstancingVariants: 0
+ m_DoubleSidedGI: 0
+ m_CustomRenderQueue: -1
+ stringTagMap: {}
+ disabledShaderPasses:
+ - MOTIONVECTORS
+ m_LockedProperties:
+ m_SavedProperties:
+ serializedVersion: 3
+ m_TexEnvs:
+ - Base_Map:
+ m_Texture: {fileID: 2800000, guid: d35d9be92695677409586244012d21f3, type: 3}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - Normal_Map:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _BaseMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _BumpMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _DetailAlbedoMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _DetailMask:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _DetailNormalMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _EmissionMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _MainTex:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _MetallicGlossMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _OcclusionMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _ParallaxMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _SpecGlossMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - unity_Lightmaps:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - unity_LightmapsInd:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - unity_ShadowMasks:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ m_Ints: []
+ m_Floats:
+ - Normal_Blend: 0.5
+ - _AddPrecomputedVelocity: 0
+ - _AlphaClip: 0
+ - _AlphaToMask: 0
+ - _Blend: 0
+ - _BlendModePreserveSpecular: 1
+ - _BumpScale: 1
+ - _ClearCoatMask: 0
+ - _ClearCoatSmoothness: 0
+ - _Cull: 2
+ - _Cutoff: 0.5
+ - _DecalMeshBiasType: 0
+ - _DecalMeshDepthBias: 0
+ - _DecalMeshViewBias: 0
+ - _DetailAlbedoMapScale: 1
+ - _DetailNormalMapScale: 1
+ - _DrawOrder: 0
+ - _DstBlend: 0
+ - _DstBlendAlpha: 0
+ - _EnvironmentReflections: 1
+ - _GlossMapScale: 0
+ - _Glossiness: 0
+ - _GlossyReflections: 0
+ - _Metallic: 0
+ - _OcclusionStrength: 1
+ - _Parallax: 0.005
+ - _QueueOffset: 0
+ - _ReceiveShadows: 1
+ - _Smoothness: 0.5
+ - _SmoothnessTextureChannel: 0
+ - _SpecularHighlights: 1
+ - _SrcBlend: 1
+ - _SrcBlendAlpha: 1
+ - _Surface: 0
+ - _WorkflowMode: 1
+ - _XRMotionVectorsPass: 1
+ - _ZWrite: 1
+ m_Colors:
+ - _BaseColor: {r: 1, g: 1, b: 1, a: 1}
+ - _Color: {r: 1, g: 1, b: 1, a: 1}
+ - _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
+ - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
+ m_BuildTextureStacks: []
+ m_AllowLocking: 1
diff --git a/Assets/_Project/Art/Materials/M_Circle.mat.meta b/Assets/_Project/Art/Materials/M_Circle.mat.meta
new file mode 100644
index 0000000..02a4c98
--- /dev/null
+++ b/Assets/_Project/Art/Materials/M_Circle.mat.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: f99227cbde481ce47a2527e6bca709d2
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 2100000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Art/Materials/M_TowerGhost_Invalid.mat b/Assets/_Project/Art/Materials/M_TowerGhost_Invalid.mat
new file mode 100644
index 0000000..fe5b2c2
--- /dev/null
+++ b/Assets/_Project/Art/Materials/M_TowerGhost_Invalid.mat
@@ -0,0 +1,141 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &-4721961284039222980
+MonoBehaviour:
+ m_ObjectHideFlags: 11
+ 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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
+ version: 10
+--- !u!21 &2100000
+Material:
+ serializedVersion: 8
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: M_TowerGhost_Invalid
+ m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
+ m_Parent: {fileID: 0}
+ m_ModifiedSerializedProperties: 0
+ m_ValidKeywords:
+ - _ALPHAPREMULTIPLY_ON
+ - _SURFACE_TYPE_TRANSPARENT
+ m_InvalidKeywords: []
+ m_LightmapFlags: 4
+ m_EnableInstancingVariants: 0
+ m_DoubleSidedGI: 0
+ m_CustomRenderQueue: 3000
+ stringTagMap:
+ RenderType: Transparent
+ disabledShaderPasses:
+ - MOTIONVECTORS
+ - DepthOnly
+ - SHADOWCASTER
+ m_LockedProperties:
+ m_SavedProperties:
+ serializedVersion: 3
+ m_TexEnvs:
+ - _BaseMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _BumpMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _DetailAlbedoMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _DetailMask:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _DetailNormalMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _EmissionMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _MainTex:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _MetallicGlossMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _OcclusionMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _ParallaxMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _SpecGlossMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - unity_Lightmaps:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - unity_LightmapsInd:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - unity_ShadowMasks:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ m_Ints: []
+ m_Floats:
+ - _AddPrecomputedVelocity: 0
+ - _AlphaClip: 0
+ - _AlphaToMask: 0
+ - _Blend: 0
+ - _BlendModePreserveSpecular: 1
+ - _BumpScale: 1
+ - _ClearCoatMask: 0
+ - _ClearCoatSmoothness: 0
+ - _Cull: 2
+ - _Cutoff: 0.5
+ - _DetailAlbedoMapScale: 1
+ - _DetailNormalMapScale: 1
+ - _DstBlend: 10
+ - _DstBlendAlpha: 10
+ - _EnvironmentReflections: 1
+ - _GlossMapScale: 0
+ - _Glossiness: 0
+ - _GlossyReflections: 0
+ - _Metallic: 0
+ - _OcclusionStrength: 1
+ - _Parallax: 0.005
+ - _QueueOffset: 0
+ - _ReceiveShadows: 1
+ - _Smoothness: 0.5
+ - _SmoothnessTextureChannel: 0
+ - _SpecularHighlights: 1
+ - _SrcBlend: 1
+ - _SrcBlendAlpha: 1
+ - _Surface: 1
+ - _WorkflowMode: 1
+ - _XRMotionVectorsPass: 1
+ - _ZWrite: 0
+ m_Colors:
+ - _BaseColor: {r: 1, g: 0, b: 0, a: 0.47058824}
+ - _Color: {r: 1, g: 0, b: 0, a: 0.47058824}
+ - _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
+ - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
+ m_BuildTextureStacks: []
+ m_AllowLocking: 1
diff --git a/Assets/_Project/Art/Materials/M_TowerGhost_Invalid.mat.meta b/Assets/_Project/Art/Materials/M_TowerGhost_Invalid.mat.meta
new file mode 100644
index 0000000..b77ca6e
--- /dev/null
+++ b/Assets/_Project/Art/Materials/M_TowerGhost_Invalid.mat.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: fa982eef7eda30e4ca8a94a76eb41d7c
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 2100000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Art/Materials/M_TowerGhost_Valid.mat b/Assets/_Project/Art/Materials/M_TowerGhost_Valid.mat
new file mode 100644
index 0000000..f746cda
--- /dev/null
+++ b/Assets/_Project/Art/Materials/M_TowerGhost_Valid.mat
@@ -0,0 +1,141 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &-4721961284039222980
+MonoBehaviour:
+ m_ObjectHideFlags: 11
+ 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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
+ version: 10
+--- !u!21 &2100000
+Material:
+ serializedVersion: 8
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: M_TowerGhost_Valid
+ m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
+ m_Parent: {fileID: 0}
+ m_ModifiedSerializedProperties: 0
+ m_ValidKeywords:
+ - _ALPHAPREMULTIPLY_ON
+ - _SURFACE_TYPE_TRANSPARENT
+ m_InvalidKeywords: []
+ m_LightmapFlags: 4
+ m_EnableInstancingVariants: 0
+ m_DoubleSidedGI: 0
+ m_CustomRenderQueue: 3000
+ stringTagMap:
+ RenderType: Transparent
+ disabledShaderPasses:
+ - MOTIONVECTORS
+ - DepthOnly
+ - SHADOWCASTER
+ m_LockedProperties:
+ m_SavedProperties:
+ serializedVersion: 3
+ m_TexEnvs:
+ - _BaseMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _BumpMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _DetailAlbedoMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _DetailMask:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _DetailNormalMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _EmissionMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _MainTex:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _MetallicGlossMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _OcclusionMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _ParallaxMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _SpecGlossMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - unity_Lightmaps:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - unity_LightmapsInd:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - unity_ShadowMasks:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ m_Ints: []
+ m_Floats:
+ - _AddPrecomputedVelocity: 0
+ - _AlphaClip: 0
+ - _AlphaToMask: 0
+ - _Blend: 0
+ - _BlendModePreserveSpecular: 1
+ - _BumpScale: 1
+ - _ClearCoatMask: 0
+ - _ClearCoatSmoothness: 0
+ - _Cull: 2
+ - _Cutoff: 0.5
+ - _DetailAlbedoMapScale: 1
+ - _DetailNormalMapScale: 1
+ - _DstBlend: 10
+ - _DstBlendAlpha: 10
+ - _EnvironmentReflections: 1
+ - _GlossMapScale: 0
+ - _Glossiness: 0
+ - _GlossyReflections: 0
+ - _Metallic: 0
+ - _OcclusionStrength: 1
+ - _Parallax: 0.005
+ - _QueueOffset: 0
+ - _ReceiveShadows: 1
+ - _Smoothness: 0.5
+ - _SmoothnessTextureChannel: 0
+ - _SpecularHighlights: 1
+ - _SrcBlend: 1
+ - _SrcBlendAlpha: 1
+ - _Surface: 1
+ - _WorkflowMode: 1
+ - _XRMotionVectorsPass: 1
+ - _ZWrite: 0
+ m_Colors:
+ - _BaseColor: {r: 1, g: 1, b: 1, a: 0.47058824}
+ - _Color: {r: 1, g: 1, b: 1, a: 0.47058824}
+ - _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
+ - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
+ m_BuildTextureStacks: []
+ m_AllowLocking: 1
diff --git a/Assets/_Project/Art/Materials/M_TowerGhost_Valid.mat.meta b/Assets/_Project/Art/Materials/M_TowerGhost_Valid.mat.meta
new file mode 100644
index 0000000..37e141e
--- /dev/null
+++ b/Assets/_Project/Art/Materials/M_TowerGhost_Valid.mat.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: a5f56f464098e1148b394962593014a2
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 2100000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Art/Textures/circle_PNG63.png b/Assets/_Project/Art/Textures/circle_PNG63.png
new file mode 100644
index 0000000..71abe07
--- /dev/null
+++ b/Assets/_Project/Art/Textures/circle_PNG63.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2b046c2e7da704ac0f4e53749151f45451e48c33e45b2e72960763caeaf71200
+size 11068
diff --git a/Assets/_Project/Art/Textures/circle_PNG63.png.meta b/Assets/_Project/Art/Textures/circle_PNG63.png.meta
new file mode 100644
index 0000000..79eac1c
--- /dev/null
+++ b/Assets/_Project/Art/Textures/circle_PNG63.png.meta
@@ -0,0 +1,117 @@
+fileFormatVersion: 2
+guid: d35d9be92695677409586244012d21f3
+TextureImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 13
+ mipmaps:
+ mipMapMode: 0
+ enableMipMap: 1
+ sRGBTexture: 1
+ linearTexture: 0
+ fadeOut: 0
+ borderMipMap: 0
+ mipMapsPreserveCoverage: 0
+ alphaTestReferenceValue: 0.5
+ mipMapFadeDistanceStart: 1
+ mipMapFadeDistanceEnd: 3
+ bumpmap:
+ convertToNormalMap: 0
+ externalNormalMap: 0
+ heightScale: 0.25
+ normalMapFilter: 0
+ flipGreenChannel: 0
+ isReadable: 0
+ streamingMipmaps: 0
+ streamingMipmapsPriority: 0
+ vTOnly: 0
+ ignoreMipmapLimit: 0
+ grayScaleToAlpha: 0
+ generateCubemap: 6
+ cubemapConvolution: 0
+ seamlessCubemap: 0
+ textureFormat: 1
+ maxTextureSize: 2048
+ textureSettings:
+ serializedVersion: 2
+ filterMode: 1
+ aniso: 1
+ mipBias: 0
+ wrapU: 0
+ wrapV: 0
+ wrapW: 0
+ nPOTScale: 1
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 0
+ spriteExtrude: 1
+ spriteMeshType: 1
+ alignment: 0
+ spritePivot: {x: 0.5, y: 0.5}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 0
+ spriteTessellationDetail: -1
+ textureType: 0
+ textureShape: 1
+ singleChannelComponent: 0
+ flipbookRows: 1
+ flipbookColumns: 1
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ ignorePngGamma: 0
+ applyGammaDecoding: 0
+ swizzle: 50462976
+ cookieLightType: 0
+ platformSettings:
+ - serializedVersion: 4
+ buildTarget: DefaultTexturePlatform
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Standalone
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ spriteSheet:
+ serializedVersion: 2
+ sprites: []
+ outline: []
+ customData:
+ physicsShape: []
+ bones: []
+ spriteID:
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable: {}
+ mipmapLimitGroupName:
+ pSDRemoveMatte: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Prefabs/Builders.meta b/Assets/_Project/Prefabs/Builders.meta
new file mode 100644
index 0000000..107c64e
--- /dev/null
+++ b/Assets/_Project/Prefabs/Builders.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: b1099735adf11d944909ce868bed668d
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Prefabs/Builders/Builder_Basic.prefab b/Assets/_Project/Prefabs/Builders/Builder_Basic.prefab
new file mode 100644
index 0000000..ccf3cba
--- /dev/null
+++ b/Assets/_Project/Prefabs/Builders/Builder_Basic.prefab
@@ -0,0 +1,282 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &116861493430507844
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 5490805221566030526}
+ - component: {fileID: 1354786839850046103}
+ - component: {fileID: 4167417797825706430}
+ - component: {fileID: 5001137156876984302}
+ - component: {fileID: 2903356073138602249}
+ - component: {fileID: 4225942884111122364}
+ - component: {fileID: 4533726421250799861}
+ - component: {fileID: 6467759961575585905}
+ m_Layer: 0
+ m_Name: Builder_Basic
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &5490805221566030526
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 116861493430507844}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 0.6, y: 0.9, z: 0.6}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 2153758330548988791}
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!33 &1354786839850046103
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 116861493430507844}
+ m_Mesh: {fileID: 10206, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!23 &4167417797825706430
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 116861493430507844}
+ 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: 31321ba15b8f8eb4c954353edc038b1d, 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!114 &5001137156876984302
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 116861493430507844}
+ 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: 2050641840
+ 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 &2903356073138602249
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 116861493430507844}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: e96cb6065543e43c4a752faaa1468eb1, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.Components.NetworkTransform
+ ShowTopMostFoldoutHeaderGroup: 1
+ NetworkTransformExpanded: 0
+ AutoOwnerAuthorityTickOffset: 1
+ PositionInterpolationType: 0
+ RotationInterpolationType: 0
+ ScaleInterpolationType: 0
+ PositionLerpSmoothing: 1
+ PositionMaxInterpolationTime: 0.1
+ RotationLerpSmoothing: 1
+ RotationMaxInterpolationTime: 0.1
+ ScaleLerpSmoothing: 1
+ ScaleMaxInterpolationTime: 0.1
+ AuthorityMode: 0
+ TickSyncChildren: 0
+ UseUnreliableDeltas: 0
+ SyncPositionX: 1
+ SyncPositionY: 1
+ SyncPositionZ: 1
+ SyncRotAngleX: 0
+ SyncRotAngleY: 0
+ SyncRotAngleZ: 0
+ SyncScaleX: 1
+ SyncScaleY: 1
+ SyncScaleZ: 1
+ PositionThreshold: 0.001
+ RotAngleThreshold: 0.01
+ ScaleThreshold: 0.01
+ UseQuaternionSynchronization: 0
+ UseQuaternionCompression: 0
+ UseHalfFloatPrecision: 0
+ InLocalSpace: 0
+ SwitchTransformSpaceWhenParented: 0
+ Interpolate: 1
+ SlerpPosition: 0
+--- !u!114 &4225942884111122364
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 116861493430507844}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 05b2c04367f8c864bb5e3e03ba42dde5, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.Builder
+ ShowTopMostFoldoutHeaderGroup: 1
+ moveSpeed: 8
+ arrivalThreshold: 0.05
+ heightOffset: 2
+ terrainRaycastMaxDistance: 100
+ terrainLayerMask:
+ serializedVersion: 2
+ m_Bits: 128
+ buildRange: 6
+--- !u!114 &4533726421250799861
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 116861493430507844}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 85df68e96d71b3f4cb302a197a6a4e05, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.BuilderInputController
+ ShowTopMostFoldoutHeaderGroup: 1
+ buildablePlaneLayerMask:
+ serializedVersion: 2
+ m_Bits: 64
+ raycastMaxDistance: 500
+--- !u!114 &6467759961575585905
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 116861493430507844}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: a65b1797079cf2d4e9de7b82e81f2283, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.BuildRangeIndicator
+ projector: {fileID: 2082893476690950776}
+ projectionDepth: 50
+--- !u!1 &4357234114074764669
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 2153758330548988791}
+ - component: {fileID: 2082893476690950776}
+ m_Layer: 0
+ m_Name: RangeIndicator
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &2153758330548988791
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 4357234114074764669}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 5490805221566030526}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &2082893476690950776
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 4357234114074764669}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 0777d029ed3dffa4692f417d4aba19ca, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.DecalProjector
+ m_Material: {fileID: 2100000, guid: f99227cbde481ce47a2527e6bca709d2, type: 2}
+ m_DrawDistance: 1000
+ m_FadeScale: 0.9
+ m_StartAngleFade: 180
+ m_EndAngleFade: 180
+ m_UVScale: {x: 1, y: 1}
+ m_UVBias: {x: 0, y: 0}
+ m_RenderingLayerMask:
+ serializedVersion: 0
+ m_Bits: 1
+ m_ScaleMode: 0
+ m_Offset: {x: 0, y: 0, z: 25}
+ m_Size: {x: 12, y: 12, z: 50}
+ m_FadeFactor: 1
+ m_VisibleInScene: 1
+ version: 1
+ m_DecalLayerMask: 1
diff --git a/Assets/_Project/Prefabs/Builders/Builder_Basic.prefab.meta b/Assets/_Project/Prefabs/Builders/Builder_Basic.prefab.meta
new file mode 100644
index 0000000..623cac7
--- /dev/null
+++ b/Assets/_Project/Prefabs/Builders/Builder_Basic.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 3398cc5831880954487717577f61b6d7
+PrefabImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Prefabs/Player/Player.prefab b/Assets/_Project/Prefabs/Player/Player.prefab
index 827282d..09405e5 100644
--- a/Assets/_Project/Prefabs/Player/Player.prefab
+++ b/Assets/_Project/Prefabs/Player/Player.prefab
@@ -11,6 +11,7 @@ GameObject:
- component: {fileID: 8600750867913649879}
- component: {fileID: 2152427255203126265}
- component: {fileID: 2918837822014987993}
+ - component: {fileID: 7845089877743661692}
m_Layer: 0
m_Name: Player
m_TagString: Untagged
@@ -45,7 +46,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
- GlobalObjectIdHash: 2568017024
+ GlobalObjectIdHash: 121878297
InScenePlacedSourceGlobalObjectIdHash: 0
DeferredDespawnTick: 0
Ownership: 1
@@ -72,3 +73,17 @@ MonoBehaviour:
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.PlayerGoldManager
ShowTopMostFoldoutHeaderGroup: 1
startingGold: 100
+--- !u!114 &7845089877743661692
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 3493329038866903420}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 0f4c46f8263f72541b0f782b446de941, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.PlayerBuilderSpawner
+ ShowTopMostFoldoutHeaderGroup: 1
+ builderPrefab: {fileID: 116861493430507844, guid: 3398cc5831880954487717577f61b6d7, type: 3}
diff --git a/Assets/_Project/Prefabs/Towers/Tower_Basic.prefab b/Assets/_Project/Prefabs/Towers/Tower_Basic.prefab
new file mode 100644
index 0000000..fa5ff31
--- /dev/null
+++ b/Assets/_Project/Prefabs/Towers/Tower_Basic.prefab
@@ -0,0 +1,154 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &6482414459531823157
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1283036264165444500}
+ - component: {fileID: 6869333096494165105}
+ - component: {fileID: 4028055828417179692}
+ - component: {fileID: 5594214090440991794}
+ - component: {fileID: 7630870068340451557}
+ - component: {fileID: 9137031893466587143}
+ m_Layer: 0
+ m_Name: Tower_Basic
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &1283036264165444500
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6482414459531823157}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 50.53879, y: 0.5, z: 5.77106}
+ m_LocalScale: {x: 2, y: 1, z: 2}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!33 &6869333096494165105
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6482414459531823157}
+ m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!23 &4028055828417179692
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6482414459531823157}
+ 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: 31321ba15b8f8eb4c954353edc038b1d, 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!65 &5594214090440991794
+BoxCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6482414459531823157}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 3
+ m_Size: {x: 1, y: 1, z: 1}
+ m_Center: {x: 0, y: 0, z: 0}
+--- !u!114 &7630870068340451557
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6482414459531823157}
+ 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: 1472871091
+ 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 &9137031893466587143
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6482414459531823157}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: fb111fc88b3d6a340a3abde5a1502af3, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.TowerInstance
+ ShowTopMostFoldoutHeaderGroup: 1
diff --git a/Assets/_Project/Prefabs/Towers/Tower_Basic.prefab.meta b/Assets/_Project/Prefabs/Towers/Tower_Basic.prefab.meta
new file mode 100644
index 0000000..ada2de8
--- /dev/null
+++ b/Assets/_Project/Prefabs/Towers/Tower_Basic.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 1511641f145758b469e64376d2a0d434
+PrefabImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scenes/Levels/Main.unity b/Assets/_Project/Scenes/Levels/Main.unity
index 7043dbf..aee9618 100644
--- a/Assets/_Project/Scenes/Levels/Main.unity
+++ b/Assets/_Project/Scenes/Levels/Main.unity
@@ -146,7 +146,7 @@ Transform:
m_GameObject: {fileID: 154690529}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
- m_LocalPosition: {x: -15.58, y: 0, z: 12.98}
+ m_LocalPosition: {x: 27, y: 0, z: 52.05}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -185,8 +185,8 @@ BoxCollider:
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 3
- m_Size: {x: 35, y: 1, z: 7}
- m_Center: {x: 0, y: 0, z: 0}
+ m_Size: {x: 20, y: 1, z: 34}
+ m_Center: {x: -13.5, y: 0, z: 8.5}
--- !u!1 &167151707
GameObject:
m_ObjectHideFlags: 0
@@ -231,12 +231,124 @@ Transform:
m_GameObject: {fileID: 167151707}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
- m_LocalPosition: {x: 24.6091, y: -0, z: -0.55913}
+ m_LocalPosition: {x: 0, y: -0, z: 0}
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 &213124036
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 213124040}
+ - component: {fileID: 213124039}
+ - component: {fileID: 213124038}
+ - component: {fileID: 213124037}
+ m_Layer: 7
+ m_Name: Cube (1)
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!65 &213124037
+BoxCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 213124036}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 3
+ m_Size: {x: 1, y: 1, z: 1}
+ m_Center: {x: 0, y: 0, z: 0}
+--- !u!23 &213124038
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 213124036}
+ 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: 31321ba15b8f8eb4c954353edc038b1d, 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 &213124039
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 213124036}
+ m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!4 &213124040
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 213124036}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 9, y: 2, z: 13}
+ m_LocalScale: {x: 2, y: 1, z: 2}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &304575571
GameObject:
m_ObjectHideFlags: 0
@@ -264,7 +376,7 @@ Transform:
m_GameObject: {fileID: 304575571}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
- m_LocalPosition: {x: -30.03, y: 0, z: 13}
+ m_LocalPosition: {x: 13, y: 0, z: 81}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -284,7 +396,7 @@ MonoBehaviour:
m_EditorClassIdentifier: Assembly-CSharp::TD.Levels.SpawnerVolume
owner: 1
spawnerIdInZone: 0
- spawnFacing: 2
+ spawnFacing: 1
placementValidity: 0
--- !u!65 &304575574
BoxCollider:
@@ -394,12 +506,12 @@ Transform:
m_GameObject: {fileID: 330585543}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
- m_LocalPosition: {x: 0, y: 1, z: -10}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
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}
+ m_Father: {fileID: 1239994224}
+ m_LocalEulerAnglesHint: {x: 45, y: -90, z: 0}
--- !u!114 &330585547
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -620,7 +732,7 @@ Transform:
m_GameObject: {fileID: 441239879}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
- m_LocalPosition: {x: 32.91642, y: 0, z: -9.56}
+ m_LocalPosition: {x: 1, y: 0, z: 2}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
@@ -633,6 +745,118 @@ Transform:
- {fileID: 923592499}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &720114039
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 720114043}
+ - component: {fileID: 720114042}
+ - component: {fileID: 720114041}
+ - component: {fileID: 720114040}
+ m_Layer: 7
+ m_Name: Cube (2)
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!65 &720114040
+BoxCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 720114039}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 3
+ m_Size: {x: 1, y: 1, z: 1}
+ m_Center: {x: 0, y: 0, z: 0}
+--- !u!23 &720114041
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 720114039}
+ 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: 31321ba15b8f8eb4c954353edc038b1d, 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 &720114042
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 720114039}
+ m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!4 &720114043
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 720114039}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 14, y: 3, z: 14}
+ m_LocalScale: {x: 2, y: 1, z: 2}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &832575517
GameObject:
m_ObjectHideFlags: 0
@@ -746,8 +970,8 @@ BoxCollider:
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 3
- m_Size: {x: 70, y: 0.5, z: 39}
- m_Center: {x: 0.5, y: 0, z: 19}
+ m_Size: {x: 32, y: 0.5, z: 87}
+ m_Center: {x: 14.5, y: 0, z: 41}
--- !u!1 &1064792475
GameObject:
m_ObjectHideFlags: 0
@@ -775,7 +999,7 @@ Transform:
m_GameObject: {fileID: 1064792475}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
- m_LocalPosition: {x: 3, y: 0, z: 13}
+ m_LocalPosition: {x: 11, y: 0, z: 40.5}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -816,8 +1040,8 @@ BoxCollider:
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 3
- m_Size: {x: 2, y: 1, z: 7}
- m_Center: {x: 0, y: 0, z: 0}
+ m_Size: {x: 6, y: 1, z: 1}
+ m_Center: {x: 2, y: 0, z: 2}
--- !u!1 &1078485323
GameObject:
m_ObjectHideFlags: 0
@@ -845,7 +1069,7 @@ Transform:
m_GameObject: {fileID: 1078485323}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
- m_LocalPosition: {x: 7.06, y: 0, z: 21.08}
+ m_LocalPosition: {x: 27, y: 0, z: 36}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -865,7 +1089,7 @@ MonoBehaviour:
m_EditorClassIdentifier: Assembly-CSharp::TD.Levels.SpawnerVolume
owner: 2
spawnerIdInZone: 0
- spawnFacing: 1
+ spawnFacing: 3
placementValidity: 0
--- !u!65 &1078485326
BoxCollider:
@@ -886,8 +1110,66 @@ BoxCollider:
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 3
- m_Size: {x: 7, y: 1, z: 10}
- m_Center: {x: 0, y: 0, z: 0}
+ m_Size: {x: 7, y: 1, z: 6}
+ m_Center: {x: 0, y: 0, z: 2}
+--- !u!1 &1239994222
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1239994224}
+ - component: {fileID: 1239994223}
+ m_Layer: 0
+ m_Name: CameraRig
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &1239994223
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1239994222}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 1e670a36689801e428873c5712ea3679, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.CameraController
+ cameraChild: {fileID: 330585545}
+ basePanSpeed: 12
+ edgePanMarginPixels: 16
+ edgePanEnabled: 1
+ minDollyDistance: 5
+ maxDollyDistance: 50
+ startDollyDistance: 20
+ zoomSpeed: 3
+ cursorAnchoredZoom: 1
+ minPitchDegrees: 30
+ maxPitchDegrees: 75
+ startPitchDegrees: 60
+ pitchSpeed: 4
+--- !u!4 &1239994224
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1239994222}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 330585546}
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1360337262
GameObject:
m_ObjectHideFlags: 0
@@ -915,7 +1197,7 @@ Transform:
m_GameObject: {fileID: 1360337262}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
- m_LocalPosition: {x: 33.16, y: 0, z: 12.58}
+ m_LocalPosition: {x: 13, y: 0, z: 3.5}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -953,8 +1235,8 @@ BoxCollider:
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 3
- m_Size: {x: 3, y: 1, z: 7}
- m_Center: {x: 0, y: 0, z: 0}
+ m_Size: {x: 7, y: 1, z: 2}
+ m_Center: {x: 0, y: 0, z: 2.5}
--- !u!1 &1464027360
GameObject:
m_ObjectHideFlags: 0
@@ -967,7 +1249,7 @@ GameObject:
- component: {fileID: 1464027363}
- component: {fileID: 1464027362}
- component: {fileID: 1464027361}
- m_Layer: 0
+ m_Layer: 7
m_Name: Plane
m_TagString: Untagged
m_Icon: {fileID: 0}
@@ -1061,13 +1343,197 @@ Transform:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1464027360}
serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0.7071068, z: 0, w: 0.7071068}
+ m_LocalPosition: {x: 14, y: 0, z: 39}
+ m_LocalScale: {x: 10, y: 1, z: 5}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 90, z: 0}
+--- !u!1 &1507514106
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1507514109}
+ - component: {fileID: 1507514107}
+ - component: {fileID: 1507514108}
+ m_Layer: 0
+ m_Name: TowerPlacementManager
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &1507514107
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1507514106}
+ 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: 1708589157
+ 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 &1507514108
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1507514106}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: d369496ba7887b844b1c220b524a507d, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.TowerPlacementManager
+ ShowTopMostFoldoutHeaderGroup: 1
+ requestsPerFrame: 3
+ towerDefinitions:
+ - {fileID: 0}
+ - {fileID: 11400000, guid: 0f693e29ca953e1439e10cb8f12e4b30, type: 2}
+--- !u!4 &1507514109
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1507514106}
+ serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
- m_LocalPosition: {x: 33.17, y: 0, z: 13.07}
- m_LocalScale: {x: 7, y: 1, z: 3}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ 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 &1538763653
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1538763654}
+ - component: {fileID: 1538763655}
+ m_Layer: 0
+ m_Name: TowerRegistry
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &1538763654
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1538763653}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ 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 &1538763655
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1538763653}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: a9dc0fbbe4422bc479ab8db7658c082b, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.TowerRegistry
+--- !u!1 &1597884408
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1597884409}
+ - component: {fileID: 1597884411}
+ - component: {fileID: 1597884410}
+ m_Layer: 0
+ m_Name: TowerPlacementController
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &1597884409
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1597884408}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ 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 &1597884410
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1597884408}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: ea0e3a4681be19e4e9c359c1123bf68d, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.TowerPlacementTestTrigger
+ towerToPlace: {fileID: 11400000, guid: 0f693e29ca953e1439e10cb8f12e4b30, type: 2}
+ towerTypeId: 1
+ controller: {fileID: 1597884411}
+--- !u!114 &1597884411
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1597884408}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 124253f2cbc2d9f46befb6e1763cd6b9, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.TowerPlacementController
+ settings: {fileID: 11400000, guid: 18f99b4b94d13eb429d4dfb9b0b37b4b, type: 2}
+ buildablePlaneLayerMask:
+ serializedVersion: 2
+ m_Bits: 64
+ raycastMaxDistance: 500
--- !u!1 &1682341399
GameObject:
m_ObjectHideFlags: 0
@@ -1174,6 +1640,118 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &1789340187
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1789340191}
+ - component: {fileID: 1789340190}
+ - component: {fileID: 1789340189}
+ - component: {fileID: 1789340188}
+ m_Layer: 7
+ m_Name: Cube
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!65 &1789340188
+BoxCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1789340187}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 3
+ m_Size: {x: 1, y: 1, z: 1}
+ m_Center: {x: 0, y: 0, z: 0}
+--- !u!23 &1789340189
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1789340187}
+ 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: 31321ba15b8f8eb4c954353edc038b1d, 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 &1789340190
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1789340187}
+ m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!4 &1789340191
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1789340187}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 9, y: 0.5, z: 9}
+ m_LocalScale: {x: 2, y: 1, z: 2}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1975687919
GameObject:
m_ObjectHideFlags: 0
@@ -1201,7 +1779,7 @@ Transform:
m_GameObject: {fileID: 1975687919}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
- m_LocalPosition: {x: 18, y: 0, z: 12.56}
+ m_LocalPosition: {x: 24, y: 0, z: 37.75}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -1240,16 +1818,22 @@ BoxCollider:
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 3
- m_Size: {x: 28, y: 1, z: 7}
- m_Center: {x: 0, y: 0, z: 0}
+ m_Size: {x: 19, y: 1, z: 34}
+ m_Center: {x: -10.5, y: 0, z: -13.5}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- - {fileID: 330585546}
- {fileID: 410087041}
- {fileID: 832575519}
- {fileID: 1682341402}
- {fileID: 441239881}
- {fileID: 1464027364}
- {fileID: 167151709}
+ - {fileID: 1507514109}
+ - {fileID: 1538763654}
+ - {fileID: 1597884409}
+ - {fileID: 1239994224}
+ - {fileID: 1789340191}
+ - {fileID: 213124040}
+ - {fileID: 720114043}
diff --git a/Assets/_Project/Scenes/Levels/TestLevel.asset b/Assets/_Project/Scenes/Levels/TestLevel.asset
index 57f092c..25478f1 100644
--- a/Assets/_Project/Scenes/Levels/TestLevel.asset
+++ b/Assets/_Project/Scenes/Levels/TestLevel.asset
@@ -18,187 +18,166 @@ MonoBehaviour:
Author: Matt
MapThumbnail: {fileID: 21300000, guid: d2e652d3e1c53454d80d3c1ec7888998, type: 3}
ScenePath: Assets/_Project/Scenes/Levels/Main.unity
- AuthoringHash: de422400f5f1f75440a3d909f65f34569621293a1499b988881845ac0dc248a4
- LastBakeTimestamp: 2026-05-01T22:11:16.4327588Z
+ AuthoringHash: 18f981c8a12a79f122c2dad6fb2dab16c7921e01c9cd7bb6aed99d09d60ad2ac
+ LastBakeTimestamp: 2026-05-03T21:39:37.7056732Z
LastBakeOutcome: 1
LastBakeWarningCount: 2
- GridOriginTile: {x: -1, y: -10}
- GridSize: {x: 70, y: 39}
- PlacementGrid: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002020202020202010101010101010101010101010101010101010101010101010101010202010101010101010101010101010101010101010101010101010101010202020000020202020202020101010101010101010101010101010101010101010101010101010102020101010101010101010101010101010101010101010101010101010102020200000202020202020201010101010101010101010101010101010101010101010101010101020201010101010101010101010101010101010101010101010101010101020202000002020202020202010101010101010101010101010101010101010101010101010101010202010101010101010101010101010101010101010101010101010101010202020000020202020202020101010101010101010101010101010101010101010101010101010102020101010101010101010101010101010101010101010101010101010102020200000202020202020201010101010101010101010101010101010101010101010101010101020201010101010101010101010101010101010101010101010101010101020202000002020202020202010101010101010101010101010101010101010101010101010101010202010101010101010101010101010101010101010101010101010101010202020000000000000000000000000000000000000000000000000000000000000000000000000000000202020202020200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002020202020202000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000202020202020200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002020202020202000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000202020202020200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002020202020202000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000202020202020200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
- WalkabilityGrid: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010000000000000000000000000000000000000000000000000000000000000000000000000000000101010101010100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001010101010101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101010101010100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001010101010101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101010101010100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001010101010101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101010101010100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
- OwnerGrid: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001010101010101010101010101010101010101010101010101010101010101010101010000020202020202020202020202020202020202020202020202020202020000000000010101010101010101010101010101010101010101010101010101010101010101010100000202020202020202020202020202020202020202020202020202020200000000000101010101010101010101010101010101010101010101010101010101010101010101000002020202020202020202020202020202020202020202020202020202000000000001010101010101010101010101010101010101010101010101010101010101010101010000020202020202020202020202020202020202020202020202020202020000000000010101010101010101010101010101010101010101010101010101010101010101010100000202020202020202020202020202020202020202020202020202020200000000000101010101010101010101010101010101010101010101010101010101010101010101000002020202020202020202020202020202020202020202020202020202000000000001010101010101010101010101010101010101010101010101010101010101010101010000020202020202020202020202020202020202020202020202020202020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
- MapAreaGrid: 010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101
+ GridOriginTile: {x: 0, y: 0}
+ GridSize: {x: 32, y: 87}
+ PlacementGrid: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000202020202020200000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000002020202020202000000000000000000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101020202020202020000000000010101010101010101010101010101010101010102020202020202000000000001010101010101010101010101010101010101010202020202020200000000000101010101010101010101010101010101010101020202020202020000000000010101010101010101010101010101010101010102020202020202000000000001010101010101010101010101010101010101010202020202020200000000000101010101010101010101010101010101010101020202020202020000000000000000000000020202020202020000000000000000000000000000000000000000000000000002020202020202000000000000000000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000002020202020202000000000000000000000000000000000000000000000000000202020202020200000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000002020202020202000000000000000000000000000000000000000000000000000202020202020200000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000
+ WalkabilityGrid: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101010101010100000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000001010101010101000000000000000000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101010101010101010000000000010101010101010101010101010101010101010101010101010101000000000001010101010101010101010101010101010101010101010101010100000000000101010101010101010101010101010101010101010101010101010000000000010101010101010101010101010101010101010101010101010101000000000001010101010101010101010101010101010101010101010101010100000000000101010101010101010101010101010101010101010101010101010000000000000000000000010101010101010000000000000000000000000000000000000000000000000001010101010101000000000000000000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000001010101010101000000000000000000000000000000000000000000000000000101010101010100000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000001010101010101000000000000000000000000000000000000000000000000000101010101010100000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000
+ OwnerGrid: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000202020202020202020202020202020202020202000000000000000000000000020202020202020202020202020202020202020200000000000000000000000002020202020202020202020202020202020202020000000000000000000000000202020202020202020202020202020202020202000000000000000000000000020202020202020202020202020202020202020200000000000000000000000002020202020202020202020202020202020202020000000000000000000000000202020202020202020202020202020202020202000000000000000000000000020202020202020202020202020202020202020200000000000000000000000002020202020202020202020202020202020202020000000000000000000000000202020202020202020202020202020202020202000000000000000000000000020202020202020202020202020202020202020200000000000000000000000002020202020202020202020202020202020202020000000000000000000000000202020202020202020202020202020202020202000000000000000000000000020202020202020202020202020202020202020200000000000000000000000002020202020202020202020202020202020202020000000000000000000000000202020202020202020202020202020202020202000000000000000000000000020202020202020202020202020202020202020200000000000000000000000002020202020202020202020202020202020202020000000000000000000000000202020202020202020202020202020202020202000000000000000000000000020202020202020202020202020202020202020200000000000000000000000002020202020202020202020202020202020202020000000000000000000000000202020202020202020202020202020202020202000000000000000000000000020202020202020202020202020202020202020200000000000000000000000002020202020202020202020202020202020202020000000000000000000000000202020202020202020202020202020202020202000000000000000000000000020202020202020202020202020202020202020200000000000000000000000002020202020202020202020202020202020202020000000000000000000000000202020202020202020202020202020202020202000000000000000000000000020202020202020202020202020202020202020200000000000000000000000002020202020202020202020202020202020202020000000000000000000000000202020202020202020202020202020202020202000000000000000000000000020202020202020202020202020202020202020200000000000000000000000002020202020202020202020202020202020202020000000000000000000000000202020202020202020202020202020202020202000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010100000000000000000000000001010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+ MapAreaGrid: 010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101
PlayerZones:
- Owner: 1
Spawners:
- SpawnerIdInZone: 0
- TilePosition: {x: 3, y: 3}
+ TilePosition: {x: 14, y: 83}
TileArea:
- - {x: 0, y: 0}
- - {x: 1, y: 0}
- - {x: 2, y: 0}
- - {x: 3, y: 0}
- - {x: 4, y: 0}
- - {x: 5, y: 0}
- - {x: 6, y: 0}
- - {x: 0, y: 1}
- - {x: 1, y: 1}
- - {x: 2, y: 1}
- - {x: 3, y: 1}
- - {x: 4, y: 1}
- - {x: 5, y: 1}
- - {x: 6, y: 1}
- - {x: 0, y: 2}
- - {x: 1, y: 2}
- - {x: 2, y: 2}
- - {x: 3, y: 2}
- - {x: 4, y: 2}
- - {x: 5, y: 2}
- - {x: 6, y: 2}
- - {x: 0, y: 3}
- - {x: 1, y: 3}
- - {x: 2, y: 3}
- - {x: 3, y: 3}
- - {x: 4, y: 3}
- - {x: 5, y: 3}
- - {x: 6, y: 3}
- - {x: 0, y: 4}
- - {x: 1, y: 4}
- - {x: 2, y: 4}
- - {x: 3, y: 4}
- - {x: 4, y: 4}
- - {x: 5, y: 4}
- - {x: 6, y: 4}
- - {x: 0, y: 5}
- - {x: 1, y: 5}
- - {x: 2, y: 5}
- - {x: 3, y: 5}
- - {x: 4, y: 5}
- - {x: 5, y: 5}
- - {x: 6, y: 5}
- - {x: 0, y: 6}
- - {x: 1, y: 6}
- - {x: 2, y: 6}
- - {x: 3, y: 6}
- - {x: 4, y: 6}
- - {x: 5, y: 6}
- - {x: 6, y: 6}
- Facing: 2
+ - {x: 11, y: 80}
+ - {x: 12, y: 80}
+ - {x: 13, y: 80}
+ - {x: 14, y: 80}
+ - {x: 15, y: 80}
+ - {x: 16, y: 80}
+ - {x: 17, y: 80}
+ - {x: 11, y: 81}
+ - {x: 12, y: 81}
+ - {x: 13, y: 81}
+ - {x: 14, y: 81}
+ - {x: 15, y: 81}
+ - {x: 16, y: 81}
+ - {x: 17, y: 81}
+ - {x: 11, y: 82}
+ - {x: 12, y: 82}
+ - {x: 13, y: 82}
+ - {x: 14, y: 82}
+ - {x: 15, y: 82}
+ - {x: 16, y: 82}
+ - {x: 17, y: 82}
+ - {x: 11, y: 83}
+ - {x: 12, y: 83}
+ - {x: 13, y: 83}
+ - {x: 14, y: 83}
+ - {x: 15, y: 83}
+ - {x: 16, y: 83}
+ - {x: 17, y: 83}
+ - {x: 11, y: 84}
+ - {x: 12, y: 84}
+ - {x: 13, y: 84}
+ - {x: 14, y: 84}
+ - {x: 15, y: 84}
+ - {x: 16, y: 84}
+ - {x: 17, y: 84}
+ - {x: 11, y: 85}
+ - {x: 12, y: 85}
+ - {x: 13, y: 85}
+ - {x: 14, y: 85}
+ - {x: 15, y: 85}
+ - {x: 16, y: 85}
+ - {x: 17, y: 85}
+ - {x: 11, y: 86}
+ - {x: 12, y: 86}
+ - {x: 13, y: 86}
+ - {x: 14, y: 86}
+ - {x: 15, y: 86}
+ - {x: 16, y: 86}
+ - {x: 17, y: 86}
+ Facing: 1
LeakExits:
- Target: 2
TileArea:
- - {x: 35, y: 0}
- - {x: 36, y: 0}
- - {x: 35, y: 1}
- - {x: 36, y: 1}
- - {x: 35, y: 2}
- - {x: 36, y: 2}
- - {x: 35, y: 3}
- - {x: 36, y: 3}
- - {x: 35, y: 4}
- - {x: 36, y: 4}
- - {x: 35, y: 5}
- - {x: 36, y: 5}
- - {x: 35, y: 6}
- - {x: 36, y: 6}
+ - {x: 11, y: 44}
+ - {x: 12, y: 44}
+ - {x: 13, y: 44}
+ - {x: 14, y: 44}
+ - {x: 15, y: 44}
+ - {x: 16, y: 44}
+ - {x: 17, y: 44}
+ - {x: 11, y: 45}
+ - {x: 12, y: 45}
+ - {x: 13, y: 45}
+ - {x: 14, y: 45}
+ - {x: 15, y: 45}
+ - {x: 16, y: 45}
+ - {x: 17, y: 45}
NormalizedWeight: 1
- Owner: 2
Spawners:
- SpawnerIdInZone: 0
- TilePosition: {x: 40, y: 12}
+ TilePosition: {x: 28, y: 40}
TileArea:
- - {x: 37, y: 7}
- - {x: 38, y: 7}
- - {x: 39, y: 7}
- - {x: 40, y: 7}
- - {x: 41, y: 7}
- - {x: 42, y: 7}
- - {x: 43, y: 7}
- - {x: 37, y: 8}
- - {x: 38, y: 8}
- - {x: 39, y: 8}
- - {x: 40, y: 8}
- - {x: 41, y: 8}
- - {x: 42, y: 8}
- - {x: 43, y: 8}
- - {x: 37, y: 9}
- - {x: 38, y: 9}
- - {x: 39, y: 9}
- - {x: 40, y: 9}
- - {x: 41, y: 9}
- - {x: 42, y: 9}
- - {x: 43, y: 9}
- - {x: 37, y: 10}
- - {x: 38, y: 10}
- - {x: 39, y: 10}
- - {x: 40, y: 10}
- - {x: 41, y: 10}
- - {x: 42, y: 10}
- - {x: 43, y: 10}
- - {x: 37, y: 11}
- - {x: 38, y: 11}
- - {x: 39, y: 11}
- - {x: 40, y: 11}
- - {x: 41, y: 11}
- - {x: 42, y: 11}
- - {x: 43, y: 11}
- - {x: 37, y: 12}
- - {x: 38, y: 12}
- - {x: 39, y: 12}
- - {x: 40, y: 12}
- - {x: 41, y: 12}
- - {x: 42, y: 12}
- - {x: 43, y: 12}
- - {x: 37, y: 13}
- - {x: 38, y: 13}
- - {x: 39, y: 13}
- - {x: 40, y: 13}
- - {x: 41, y: 13}
- - {x: 42, y: 13}
- - {x: 43, y: 13}
- - {x: 37, y: 14}
- - {x: 38, y: 14}
- - {x: 39, y: 14}
- - {x: 40, y: 14}
- - {x: 41, y: 14}
- - {x: 42, y: 14}
- - {x: 43, y: 14}
- - {x: 37, y: 15}
- - {x: 38, y: 15}
- - {x: 39, y: 15}
- - {x: 40, y: 15}
- - {x: 41, y: 15}
- - {x: 42, y: 15}
- - {x: 43, y: 15}
- - {x: 37, y: 16}
- - {x: 38, y: 16}
- - {x: 39, y: 16}
- - {x: 40, y: 16}
- - {x: 41, y: 16}
- - {x: 42, y: 16}
- - {x: 43, y: 16}
- Facing: 1
+ - {x: 25, y: 37}
+ - {x: 26, y: 37}
+ - {x: 27, y: 37}
+ - {x: 28, y: 37}
+ - {x: 29, y: 37}
+ - {x: 30, y: 37}
+ - {x: 31, y: 37}
+ - {x: 25, y: 38}
+ - {x: 26, y: 38}
+ - {x: 27, y: 38}
+ - {x: 28, y: 38}
+ - {x: 29, y: 38}
+ - {x: 30, y: 38}
+ - {x: 31, y: 38}
+ - {x: 25, y: 39}
+ - {x: 26, y: 39}
+ - {x: 27, y: 39}
+ - {x: 28, y: 39}
+ - {x: 29, y: 39}
+ - {x: 30, y: 39}
+ - {x: 31, y: 39}
+ - {x: 25, y: 40}
+ - {x: 26, y: 40}
+ - {x: 27, y: 40}
+ - {x: 28, y: 40}
+ - {x: 29, y: 40}
+ - {x: 30, y: 40}
+ - {x: 31, y: 40}
+ - {x: 25, y: 41}
+ - {x: 26, y: 41}
+ - {x: 27, y: 41}
+ - {x: 28, y: 41}
+ - {x: 29, y: 41}
+ - {x: 30, y: 41}
+ - {x: 31, y: 41}
+ - {x: 25, y: 42}
+ - {x: 26, y: 42}
+ - {x: 27, y: 42}
+ - {x: 28, y: 42}
+ - {x: 29, y: 42}
+ - {x: 30, y: 42}
+ - {x: 31, y: 42}
+ - {x: 25, y: 43}
+ - {x: 26, y: 43}
+ - {x: 27, y: 43}
+ - {x: 28, y: 43}
+ - {x: 29, y: 43}
+ - {x: 30, y: 43}
+ - {x: 31, y: 43}
+ Facing: 3
LeakExits: []
Goals:
- TileArea:
- - {x: 65, y: 0}
- - {x: 66, y: 0}
- - {x: 67, y: 0}
- - {x: 65, y: 1}
- - {x: 66, y: 1}
- - {x: 67, y: 1}
- - {x: 65, y: 2}
- - {x: 66, y: 2}
- - {x: 67, y: 2}
- - {x: 65, y: 3}
- - {x: 66, y: 3}
- - {x: 67, y: 3}
- - {x: 65, y: 4}
- - {x: 66, y: 4}
- - {x: 67, y: 4}
- - {x: 65, y: 5}
- - {x: 66, y: 5}
- - {x: 67, y: 5}
- - {x: 65, y: 6}
- - {x: 66, y: 6}
- - {x: 67, y: 6}
+ - {x: 11, y: 7}
+ - {x: 12, y: 7}
+ - {x: 13, y: 7}
+ - {x: 14, y: 7}
+ - {x: 15, y: 7}
+ - {x: 16, y: 7}
+ - {x: 17, y: 7}
+ - {x: 11, y: 8}
+ - {x: 12, y: 8}
+ - {x: 13, y: 8}
+ - {x: 14, y: 8}
+ - {x: 15, y: 8}
+ - {x: 16, y: 8}
+ - {x: 17, y: 8}
+ - {x: 11, y: 9}
+ - {x: 12, y: 9}
+ - {x: 13, y: 9}
+ - {x: 14, y: 9}
+ - {x: 15, y: 9}
+ - {x: 16, y: 9}
+ - {x: 17, y: 9}
diff --git a/Assets/_Project/Scenes/Levels/TestLevel_Thumbnail.png b/Assets/_Project/Scenes/Levels/TestLevel_Thumbnail.png
index 8f50996..d282ffc 100644
--- a/Assets/_Project/Scenes/Levels/TestLevel_Thumbnail.png
+++ b/Assets/_Project/Scenes/Levels/TestLevel_Thumbnail.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:62cfc398409e4285b50681d79ae4767183db364b3f9feb9072ddf9dd60ec07d9
-size 12253
+oid sha256:3c98285c3a823f2942e9dff02edf75918e7336b4f1496570f2e32a2ffc80e55e
+size 8745
diff --git a/Assets/_Project/Scripts/Gameplay/BuildRangeIndicator.cs b/Assets/_Project/Scripts/Gameplay/BuildRangeIndicator.cs
new file mode 100644
index 0000000..bff8b19
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/BuildRangeIndicator.cs
@@ -0,0 +1,121 @@
+// Assets/_Project/Scripts/Gameplay/BuildRangeIndicator.cs
+using UnityEngine;
+using UnityEngine.Rendering.Universal;
+
+namespace TD.Gameplay
+{
+ ///
+ /// Visualizes a builder's build range as a decal projector circle on the ground.
+ /// Visible only to the owning client, and only while placement mode is active.
+ ///
+ ///
+ /// Sits as a child of the GameObject. The
+ /// renders a circular texture onto whatever ground
+ /// geometry is below — flat plane or sloped terrain alike, no special handling needed.
+ ///
+ /// Owner-only. Non-owning clients should not see other players' build
+ /// range indicators (would be visual clutter). The decal projector is force-disabled
+ /// for non-owners on spawn.
+ ///
+ /// Toggling. The indicator is only visible when the local player is in
+ /// placement mode. It checks TowerPlacementController.IsPlacing each frame
+ /// and toggles the projector accordingly. When sized correctly the projector size
+ /// matches buildRange * 2 (diameter).
+ ///
+ public class BuildRangeIndicator : MonoBehaviour
+ {
+ [Tooltip("DecalProjector child component to drive. Auto-found in Awake if empty.")]
+ [SerializeField] private DecalProjector projector;
+
+ [Tooltip("Vertical thickness of the decal projector's projection volume. Should " +
+ "exceed your map's vertical range so the decal projects onto terrain at any height.")]
+ [SerializeField] private float projectionDepth = 50f;
+
+ // Cached references resolved lazily.
+ private Builder cachedBuilder;
+ private TowerPlacementController cachedPlacementController;
+
+ // ----- Lifecycle --------------------------------------------------
+
+ private void Awake()
+ {
+ if (projector == null) projector = GetComponentInChildren();
+ if (projector == null)
+ {
+ Debug.LogError("[BuildRangeIndicator] No DecalProjector found. Add one as a " +
+ "child of this GameObject.");
+ enabled = false;
+ return;
+ }
+
+ // Start hidden; visibility is updated each frame.
+ projector.enabled = false;
+ }
+
+ private void Start()
+ {
+ cachedBuilder = GetComponentInParent();
+ if (cachedBuilder == null)
+ {
+ Debug.LogError("[BuildRangeIndicator] No Builder found in parents. " +
+ "Disabling indicator.");
+ enabled = false;
+ return;
+ }
+
+ // Hide for non-owners — other players don't see your range indicator.
+ if (!cachedBuilder.IsOwner)
+ {
+ enabled = false;
+ if (projector != null) projector.enabled = false;
+ return;
+ }
+
+ // Size the projector to match the builder's range (diameter = 2 * range).
+ // Modern URP DecalProjector exposes width/height/pivot as separate properties
+ // rather than a single Vector3 size. Width and Height are the ground-plane extents
+ // of the projection; pivot.z is the volume's depth offset (0 = volume centered
+ // on the projector position, projecting equally above and below).
+ float diameter = cachedBuilder.BuildRange * 2f;
+
+ // The DecalProjector exposes a `size` Vector3 on its API even though the
+ // inspector splits it into Width/Height/ProjectionDepth — assignment is still
+ // valid in current URP. We use it here to set all three at once.
+ projector.size = new Vector3(diameter, diameter, projectionDepth);
+ projector.pivot = new Vector3(0f, 0f, 0f);
+
+ // Center the projection volume on the builder. The decal projector projects
+ // along its local +Z axis by default; rotate to project downward (look down).
+ //
+ // CRITICAL: rotate the PROJECTOR's transform, not this component's transform.
+ // BuildRangeIndicator lives on the Builder root (so it can find Builder via
+ // GetComponentInParent), but `transform` here is the Builder's transform —
+ // rotating it tips the cylinder onto its side. The projector lives on a child
+ // GameObject; that's the one that needs the 90° X rotation.
+ projector.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
+ projector.transform.localPosition = Vector3.zero;
+ }
+
+ private void Update()
+ {
+ if (cachedBuilder == null) return;
+
+ bool shouldShow = IsLocalPlayerPlacing();
+ if (projector.enabled != shouldShow)
+ projector.enabled = shouldShow;
+ }
+
+ // ----- Helpers ----------------------------------------------------
+
+ private bool IsLocalPlayerPlacing()
+ {
+ if (cachedPlacementController == null)
+ {
+ cachedPlacementController =
+ UnityEngine.Object.FindAnyObjectByType();
+ if (cachedPlacementController == null) return false;
+ }
+ return cachedPlacementController.IsPlacing;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/BuildRangeIndicator.cs.meta b/Assets/_Project/Scripts/Gameplay/BuildRangeIndicator.cs.meta
new file mode 100644
index 0000000..a532ecf
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/BuildRangeIndicator.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: a65b1797079cf2d4e9de7b82e81f2283
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/Builder.cs b/Assets/_Project/Scripts/Gameplay/Builder.cs
new file mode 100644
index 0000000..805bd81
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/Builder.cs
@@ -0,0 +1,303 @@
+// Assets/_Project/Scripts/Gameplay/Builder.cs
+using System.Collections.Generic;
+using Unity.Netcode;
+using UnityEngine;
+using TD.Core;
+using TD.Levels;
+
+namespace TD.Gameplay
+{
+ ///
+ /// Per-player avatar that gates tower placement by proximity. Server-authoritative
+ /// position; clients submit move requests via Rpc and the server validates and applies.
+ ///
+ ///
+ /// Pure visual avatar. Builders have no collider for gameplay purposes —
+ /// they don't block enemies, can't be attacked, and aren't selected as targets. They
+ /// are visible to all players but only their owner can move them.
+ ///
+ /// Terrain-aware height. Each frame the server casts a ray straight down
+ /// from the builder against the and sets Y to
+ /// hit.point.y + heightOffset. If the ray misses, falls back to the buildable
+ /// plane Y. Towers are not on the terrain layer, so they don't influence height.
+ ///
+ /// Range gating. is the public query
+ /// that TowerPlacementManager uses to validate placement requests. Builder range
+ /// is measured center-of-builder to center-of-anchor-tile in world units.
+ ///
+ /// Static registry. Like , builders register
+ /// themselves in a static dictionary keyed by OwnerClientId on spawn, so server
+ /// gameplay code (notably TowerPlacementManager) can find a player's builder without
+ /// scene traversal.
+ ///
+ [RequireComponent(typeof(NetworkObject))]
+ public class Builder : NetworkBehaviour
+ {
+ // ----- Static registry --------------------------------------------
+
+ private static readonly Dictionary s_byClientId
+ = new Dictionary();
+
+ ///
+ /// Returns the Builder owned by the given client, or null if none is currently spawned.
+ /// Safe to call on server or client.
+ ///
+ public static Builder GetForClient(ulong clientId)
+ {
+ s_byClientId.TryGetValue(clientId, out var builder);
+ return builder;
+ }
+
+ ///
+ /// Convenience: the local client's own builder. Returns null on a dedicated server
+ /// or before the local player has spawned.
+ ///
+ public static Builder Local
+ {
+ get
+ {
+ var nm = NetworkManager.Singleton;
+ if (nm == null || !nm.IsClient) return null;
+ return GetForClient(nm.LocalClientId);
+ }
+ }
+
+ // ----- Inspector --------------------------------------------------
+
+ [Header("Movement")]
+ [Tooltip("Speed at which the builder moves toward its target position, in world " +
+ "units per second.")]
+ [SerializeField] private float moveSpeed = 8f;
+
+ [Tooltip("Distance below which the builder is considered to have arrived at its " +
+ "target. Smaller = more precise but more jitter; larger = less precise " +
+ "but smoother.")]
+ [SerializeField] private float arrivalThreshold = 0.05f;
+
+ [Header("Height tracking")]
+ [Tooltip("Vertical offset above the terrain at which the builder hovers. " +
+ "Re-evaluated every server tick by raycasting straight down.")]
+ [SerializeField] private float heightOffset = 2f;
+
+ [Tooltip("Maximum distance to cast downward when sampling terrain height. Should " +
+ "exceed your map's vertical range.")]
+ [SerializeField] private float terrainRaycastMaxDistance = 100f;
+
+ [Tooltip("Physics layer mask used for terrain height sampling. Towers MUST NOT be " +
+ "on this layer — only ground geometry. Falls back to the buildable plane Y " +
+ "if no terrain hit.")]
+ [SerializeField] private LayerMask terrainLayerMask;
+
+ [Header("Build range")]
+ [Tooltip("Maximum distance from the builder's center to a tower's anchor tile center " +
+ "for placement to be allowed, measured in world units (== tiles).")]
+ [SerializeField] private float buildRange = 6f;
+
+ // ----- Networked state --------------------------------------------
+
+ // Server-authoritative target position. The server moves the builder toward this
+ // each frame; clients render the builder smoothly via NetworkTransform's interpolation.
+ // We replicate the target (not the live position) so the server's intent is visible
+ // to clients, but the rendered position is whatever NetworkTransform interpolates to.
+ private readonly NetworkVariable targetPosition = new NetworkVariable(
+ value: Vector3.zero,
+ readPerm: NetworkVariableReadPermission.Everyone,
+ writePerm: NetworkVariableWritePermission.Server);
+
+ // ----- Public accessors -------------------------------------------
+
+ /// The builder's current world position (its actual transform position,
+ /// not the target).
+ public Vector3 CurrentPosition => transform.position;
+
+ /// The builder's target position. Server moves toward this each frame.
+ public Vector3 TargetPosition => targetPosition.Value;
+
+ /// True if the builder has arrived at its target (within
+ /// ).
+ public bool IsAtTarget =>
+ Vector3.SqrMagnitude(transform.position - targetPosition.Value)
+ < arrivalThreshold * arrivalThreshold;
+
+ /// Build range in world units.
+ public float BuildRange => buildRange;
+
+ // ----- Lifecycle --------------------------------------------------
+
+ public override void OnNetworkSpawn()
+ {
+ s_byClientId[OwnerClientId] = this;
+
+ if (IsServer)
+ {
+ // Set initial target = current position so the builder doesn't drift on spawn.
+ // The spawner is responsible for placing this builder at a sensible position
+ // BEFORE Spawn() — see PlayerSpawnHelper / Player.OnNetworkSpawn.
+ targetPosition.Value = transform.position;
+ Debug.Log($"[Builder] Spawned for client {OwnerClientId} at " +
+ $"{transform.position}.");
+ }
+
+ ApplyOwnerColor();
+ }
+
+ // ----- Owner color tinting ----------------------------------------
+
+ // Lazily allocated; reused across renderers. Construction in a field initializer
+ // would throw on this MonoBehaviour at scene load.
+ private MaterialPropertyBlock colorPropertyBlock;
+ // Both _Color (legacy Standard) and _BaseColor (URP Lit) — writing both lets the
+ // tint apply regardless of which shader the prefab uses. Unknown property writes
+ // are silently ignored by the shader.
+ private static readonly int ColorPropertyId = Shader.PropertyToID("_Color");
+ private static readonly int BaseColorPropertyId = Shader.PropertyToID("_BaseColor");
+
+ private void ApplyOwnerColor()
+ {
+ // Owner color comes from the slot mapping. Same stub mapping as elsewhere —
+ // replaced when MatchState lands.
+ byte slotByte = (byte)(OwnerClientId + 1);
+ PlayerSlot slot = (slotByte >= 1 && slotByte <= 9) ? (PlayerSlot)slotByte : PlayerSlot.None;
+
+ Color c = PlayerColors.Get(slot);
+ c.a = 1f;
+
+ colorPropertyBlock ??= new MaterialPropertyBlock();
+ colorPropertyBlock.SetColor(ColorPropertyId, c);
+ colorPropertyBlock.SetColor(BaseColorPropertyId, c);
+
+ foreach (var rend in GetComponentsInChildren())
+ rend.SetPropertyBlock(colorPropertyBlock);
+ }
+
+ public override void OnNetworkDespawn()
+ {
+ if (s_byClientId.TryGetValue(OwnerClientId, out var registered) && registered == this)
+ s_byClientId.Remove(OwnerClientId);
+ }
+
+ // ----- Per-frame movement (server only) ---------------------------
+
+ private void Update()
+ {
+ if (!IsServer) return;
+
+ // Move toward target on the XZ plane.
+ Vector3 current = transform.position;
+ Vector3 target = targetPosition.Value;
+
+ // Flatten to XZ for distance/movement calculations; Y is driven by terrain raycast.
+ Vector3 currentXZ = new Vector3(current.x, 0f, current.z);
+ Vector3 targetXZ = new Vector3(target.x, 0f, target.z);
+
+ Vector3 newXZ;
+ if (Vector3.SqrMagnitude(currentXZ - targetXZ) <= arrivalThreshold * arrivalThreshold)
+ {
+ newXZ = targetXZ;
+ }
+ else
+ {
+ newXZ = Vector3.MoveTowards(currentXZ, targetXZ, moveSpeed * Time.deltaTime);
+ }
+
+ // Resolve Y from terrain.
+ float groundY = SampleTerrainY(new Vector3(newXZ.x, 0f, newXZ.z));
+ transform.position = new Vector3(newXZ.x, groundY + heightOffset, newXZ.z);
+ }
+
+ ///
+ /// Casts a ray straight down at and returns the hit Y, or
+ /// if nothing was hit on the
+ /// terrain layer.
+ ///
+ private float SampleTerrainY(Vector3 xzPos)
+ {
+ // Ray origin: high above the map. terrainRaycastMaxDistance defines how far to cast.
+ Vector3 origin = new Vector3(xzPos.x, terrainRaycastMaxDistance, xzPos.z);
+ if (Physics.Raycast(origin, Vector3.down, out RaycastHit hit,
+ terrainRaycastMaxDistance * 2f, terrainLayerMask))
+ {
+ return hit.point.y;
+ }
+
+ // Fallback: builder hovers above the buildable plane.
+ return GridCoordinates.BUILDABLE_PLANE_Y;
+ }
+
+ // ----- Server move API --------------------------------------------
+
+ ///
+ /// Server-side entry point: directly sets the move target. Called by the input
+ /// controller's Rpc handler after validation. Out-of-map positions are clamped
+ /// to the current position (no-op).
+ ///
+ public void ServerSetMoveTarget(Vector3 worldPos)
+ {
+ if (!IsServer)
+ {
+ Debug.LogError("[Builder] ServerSetMoveTarget called on a client.");
+ return;
+ }
+
+ // Clamp to map area: convert XZ to a tile and check IsInMap.
+ var loader = LevelLoader.Instance;
+ if (loader != null && loader.IsLoaded)
+ {
+ Vector2Int tile = GridCoordinates.WorldToGrid(worldPos);
+ if (!loader.IsInMap(tile))
+ {
+ // Out-of-map move requests are rejected silently. Could log if useful for
+ // debugging client/server mismatch, but otherwise this is normal.
+ return;
+ }
+ }
+
+ // Y is overwritten by terrain raycast each Update; we only honor X and Z here.
+ targetPosition.Value = new Vector3(worldPos.x, 0f, worldPos.z);
+ }
+
+ // ----- Owner-only move RPC ----------------------------------------
+
+ ///
+ /// Owner-only Rpc: a client requests their builder move to a world position.
+ /// Server validates (in-map check) and applies via .
+ ///
+ [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)]
+ public void RequestMoveRpc(Vector3 worldPos)
+ {
+ ServerSetMoveTarget(worldPos);
+ }
+
+ // ----- Range query (used by TowerPlacementManager) ----------------
+
+ ///
+ /// True if the tower with the given anchor and footprint size is within build range
+ /// of this builder's CURRENT position. Range is measured from builder center to the
+ /// nearest point of the tower footprint, in world units.
+ ///
+ ///
+ /// "Nearest point of the footprint" rather than "footprint center" so that a tower
+ /// is reachable when ANY of its tiles is within range, even if the center is
+ /// slightly outside. Aligns with player intuition that "I can reach this tile."
+ ///
+ public bool IsTileWithinBuildRange(Vector2Int anchor, Vector2Int footprintSize)
+ {
+ Vector3 builderXZ = new Vector3(transform.position.x, 0f, transform.position.z);
+
+ // Find the point on the footprint rectangle nearest to the builder.
+ float minX = anchor.x * GridCoordinates.TILE_SIZE - GridCoordinates.TILE_SIZE * 0.5f;
+ float maxX = (anchor.x + footprintSize.x - 1) * GridCoordinates.TILE_SIZE
+ + GridCoordinates.TILE_SIZE * 0.5f;
+ float minZ = anchor.y * GridCoordinates.TILE_SIZE - GridCoordinates.TILE_SIZE * 0.5f;
+ float maxZ = (anchor.y + footprintSize.y - 1) * GridCoordinates.TILE_SIZE
+ + GridCoordinates.TILE_SIZE * 0.5f;
+
+ float nearestX = Mathf.Clamp(builderXZ.x, minX, maxX);
+ float nearestZ = Mathf.Clamp(builderXZ.z, minZ, maxZ);
+ Vector3 nearestPoint = new Vector3(nearestX, 0f, nearestZ);
+
+ return Vector3.SqrMagnitude(builderXZ - nearestPoint)
+ <= buildRange * buildRange;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/Builder.cs.meta b/Assets/_Project/Scripts/Gameplay/Builder.cs.meta
new file mode 100644
index 0000000..e069441
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/Builder.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 05b2c04367f8c864bb5e3e03ba42dde5
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/BuilderInputController.cs b/Assets/_Project/Scripts/Gameplay/BuilderInputController.cs
new file mode 100644
index 0000000..e26313a
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/BuilderInputController.cs
@@ -0,0 +1,121 @@
+// Assets/_Project/Scripts/Gameplay/BuilderInputController.cs
+using Unity.Netcode;
+using UnityEngine;
+using UnityEngine.InputSystem;
+using TD.Core;
+
+namespace TD.Gameplay
+{
+ ///
+ /// Owner-only client-side controller for builder input. Handles right-click-to-move,
+ /// deferring to placement mode (right-click cancels placement instead).
+ ///
+ ///
+ /// Owner-only. This component lives on the same GameObject as
+ /// but only its owning client processes input. Non-owner clients
+ /// have this component but its Update is a no-op. The owner sends move requests via
+ /// .
+ ///
+ /// Right-click priority. If TowerPlacementController.IsPlacing is
+ /// true, right-click cancels placement (handled by TowerPlacementController
+ /// itself). When NOT placing, right-click moves the builder.
+ ///
+ /// Raycast target. The cursor is raycast against the BuildablePlane layer
+ /// (same as placement). The hit point's XZ is sent as the target; Y is recomputed by
+ /// the server via terrain raycast.
+ ///
+ public class BuilderInputController : NetworkBehaviour
+ {
+ // ----- Inspector --------------------------------------------------
+
+ [Tooltip("Physics layer mask for the BuildablePlane collider. Cursor is raycast " +
+ "against this layer to determine the move target.")]
+ [SerializeField] private LayerMask buildablePlaneLayerMask;
+
+ [Tooltip("Maximum raycast distance for cursor → world conversion.")]
+ [SerializeField] private float raycastMaxDistance = 500f;
+
+ // Cached reference to the sibling Builder component.
+ private Builder builder;
+
+ // Cached reference to the local TowerPlacementController, looked up lazily because
+ // it may not exist at OnNetworkSpawn time.
+ private TowerPlacementController cachedPlacementController;
+
+ // ----- Lifecycle --------------------------------------------------
+
+ public override void OnNetworkSpawn()
+ {
+ builder = GetComponent();
+ if (builder == null)
+ {
+ Debug.LogError("[BuilderInputController] Missing Builder component on the " +
+ "same GameObject. Disabling input.");
+ enabled = false;
+ return;
+ }
+
+ // Non-owners do nothing.
+ if (!IsOwner)
+ {
+ enabled = false;
+ }
+ }
+
+ // ----- Input handling ---------------------------------------------
+
+ private void Update()
+ {
+ // Defensive: enabled is set false for non-owners in OnNetworkSpawn, but keep
+ // the IsOwner check here in case order-of-operations changes.
+ if (!IsOwner) return;
+
+ var mouse = Mouse.current;
+ if (mouse == null) return;
+
+ if (!mouse.rightButton.wasPressedThisFrame) return;
+
+ // Defer to placement mode: if the player is placing a tower, right-click cancels
+ // placement rather than moving the builder. TowerPlacementController handles
+ // the cancel itself; we just don't process the click here.
+ if (IsLocalPlayerPlacing()) return;
+
+ // Cursor → world.
+ if (!TryGetBuildablePlaneHit(mouse.position.ReadValue(), out Vector3 worldPoint))
+ return;
+
+ // Submit to server.
+ builder.RequestMoveRpc(worldPoint);
+ }
+
+ // ----- Helpers ----------------------------------------------------
+
+ private bool IsLocalPlayerPlacing()
+ {
+ if (cachedPlacementController == null)
+ {
+ // Find lazily — controller may have been added after this component spawned.
+ cachedPlacementController =
+ UnityEngine.Object.FindAnyObjectByType();
+ if (cachedPlacementController == null) return false;
+ }
+ return cachedPlacementController.IsPlacing;
+ }
+
+ private bool TryGetBuildablePlaneHit(Vector2 screenPos, out Vector3 hitPoint)
+ {
+ hitPoint = Vector3.zero;
+
+ var cam = Camera.main;
+ if (cam == null) return false;
+
+ Ray ray = cam.ScreenPointToRay(new Vector3(screenPos.x, screenPos.y, 0f));
+ if (Physics.Raycast(ray, out RaycastHit hit, raycastMaxDistance, buildablePlaneLayerMask))
+ {
+ hitPoint = hit.point;
+ return true;
+ }
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/BuilderInputController.cs.meta b/Assets/_Project/Scripts/Gameplay/BuilderInputController.cs.meta
new file mode 100644
index 0000000..098e39e
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/BuilderInputController.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 85df68e96d71b3f4cb302a197a6a4e05
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/CameraController.cs b/Assets/_Project/Scripts/Gameplay/CameraController.cs
new file mode 100644
index 0000000..9b56e65
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/CameraController.cs
@@ -0,0 +1,459 @@
+// Assets/_Project/Scripts/Gameplay/CameraController.cs
+using UnityEngine;
+using UnityEngine.InputSystem;
+using TD.Core;
+using TD.Levels;
+
+namespace TD.Gameplay
+{
+ ///
+ /// Per-client RTS-style camera controller. Lives on a "camera rig" GameObject (the pivot)
+ /// with a child GameObject. The pivot tracks the focus point on the
+ /// buildable plane; the camera child orbits the pivot at a configurable pitch and dolly
+ /// distance. Yaw is fixed.
+ ///
+ ///
+ /// Rig math. The pivot's world rotation is set to (pitch, 0, 0). The
+ /// camera child sits at local position (0, 0, -dolly) with identity local rotation.
+ /// When the pivot rotates, the child's world position rotates with it, placing the camera
+ /// up-and-back from the pivot at the right angle to look down at it. At pitch 0° the camera
+ /// is horizontal (worm's-eye); at 90° it's directly overhead (top-down).
+ ///
+ /// Inputs.
+ ///
+ /// - WASD or Arrow keys — pan the pivot across the buildable plane.
+ /// - Mouse near screen edge — edge-pan, when enabled.
+ /// - Scroll wheel — dolly zoom (cursor-anchored).
+ /// - Alt + Scroll wheel — adjust pitch.
+ ///
+ ///
+ /// Bounds clamping. The pivot's tile is required to satisfy
+ /// . When a frame's combined movement would leave the map,
+ /// the controller tries each axis independently and applies whichever still lands in-map —
+ /// producing wall-sliding behavior. If the loader isn't ready yet, clamping is skipped.
+ ///
+ /// Speed scaling. Pan speed scales linearly with the current dolly distance, so
+ /// zoomed-out panning covers more ground per second and zoomed-in panning is precise.
+ ///
+ /// Public API for minimap. , ,
+ /// , exist so the minimap (when implemented)
+ /// can drive the camera without this controller knowing about it.
+ ///
+ public class CameraController : MonoBehaviour
+ {
+ // ----- Inspector --------------------------------------------------
+
+ [Header("Rig")]
+ [Tooltip("The camera GameObject that lives as a child of this rig pivot. " +
+ "Auto-found via GetComponentInChildren if left empty.")]
+ [SerializeField] private Camera cameraChild;
+
+ [Header("Pan")]
+ [Tooltip("Base pan speed in world units per second at minimum zoom. " +
+ "Effective speed scales linearly with current dolly distance.")]
+ [SerializeField] private float basePanSpeed = 12f;
+
+ [Tooltip("Distance in pixels from each screen edge that triggers edge-pan. " +
+ "Smaller = needs precise mouse positioning; larger = activates more easily.")]
+ [SerializeField] private float edgePanMarginPixels = 16f;
+
+ [Tooltip("Whether edge-panning is enabled. Toggleable at runtime via " +
+ "EdgePanEnabled property — wire this to a settings menu when one exists.")]
+ [SerializeField] private bool edgePanEnabled = true;
+
+ [Header("Zoom")]
+ [Tooltip("Closest zoom (smallest dolly distance from pivot to camera).")]
+ [SerializeField] private float minDollyDistance = 5f;
+
+ [Tooltip("Farthest zoom (largest dolly distance from pivot to camera).")]
+ [SerializeField] private float maxDollyDistance = 50f;
+
+ [Tooltip("Starting dolly distance at match start.")]
+ [SerializeField] private float startDollyDistance = 20f;
+
+ [Tooltip("Dolly distance change per scroll wheel tick (positive value; sign is " +
+ "applied based on scroll direction). Tuned for ±1-per-click scroll input. " +
+ "Adjust to taste.")]
+ [SerializeField] private float zoomSpeed = 3f;
+
+ [Tooltip("If true, zooming pulls the world toward the cursor — the screen point under " +
+ "the cursor stays roughly anchored to the same world point. If false, zoom " +
+ "centers on the screen midpoint.")]
+ [SerializeField] private bool cursorAnchoredZoom = true;
+
+ [Header("Pitch")]
+ [Tooltip("Minimum pitch angle in degrees. 0° is horizontal; 90° is top-down.")]
+ [SerializeField] private float minPitchDegrees = 30f;
+
+ [Tooltip("Maximum pitch angle in degrees. 0° is horizontal; 90° is top-down.")]
+ [SerializeField] private float maxPitchDegrees = 75f;
+
+ [Tooltip("Starting pitch angle at match start.")]
+ [SerializeField] private float startPitchDegrees = 60f;
+
+ [Tooltip("Pitch change in degrees per Alt+Scroll tick. Tuned for ±1-per-click scroll " +
+ "input. Adjust to taste.")]
+ [SerializeField] private float pitchSpeed = 4f;
+
+ // ----- Runtime state ----------------------------------------------
+
+ private float currentDolly;
+ private float currentPitch;
+
+ // True between BeginDrag and EndDrag (minimap drag mode). Suppresses normal
+ // input handling so minimap-driven movement isn't fighting keyboard/edge-pan.
+ private bool isExternalDragActive;
+
+ // ----- Public API -------------------------------------------------
+
+ /// Whether edge-panning is currently active. Wire to settings UI.
+ public bool EdgePanEnabled
+ {
+ get => edgePanEnabled;
+ set => edgePanEnabled = value;
+ }
+
+ /// Snaps the pivot to . Y is ignored. Used by the
+ /// minimap for click-to-jump.
+ public void JumpTo(Vector3 worldPos)
+ {
+ Vector3 newPivot = new Vector3(worldPos.x, GridCoordinates.BUILDABLE_PLANE_Y, worldPos.z);
+ TryMovePivotTo(newPivot);
+ ApplyTransform();
+ }
+
+ /// Begins external drag mode (e.g., from the minimap). Subsequent
+ /// calls drive the pivot directly, bypassing keyboard/edge
+ /// input until .
+ public void BeginDrag() => isExternalDragActive = true;
+
+ /// Updates the pivot to the dragged world position. Same effect as
+ /// but intended to be called every frame during a drag.
+ public void UpdateDrag(Vector3 worldPos) => JumpTo(worldPos);
+
+ /// Ends external drag mode. Normal input handling resumes.
+ public void EndDrag() => isExternalDragActive = false;
+
+ // ----- Lifecycle --------------------------------------------------
+
+ private void Start()
+ {
+ if (cameraChild == null) cameraChild = GetComponentInChildren();
+ if (cameraChild == null)
+ {
+ Debug.LogError("[CameraController] No child Camera found. Place a Camera as " +
+ "a child of this GameObject or assign it in the inspector.");
+ enabled = false;
+ return;
+ }
+
+ currentDolly = Mathf.Clamp(startDollyDistance, minDollyDistance, maxDollyDistance);
+ currentPitch = Mathf.Clamp(startPitchDegrees, minPitchDegrees, maxPitchDegrees);
+
+ // Compute initial pivot position. Falls back to current transform if loader or
+ // local-player data isn't ready (e.g., editor preview before Start Host).
+ Vector3 startPivot = ComputeStartPivot(transform.position);
+ transform.position = startPivot;
+
+ ApplyTransform();
+ }
+
+ private void Update()
+ {
+ if (isExternalDragActive)
+ {
+ // Minimap or other external system is driving — don't fight it.
+ return;
+ }
+
+ HandleZoomAndPitch();
+ HandlePan();
+ }
+
+ // ----- Input handlers ---------------------------------------------
+
+ private void HandlePan()
+ {
+ Vector2 dir = Vector2.zero;
+
+ // Keyboard: WASD + arrow keys
+ var kb = Keyboard.current;
+ if (kb != null)
+ {
+ if (kb.aKey.isPressed || kb.leftArrowKey.isPressed) dir.x -= 1f;
+ if (kb.dKey.isPressed || kb.rightArrowKey.isPressed) dir.x += 1f;
+ if (kb.sKey.isPressed || kb.downArrowKey.isPressed) dir.y -= 1f;
+ if (kb.wKey.isPressed || kb.upArrowKey.isPressed) dir.y += 1f;
+ }
+
+ // Edge-pan: mouse near screen edge adds to keyboard direction.
+ if (edgePanEnabled && Mouse.current != null)
+ {
+ Vector2 mousePos = Mouse.current.position.ReadValue();
+ if (mousePos.x <= edgePanMarginPixels) dir.x -= 1f;
+ if (mousePos.x >= Screen.width - edgePanMarginPixels) dir.x += 1f;
+ if (mousePos.y <= edgePanMarginPixels) dir.y -= 1f;
+ if (mousePos.y >= Screen.height - edgePanMarginPixels) dir.y += 1f;
+ }
+
+ if (dir.sqrMagnitude < 0.0001f) return;
+
+ // Normalize to prevent diagonal speed boost when both axes active.
+ if (dir.sqrMagnitude > 1f) dir.Normalize();
+
+ // Scale speed by current zoom: at min zoom multiplier=1, at max zoom multiplier
+ // is the ratio of max-to-min dolly distance.
+ float speedMultiplier = currentDolly / minDollyDistance;
+ float distance = basePanSpeed * speedMultiplier * Time.deltaTime;
+
+ // dir.x maps to world X, dir.y maps to world Z (since pivot is on XZ plane).
+ Vector3 desired = transform.position + new Vector3(dir.x, 0f, dir.y) * distance;
+ TryMovePivotTo(desired);
+ ApplyTransform();
+ }
+
+ private void HandleZoomAndPitch()
+ {
+ var mouse = Mouse.current;
+ var kb = Keyboard.current;
+ if (mouse == null) return;
+
+ // Scroll wheel values are inconsistent across platforms and devices:
+ // - Windows click-wheel mice: ±120 per click (legacy Win32 convention)
+ // - Free-spin wheels and trackpads: small fractional values per tick
+ // - macOS / Linux mice: typically ±1 per click
+ // We use the raw value with no normalization. Speed defaults are tuned for
+ // ±1-per-click hardware (Steam Deck, macOS, many Linux/Windows mice). On
+ // ±120-per-click hardware, scroll naturally feels faster — which usually
+ // matches the "discrete tick" feel users expect from those mice anyway.
+ float scrollDelta = mouse.scroll.ReadValue().y;
+ if (Mathf.Abs(scrollDelta) < 0.0001f) return;
+
+ bool altHeld = kb != null &&
+ (kb.leftAltKey.isPressed || kb.rightAltKey.isPressed);
+
+ if (altHeld)
+ {
+ // Alt + scroll: adjust pitch.
+ currentPitch = Mathf.Clamp(currentPitch - scrollDelta * pitchSpeed,
+ minPitchDegrees, maxPitchDegrees);
+ ApplyTransform();
+ }
+ else
+ {
+ // Plain scroll: dolly zoom. Negative scroll = zoom out.
+ ApplyZoom(scrollDelta * zoomSpeed, mouse.position.ReadValue());
+ }
+ }
+
+ // ----- Zoom with cursor anchor ------------------------------------
+
+ private void ApplyZoom(float zoomDelta, Vector2 cursorScreen)
+ {
+ if (!cursorAnchoredZoom)
+ {
+ currentDolly = Mathf.Clamp(currentDolly - zoomDelta,
+ minDollyDistance, maxDollyDistance);
+ ApplyTransform();
+ return;
+ }
+
+ // Cursor-anchored zoom:
+ // 1. Find the world point under the cursor BEFORE the zoom.
+ // 2. Apply zoom (changes dolly).
+ // 3. Find the world point under the cursor AFTER the zoom.
+ // 4. Translate the pivot by (before - after) so the cursor's world point stays put.
+
+ if (!TryGroundPlanePoint(cursorScreen, out Vector3 before))
+ {
+ // Cursor not over the ground plane — fall back to non-anchored zoom.
+ currentDolly = Mathf.Clamp(currentDolly - zoomDelta,
+ minDollyDistance, maxDollyDistance);
+ ApplyTransform();
+ return;
+ }
+
+ currentDolly = Mathf.Clamp(currentDolly - zoomDelta,
+ minDollyDistance, maxDollyDistance);
+ ApplyTransform();
+
+ if (!TryGroundPlanePoint(cursorScreen, out Vector3 after))
+ {
+ return;
+ }
+
+ Vector3 desiredPivot = transform.position + (before - after);
+ TryMovePivotTo(desiredPivot);
+ ApplyTransform();
+ }
+
+ // ----- Pivot movement with bounds clamping ------------------------
+
+ ///
+ /// Attempts to move the pivot to . If the new position would
+ /// leave the map, tries each axis independently and applies whichever still lands in
+ /// the map (wall-sliding). If neither axis works, the pivot doesn't move.
+ ///
+ private void TryMovePivotTo(Vector3 desired)
+ {
+ // If LevelLoader isn't loaded, accept the move as-is (editor preview, early frames).
+ var loader = LevelLoader.Instance;
+ if (loader == null || !loader.IsLoaded)
+ {
+ transform.position = new Vector3(desired.x, GridCoordinates.BUILDABLE_PLANE_Y, desired.z);
+ return;
+ }
+
+ Vector3 current = transform.position;
+
+ if (IsPositionInMap(loader, desired))
+ {
+ transform.position = new Vector3(desired.x, GridCoordinates.BUILDABLE_PLANE_Y, desired.z);
+ return;
+ }
+
+ // Try X-only move (slide along the Z wall).
+ Vector3 xOnly = new Vector3(desired.x, current.y, current.z);
+ if (IsPositionInMap(loader, xOnly))
+ {
+ transform.position = new Vector3(xOnly.x, GridCoordinates.BUILDABLE_PLANE_Y, xOnly.z);
+ return;
+ }
+
+ // Try Z-only move (slide along the X wall).
+ Vector3 zOnly = new Vector3(current.x, current.y, desired.z);
+ if (IsPositionInMap(loader, zOnly))
+ {
+ transform.position = new Vector3(zOnly.x, GridCoordinates.BUILDABLE_PLANE_Y, zOnly.z);
+ return;
+ }
+
+ // Both axes blocked — don't move.
+ }
+
+ private static bool IsPositionInMap(LevelLoader loader, Vector3 worldPos)
+ {
+ Vector2Int tile = GridCoordinates.WorldToGrid(worldPos);
+ return loader.IsInMap(tile);
+ }
+
+ // ----- Transform composition --------------------------------------
+
+ ///
+ /// Applies to the pivot rotation and
+ /// to the camera's local Z offset. Yaw stays at 0.
+ ///
+ private void ApplyTransform()
+ {
+ transform.rotation = Quaternion.Euler(currentPitch, 0f, 0f);
+ cameraChild.transform.localPosition = new Vector3(0f, 0f, -currentDolly);
+ cameraChild.transform.localRotation = Quaternion.identity;
+ }
+
+ // ----- Ground-plane raycast helper --------------------------------
+
+ ///
+ /// Casts a ray from the camera through against the
+ /// buildable plane (Y = ). Returns
+ /// false if the ray is parallel to the plane or the cursor is above the horizon.
+ ///
+ private bool TryGroundPlanePoint(Vector2 screenPos, out Vector3 worldPos)
+ {
+ worldPos = default;
+ Ray ray = cameraChild.ScreenPointToRay(new Vector3(screenPos.x, screenPos.y, 0f));
+
+ Plane ground = new Plane(Vector3.up, new Vector3(0f, GridCoordinates.BUILDABLE_PLANE_Y, 0f));
+ if (!ground.Raycast(ray, out float enter)) return false;
+
+ worldPos = ray.GetPoint(enter);
+ return true;
+ }
+
+ // ----- Initial position -------------------------------------------
+
+ ///
+ /// Computes the initial pivot position as the centroid of the local player's owned
+ /// tiles. Falls back to the map center, and finally to
+ /// (the rig's authoring-time position) if neither is available yet.
+ ///
+ private Vector3 ComputeStartPivot(Vector3 fallback)
+ {
+ var loader = LevelLoader.Instance;
+ if (loader == null || !loader.IsLoaded) return fallback;
+
+ PlayerSlot localSlot = GetLocalPlayerSlot();
+
+ if (localSlot != PlayerSlot.None)
+ {
+ if (TryComputeZoneCentroid(loader, localSlot, out Vector3 centroid))
+ return centroid;
+ }
+
+ // Fall back to map center.
+ return ComputeMapCenter(loader.LevelData);
+ }
+
+ private static bool TryComputeZoneCentroid(LevelLoader loader, PlayerSlot slot,
+ out Vector3 centroid)
+ {
+ centroid = default;
+
+ var levelData = loader.LevelData;
+ if (levelData == null || levelData.OwnerGrid == null) return false;
+
+ // Sum the world positions of every tile owned by this slot.
+ // Using Vector2 sums to avoid Y precision drift.
+ Vector2 sum = Vector2.zero;
+ int count = 0;
+
+ for (int y = 0; y < levelData.GridSize.y; y++)
+ {
+ for (int x = 0; x < levelData.GridSize.x; x++)
+ {
+ int idx = y * levelData.GridSize.x + x;
+ if (levelData.OwnerGrid[idx] != slot) continue;
+
+ Vector2Int tile = new Vector2Int(
+ levelData.GridOriginTile.x + x,
+ levelData.GridOriginTile.y + y);
+ Vector3 world = GridCoordinates.GridToWorld(tile);
+ sum.x += world.x;
+ sum.y += world.z;
+ count++;
+ }
+ }
+
+ if (count == 0) return false;
+
+ centroid = new Vector3(sum.x / count, GridCoordinates.BUILDABLE_PLANE_Y, sum.y / count);
+ return true;
+ }
+
+ private static Vector3 ComputeMapCenter(LevelData levelData)
+ {
+ if (levelData == null) return Vector3.zero;
+ float cx = (levelData.GridOriginTile.x + (levelData.GridSize.x - 1) * 0.5f)
+ * GridCoordinates.TILE_SIZE;
+ float cz = (levelData.GridOriginTile.y + (levelData.GridSize.y - 1) * 0.5f)
+ * GridCoordinates.TILE_SIZE;
+ return new Vector3(cx, GridCoordinates.BUILDABLE_PLANE_Y, cz);
+ }
+
+ // ----- Player slot ------------------------------------------------
+
+ ///
+ /// Returns the local player's PlayerSlot.
+ /// STUB: same trivial mapping used elsewhere; replaced when MatchState lands.
+ ///
+ private static PlayerSlot GetLocalPlayerSlot()
+ {
+ var nm = Unity.Netcode.NetworkManager.Singleton;
+ if (nm == null || !nm.IsClient) return PlayerSlot.None;
+
+ ulong clientId = nm.LocalClientId;
+ byte slotByte = (byte)(clientId + 1);
+ if (slotByte < 1 || slotByte > 9) return PlayerSlot.None;
+ return (PlayerSlot)slotByte;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/CameraController.cs.meta b/Assets/_Project/Scripts/Gameplay/CameraController.cs.meta
new file mode 100644
index 0000000..882c598
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/CameraController.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 1e670a36689801e428873c5712ea3679
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/LevelLoader.cs b/Assets/_Project/Scripts/Gameplay/LevelLoader.cs
index b515909..bfa8fe0 100644
--- a/Assets/_Project/Scripts/Gameplay/LevelLoader.cs
+++ b/Assets/_Project/Scripts/Gameplay/LevelLoader.cs
@@ -74,15 +74,30 @@ namespace TD.Gameplay
// The mutable walkability grid. Initialized from LevelData.WalkabilityGrid
// and mutated by tower placement at runtime. Stays in lockstep with the
- // baked grid until towers are placed (none yet, since tower placement
- // isn't implemented).
+ // baked grid until towers are placed.
//
- // This is intentionally NOT exposed through a property yet -- consumers
- // will query through IsWalkable(Vector2Int) instead, hiding the array
- // indexing. When tower placement needs to mutate it, we'll expose a
- // SetWalkable method then. Easier to add than to take away.
+ // Consumers query through IsWalkable(Vector2Int) and mutate through
+ // SetWalkable(Vector2Int, bool), which hide the flat-array indexing.
+ //
+ // NOTE: This grid will move to MatchState when that NetworkBehaviour is
+ // implemented, so server-authoritative mutation is co-located with other
+ // match-wide state. For now it lives here because no consumer needed it
+ // to live elsewhere.
private bool[] runtimeWalkability;
+ // Per-tile tower occupancy. True when a tower's footprint covers this tile.
+ // Distinct from runtimeWalkability because:
+ // (a) spawner and leak-exit tiles are walkable but can never be occupied
+ // by a tower — tracking them separately avoids ambiguity.
+ // (b) the placement ghost needs to detect tile conflicts independently
+ // of walkability (a tile is walkable until the tower is placed,
+ // but the ghost should turn red as soon as a prior placement
+ // reserves it).
+ //
+ // Initialized to all-false on Awake. Mutated via SetOccupied; queried
+ // via IsOccupied. Will move to MatchState alongside runtimeWalkability.
+ private bool[] runtimeOccupied;
+
// The buildable-plane GameObject we instantiated as our child.
// Cached for inspector debugging and future destruction.
private GameObject buildablePlaneGO;
@@ -113,6 +128,7 @@ namespace TD.Gameplay
}
InitializeRuntimeWalkability();
+ InitializeRuntimeOccupied();
SpawnBuildablePlane();
IsLoaded = true;
@@ -190,6 +206,13 @@ namespace TD.Gameplay
level.WalkabilityGrid, runtimeWalkability, level.WalkabilityGrid.Length);
}
+ private void InitializeRuntimeOccupied()
+ {
+ // All tiles start unoccupied. bool[] default-initializes to false,
+ // so Array.Clear is redundant but makes intent explicit.
+ runtimeOccupied = new bool[level.WalkabilityGrid.Length];
+ }
+
private void SpawnBuildablePlane()
{
// Compute the world-space center and size of the grid.
@@ -314,6 +337,59 @@ namespace TD.Gameplay
return level.OwnerGrid[idx];
}
+ ///
+ /// True if is currently occupied by a placed tower footprint.
+ /// Returns false for out-of-bounds tiles. Unlike , this grid
+ /// starts all-false and only becomes true when a tower is successfully placed.
+ ///
+ ///
+ /// The placement ghost uses this (alongside and
+ /// ) to decide whether to render as valid (white) or invalid (red).
+ /// The server uses it as part of the tile-availability check before path validation.
+ ///
+ public bool IsOccupied(Vector2Int tile)
+ {
+ if (!TryFlatIndex(tile, out int idx)) return false;
+ return runtimeOccupied[idx];
+ }
+
+ // ----- Runtime mutators -------------------------------------------
+ //
+ // Called by TowerPlacementManager (server-side) when a tower placement
+ // is accepted. Both mutators must be called for every tile in the tower's
+ // footprint so the two grids stay in sync.
+ //
+ // These are not RPCs — LevelLoader is a plain MonoBehaviour, not a
+ // NetworkBehaviour. The server calls these directly after authoritative
+ // validation; clients learn about the change when the TowerInstance
+ // NetworkObject spawns and its Start/OnNetworkSpawn stamps its own
+ // footprint locally.
+
+ ///
+ /// Sets the runtime walkability of . Called by
+ /// TowerPlacementManager on the server when a tower is accepted (pass
+ /// false) and when a tower is sold/destroyed (pass true).
+ /// No-ops silently for out-of-bounds tiles.
+ ///
+ public void SetWalkable(Vector2Int tile, bool walkable)
+ {
+ if (!TryFlatIndex(tile, out int idx)) return;
+ runtimeWalkability[idx] = walkable;
+ }
+
+ ///
+ /// Sets the runtime occupancy of . Called alongside
+ /// — always update both grids together so they
+ /// stay in sync. Pass true when a tower is placed, false when
+ /// it is sold or destroyed.
+ /// No-ops silently for out-of-bounds tiles.
+ ///
+ public void SetOccupied(Vector2Int tile, bool occupied)
+ {
+ if (!TryFlatIndex(tile, out int idx)) return;
+ runtimeOccupied[idx] = occupied;
+ }
+
// Translates world-tile coordinates to a flat-array index, returning
// false if the tile is out of bounds. Used by all query methods.
private bool TryFlatIndex(Vector2Int tile, out int idx)
@@ -332,9 +408,9 @@ namespace TD.Gameplay
// Gizmos run in both edit mode and play mode. In edit mode we use the
// baked walkability/owner grids from the LevelData asset directly,
// because runtimeWalkability hasn't been initialized yet. In play mode
- // we use runtimeWalkability so the visualization reflects any future
- // tower stamps. Owner and placement grids are immutable, so we read
- // them from the asset in both modes.
+ // we use runtimeWalkability so the visualization reflects tower stamps.
+ // Owner and placement grids are immutable, so we read them from the
+ // asset in both modes.
private void OnDrawGizmos()
{
diff --git a/Assets/_Project/Scripts/Gameplay/PlayerBuilderSpawner.cs b/Assets/_Project/Scripts/Gameplay/PlayerBuilderSpawner.cs
new file mode 100644
index 0000000..6e3a0fa
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/PlayerBuilderSpawner.cs
@@ -0,0 +1,141 @@
+// Assets/_Project/Scripts/Gameplay/PlayerBuilderSpawner.cs
+using Unity.Netcode;
+using UnityEngine;
+using TD.Core;
+using TD.Levels;
+
+namespace TD.Gameplay
+{
+ ///
+ /// Lives on the Player Prefab. On the server, when the player NetworkObject spawns,
+ /// instantiates and spawns a separate NetworkObject owned by that
+ /// player. The builder is positioned at the centroid of the player's zone before spawn.
+ ///
+ ///
+ /// Why a separate NetworkObject? Multi-builder races (Path E) become "spawn
+ /// N builder NetworkObjects" without restructuring the Player Prefab. See the design
+ /// discussion in Path D scoping.
+ ///
+ /// Server-only. Spawning is server-authoritative. Non-server peers are
+ /// no-ops; they just receive the resulting Builder NetworkObject like any other
+ /// replicated spawn.
+ ///
+ /// Lifetime. The spawned builder is destroyed when the player NetworkObject
+ /// despawns (e.g., disconnect). NGO does this automatically because we set
+ /// destroyWithScene and store no other references — the builder's despawn cleans
+ /// up the static registry in .
+ ///
+ public class PlayerBuilderSpawner : NetworkBehaviour
+ {
+ [Tooltip("Builder prefab to instantiate. Must be registered with the NetworkManager " +
+ "as a network prefab.")]
+ [SerializeField] private GameObject builderPrefab;
+
+ // Cached reference so we can despawn the builder if needed (e.g., player disconnects).
+ private NetworkObject spawnedBuilder;
+
+ public override void OnNetworkSpawn()
+ {
+ if (!IsServer) return;
+
+ if (builderPrefab == null)
+ {
+ Debug.LogError("[PlayerBuilderSpawner] No Builder prefab assigned. " +
+ "Cannot spawn builder for client " + OwnerClientId + ".");
+ return;
+ }
+
+ SpawnBuilderForOwner();
+ }
+
+ public override void OnNetworkDespawn()
+ {
+ // When the player despawns (disconnect), also despawn their builder if it still exists.
+ if (IsServer && spawnedBuilder != null && spawnedBuilder.IsSpawned)
+ {
+ spawnedBuilder.Despawn(destroy: true);
+ }
+ spawnedBuilder = null;
+ }
+
+ private void SpawnBuilderForOwner()
+ {
+ // Compute initial position: centroid of this player's zone.
+ // Falls back to origin if loader/zone data isn't available.
+ Vector3 spawnPos = ComputeZoneCentroid(OwnerToSlot(OwnerClientId));
+
+ var go = Instantiate(builderPrefab, spawnPos, Quaternion.identity);
+ var netObj = go.GetComponent();
+ if (netObj == null)
+ {
+ Debug.LogError("[PlayerBuilderSpawner] Builder prefab is missing a " +
+ "NetworkObject component. Cannot spawn.");
+ Destroy(go);
+ return;
+ }
+
+ netObj.SpawnWithOwnership(OwnerClientId, destroyWithScene: true);
+ spawnedBuilder = netObj;
+
+ // Tell NetworkTransform to teleport to the spawn position, so clients don't
+ // interpolate from the prefab's authoring position (typically the origin) to
+ // the spawn position over the first few sync ticks. Without this, clients see
+ // the builder smoothly drift from world origin to its spawn point — which is
+ // exactly what we don't want for a brand-new spawn.
+ //
+ // Teleport sets a one-frame "no interpolation" flag that NetworkTransform
+ // honors on its next sync, so clients snap to the position instead.
+ var netTransform = go.GetComponent();
+ if (netTransform != null)
+ {
+ netTransform.Teleport(spawnPos, Quaternion.identity, go.transform.localScale);
+ }
+ }
+
+ // ----- Helpers ----------------------------------------------------
+
+ ///
+ /// Stub mapping: client 0 = Player1, client 1 = Player2, etc.
+ /// Replaced by MatchState's authoritative assignment when that lands.
+ ///
+ private static PlayerSlot OwnerToSlot(ulong clientId)
+ {
+ byte slotByte = (byte)(clientId + 1);
+ if (slotByte < 1 || slotByte > 9) return PlayerSlot.None;
+ return (PlayerSlot)slotByte;
+ }
+
+ private static Vector3 ComputeZoneCentroid(PlayerSlot slot)
+ {
+ var loader = LevelLoader.Instance;
+ if (loader == null || !loader.IsLoaded) return Vector3.zero;
+ if (slot == PlayerSlot.None) return Vector3.zero;
+
+ var levelData = loader.LevelData;
+ if (levelData == null || levelData.OwnerGrid == null) return Vector3.zero;
+
+ Vector2 sum = Vector2.zero;
+ int count = 0;
+
+ for (int y = 0; y < levelData.GridSize.y; y++)
+ {
+ for (int x = 0; x < levelData.GridSize.x; x++)
+ {
+ int idx = y * levelData.GridSize.x + x;
+ if (levelData.OwnerGrid[idx] != slot) continue;
+
+ Vector2Int tile = new Vector2Int(
+ levelData.GridOriginTile.x + x,
+ levelData.GridOriginTile.y + y);
+ Vector3 world = GridCoordinates.GridToWorld(tile);
+ sum.x += world.x;
+ sum.y += world.z;
+ count++;
+ }
+ }
+
+ if (count == 0) return Vector3.zero;
+ return new Vector3(sum.x / count, GridCoordinates.BUILDABLE_PLANE_Y, sum.y / count);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/PlayerBuilderSpawner.cs.meta b/Assets/_Project/Scripts/Gameplay/PlayerBuilderSpawner.cs.meta
new file mode 100644
index 0000000..e88bf80
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/PlayerBuilderSpawner.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 0f4c46f8263f72541b0f782b446de941
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/PlayerGoldManager.cs b/Assets/_Project/Scripts/Gameplay/PlayerGoldManager.cs
index 65dfca6..b178ca8 100644
--- a/Assets/_Project/Scripts/Gameplay/PlayerGoldManager.cs
+++ b/Assets/_Project/Scripts/Gameplay/PlayerGoldManager.cs
@@ -143,6 +143,25 @@ namespace TD.Gameplay
currentGold.Value += amount;
}
+ ///
+ /// Server-side entry point for deducting gold (tower placement, other costs).
+ /// Direct call — not Rpc-wrapped — because deductions always originate from
+ /// server-authoritative validation (e.g., TowerPlacementManager after
+ /// all checks have passed). Clamps to zero; gold cannot go negative.
+ ///
+ public void DeductGold(int amount)
+ {
+ if (!IsServer)
+ {
+ Debug.LogError("[PlayerGoldManager] DeductGold called on a client. " +
+ "Only server code should call this directly.");
+ return;
+ }
+
+ if (amount <= 0) return;
+ currentGold.Value = Mathf.Max(0, currentGold.Value - amount);
+ }
+
// --- Server-side Rpc ---------------------------------------------
// InvokePermission = Owner: only the client that owns this NetworkObject
@@ -175,4 +194,4 @@ namespace TD.Gameplay
currentGold.Value -= amount;
}
}
-}
+}
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/TowerInstance.cs b/Assets/_Project/Scripts/Gameplay/TowerInstance.cs
new file mode 100644
index 0000000..ba5a535
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/TowerInstance.cs
@@ -0,0 +1,273 @@
+// Assets/_Project/Scripts/Gameplay/TowerInstance.cs
+using Unity.Collections;
+using Unity.Netcode;
+using UnityEngine;
+using TD.Core;
+using TD.Towers;
+
+namespace TD.Gameplay
+{
+ ///
+ /// Per-tower runtime component. Lives on the tower's NetworkObject prefab root.
+ ///
+ /// Responsibilities:
+ ///
+ /// - Hold the network-replicated identity of this tower: which
+ /// it is and which owns it.
+ /// - On , stamp the tower's footprint into
+ /// on every client so local grids stay in sync
+ /// with the server-authoritative state.
+ /// - Apply the owner's player color to the tower mesh, so towers are
+ /// visually distinct by zone during testing.
+ ///
+ ///
+ ///
+ /// Grid stamping split. The server stamps the footprint in
+ /// TowerPlacementManager.ProcessRequest (before NetworkObject.Spawn)
+ /// so the path-validity check in the same frame sees the updated grid. Non-host
+ /// clients stamp in when NGO replicates the
+ /// NetworkObject to them. The server's also runs,
+ /// but by then the footprint is already stamped — and
+ /// are idempotent writes, so double-stamping is safe.
+ ///
+ /// Definition reference replication. TowerDefinition assets live in the
+ /// project on all clients. We replicate the asset by name via a
+ /// holding a FixedString64Bytes, then look
+ /// up the asset locally. This avoids serializing the full ScriptableObject over the
+ /// network. The lookup uses a singleton that must be
+ /// present in the scene. (Temporary: will be driven by RaceDefinition in Path E.)
+ ///
+ /// Combat. No combat logic here yet. Combat fields live stubbed on
+ /// ; they will be consumed by a future
+ /// TowerCombat component added to the same prefab.
+ ///
+ [RequireComponent(typeof(NetworkObject))]
+ public class TowerInstance : NetworkBehaviour
+ {
+ // ----- Networked state ------------------------------------------------
+
+ // The name of the TowerDefinition asset for this tower. Replicated so all
+ // clients can look up the full definition locally without sending the whole
+ // ScriptableObject over the wire.
+ private readonly NetworkVariable definitionName =
+ new NetworkVariable(
+ default,
+ readPerm: NetworkVariableReadPermission.Everyone,
+ writePerm: NetworkVariableWritePermission.Server);
+
+ // The footprint anchor (SW corner, world-tile coords). Replicated so
+ // clients can stamp the correct tiles in OnNetworkSpawn.
+ private readonly NetworkVariable anchorTile =
+ new NetworkVariable(
+ default,
+ readPerm: NetworkVariableReadPermission.Everyone,
+ writePerm: NetworkVariableWritePermission.Server);
+
+ // The PlayerSlot that placed this tower. Replicated for the HUD context
+ // panel and for view-selection by non-owning clients.
+ private readonly NetworkVariable ownerSlot =
+ new NetworkVariable(
+ PlayerSlot.None,
+ readPerm: NetworkVariableReadPermission.Everyone,
+ writePerm: NetworkVariableWritePermission.Server);
+
+ // ----- Local resolved state -------------------------------------------
+
+ // Resolved on every client in OnNetworkSpawn from definitionName.
+ // Null if the lookup fails (missing TowerRegistry or unknown name).
+ private TowerDefinition resolvedDefinition;
+
+ // ----- Pre-spawn initialization data ----------------------------------
+ //
+ // Set by InitializeServer (called by TowerPlacementManager BEFORE Spawn).
+ // Read by the server's OnNetworkSpawn to populate the NetworkVariables.
+ //
+ // Why this two-step dance: NGO 2.x disallows writing NetworkVariables
+ // before NetworkObject.Spawn() — those writes produce warnings and may
+ // not replicate reliably. The supported pattern is to set NVs inside
+ // OnNetworkSpawn on the server; NGO captures those writes and includes
+ // them in the initial sync message sent to clients, so every client
+ // sees correct values on its very first OnNetworkSpawn callback.
+
+ private TowerDefinition pendingDefinition;
+ private Vector2Int pendingAnchor;
+ private PlayerSlot pendingOwner = PlayerSlot.None;
+ private bool hasPendingInit;
+
+ // ----- Public accessors -----------------------------------------------
+
+ /// The TowerDefinition for this tower, resolved locally. Null until
+ /// runs and the definition lookup succeeds.
+ public TowerDefinition Definition => resolvedDefinition;
+
+ /// The PlayerSlot that placed this tower.
+ public PlayerSlot Owner => ownerSlot.Value;
+
+ /// The footprint anchor tile (SW corner, world-tile coords).
+ public Vector2Int AnchorTile => anchorTile.Value;
+
+ // ----- Server-only initialization -------------------------------------
+
+ ///
+ /// Called by TowerPlacementManager on the server immediately after
+ /// instantiation and before NetworkObject.Spawn. Stores the data that
+ /// the server's will copy into the
+ /// NetworkVariables. NetworkVariables themselves are NOT written here —
+ /// see the comment on the pending-init fields above for why.
+ ///
+ public void InitializeServer(TowerDefinition def, Vector2Int anchor, PlayerSlot owner)
+ {
+ var nm = NetworkManager.Singleton;
+ if (nm == null || !nm.IsServer)
+ {
+ Debug.LogError("[TowerInstance] InitializeServer called when not running " +
+ "as a server. This must only be called by " +
+ "TowerPlacementManager on the server.");
+ return;
+ }
+
+ pendingDefinition = def;
+ pendingAnchor = anchor;
+ pendingOwner = owner;
+ hasPendingInit = true;
+
+ // Cache the resolved definition on the server immediately — clients
+ // will resolve via the registry lookup once definitionName arrives.
+ resolvedDefinition = def;
+ }
+
+ // ----- NGO lifecycle --------------------------------------------------
+
+ public override void OnNetworkSpawn()
+ {
+ // Server-only step: now that the NetworkObject is fully spawned,
+ // write the pending init values to the NetworkVariables. These writes
+ // will be captured in the initial sync message sent to clients, so
+ // every client sees correct values on its very first OnNetworkSpawn.
+ if (IsServer && hasPendingInit)
+ {
+ definitionName.Value = new FixedString64Bytes(pendingDefinition.name);
+ anchorTile.Value = pendingAnchor;
+ ownerSlot.Value = pendingOwner;
+
+ // Clear the pending data — it's now committed to NetworkVariables.
+ hasPendingInit = false;
+ }
+
+ // Resolve the TowerDefinition from the (now-available) replicated name.
+ // On the server this is already set by InitializeServer; the lookup is
+ // redundant but harmless and keeps the code path uniform.
+ ResolveDefinition();
+
+ // Stamp the footprint into the local LevelLoader grids.
+ // The server already stamped in TowerPlacementManager before Spawn(),
+ // but SetWalkable/SetOccupied are idempotent — double-stamping is safe.
+ StampFootprint(walkable: false, occupied: true);
+
+ // Apply owner color to the mesh renderer.
+ ApplyOwnerColor();
+
+ if (resolvedDefinition != null)
+ {
+ Debug.Log($"[TowerInstance] Spawned '{resolvedDefinition.DisplayName}' " +
+ $"for {ownerSlot.Value} at anchor {anchorTile.Value}. " +
+ $"IsServer={IsServer}");
+ }
+ }
+
+ public override void OnNetworkDespawn()
+ {
+ // Un-stamp the footprint when the tower is destroyed (sold, wave end, etc.)
+ // so the tiles become walkable and buildable again.
+ StampFootprint(walkable: true, occupied: false);
+ }
+
+ // ----- Private helpers ------------------------------------------------
+
+ private void ResolveDefinition()
+ {
+ // Already resolved (server path via InitializeServer).
+ if (resolvedDefinition != null) return;
+
+ string defName = definitionName.Value.ToString();
+ if (string.IsNullOrEmpty(defName))
+ {
+ Debug.LogError($"[TowerInstance] NetworkObject {NetworkObjectId}: " +
+ $"definitionName is empty. Cannot resolve TowerDefinition.");
+ return;
+ }
+
+ var registry = TowerRegistry.Instance;
+ if (registry == null)
+ {
+ Debug.LogError($"[TowerInstance] NetworkObject {NetworkObjectId}: " +
+ $"No TowerRegistry found in scene. Cannot resolve '{defName}'.");
+ return;
+ }
+
+ resolvedDefinition = registry.Get(defName);
+ if (resolvedDefinition == null)
+ {
+ Debug.LogError($"[TowerInstance] NetworkObject {NetworkObjectId}: " +
+ $"TowerRegistry does not contain a definition named '{defName}'.");
+ }
+ }
+
+ private void StampFootprint(bool walkable, bool occupied)
+ {
+ var loader = LevelLoader.Instance;
+ if (loader == null || !loader.IsLoaded)
+ {
+ Debug.LogWarning($"[TowerInstance] NetworkObject {NetworkObjectId}: " +
+ $"LevelLoader not available during footprint stamp. " +
+ $"Grids may be out of sync.");
+ return;
+ }
+
+ // Determine footprint size from the resolved definition, falling back to
+ // 2×2 if the definition hasn't resolved yet (shouldn't happen, but defensive).
+ Vector2Int footprintSize = resolvedDefinition != null
+ ? resolvedDefinition.FootprintSize
+ : new Vector2Int(2, 2);
+
+ foreach (var tile in GridCoordinates.GetFootprintTiles(anchorTile.Value, footprintSize))
+ {
+ loader.SetWalkable(tile, walkable);
+ loader.SetOccupied(tile, occupied);
+ }
+ }
+
+ // Reused per-instance across color updates to avoid per-call GC allocation.
+ // MaterialPropertyBlock is not thread-safe but all rendering runs on the
+ // main thread, so a single instance per TowerInstance is fine.
+ private MaterialPropertyBlock colorPropertyBlock;
+ private static readonly int ColorPropertyId = Shader.PropertyToID("_Color");
+ // URP Lit uses _BaseColor, not _Color. Writing both ensures the tint applies
+ // regardless of which shader the prefab uses; unknown property writes are
+ // silently ignored.
+ private static readonly int BaseColorPropertyId = Shader.PropertyToID("_BaseColor");
+
+ private void ApplyOwnerColor()
+ {
+ Color ownerColor = PlayerColors.Get(ownerSlot.Value);
+ ownerColor.a = 1f;
+
+ // MaterialPropertyBlock sets per-renderer properties without allocating
+ // a new Material object. Safe to reuse across calls on the same instance.
+ // All Unity standard/URP shaders expose _Color or _BaseColor, so no shader changes needed.
+ colorPropertyBlock ??= new MaterialPropertyBlock();
+ colorPropertyBlock.SetColor(ColorPropertyId, ownerColor);
+ colorPropertyBlock.SetColor(BaseColorPropertyId, ownerColor);
+
+ var renderers = GetComponentsInChildren();
+ foreach (var rend in renderers)
+ rend.SetPropertyBlock(colorPropertyBlock);
+
+ if (renderers.Length == 0)
+ {
+ Debug.LogWarning($"[TowerInstance] NetworkObject {NetworkObjectId}: " +
+ $"No MeshRenderers found for owner color tinting.");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/TowerInstance.cs.meta b/Assets/_Project/Scripts/Gameplay/TowerInstance.cs.meta
new file mode 100644
index 0000000..6ed6c73
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/TowerInstance.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: fb111fc88b3d6a340a3abde5a1502af3
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/TowerPlacementController.cs b/Assets/_Project/Scripts/Gameplay/TowerPlacementController.cs
new file mode 100644
index 0000000..5797da7
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/TowerPlacementController.cs
@@ -0,0 +1,421 @@
+// Assets/_Project/Scripts/Gameplay/TowerPlacementController.cs
+using UnityEngine;
+using UnityEngine.InputSystem;
+using TD.Core;
+using TD.Towers;
+
+namespace TD.Gameplay
+{
+ ///
+ /// Per-client controller for the tower placement UX. Handles hover raycasts against
+ /// the BuildablePlane collider, drives the placement ghost, and dispatches placement
+ /// requests to via RPC.
+ ///
+ ///
+ /// Plain MonoBehaviour. Placement visuals (ghost, cursor color) are
+ /// purely cosmetic and local. This component does not need to be a NetworkBehaviour.
+ /// All server-authoritative logic lives in .
+ ///
+ /// Ghost validity check. The ghost checks ownership, placement state,
+ /// and tile occupancy only. It does NOT run a local path check. If the server rejects
+ /// because the tower would block the path, the ghost disappears and a rejection
+ /// message is shown. This avoids the complexity of maintaining a client-side BFS
+ /// that may be slightly stale.
+ ///
+ /// Ghost colors.
+ ///
+ /// - White — all local checks pass.
+ /// - Red — any local check fails (wrong zone, not buildable, already occupied).
+ ///
+ /// Green "pending construction" ghost is a separate system implemented in Path D.
+ ///
+ /// Placement activation. The controller is idle until
+ /// is called (e.g., from a HUD tower button). The player
+ /// right-clicks or the placement is confirmed/rejected to return to idle.
+ ///
+ /// Input System. Uses the New Input System package. Mouse position and
+ /// button state are read from Mouse.current each frame.
+ ///
+ /// Player slot. The local player slot is currently a stub
+ /// (client 0 = Player1, etc.) matching TowerPlacementManager.ClientIdToPlayerSlot.
+ /// This will be replaced when MatchState carries the authoritative slot assignment.
+ ///
+ public class TowerPlacementController : MonoBehaviour
+ {
+ // ----- Inspector --------------------------------------------------
+
+ [Tooltip("Project-wide placement settings: ghost materials and rejection messages.")]
+ [SerializeField] private TowerPlacementSettings settings;
+
+ [Tooltip("Physics layer mask for the BuildablePlane collider. Set this to the " +
+ "'BuildablePlane' layer. Raycasts only hit this layer so stray colliders " +
+ "in the scene don't interfere.")]
+ [SerializeField] private LayerMask buildablePlaneLayerMask;
+
+ [Tooltip("Maximum raycast distance from the camera to the buildable plane. " +
+ "Should be at least as large as the camera's far clip plane.")]
+ [SerializeField] private float raycastMaxDistance = 500f;
+
+ // ----- Active-placement state -------------------------------------
+
+ // Null when placement mode is inactive.
+ private TowerDefinition activeDef;
+ private int activeTowerTypeId;
+
+ // The ghost GameObject: the tower prefab instantiated with transparent materials.
+ // Null when placement mode is inactive.
+ private GameObject ghostGO;
+
+ // Cached renderers on the ghost, populated when the ghost is created.
+ private Renderer[] ghostRenderers = System.Array.Empty();
+
+ // Reused each frame to avoid allocation. Lazily constructed on first use because
+ // Unity disallows MaterialPropertyBlock construction in field initializers.
+ private MaterialPropertyBlock ghostPropertyBlock;
+ private static readonly int ColorPropertyId = Shader.PropertyToID("_Color");
+
+ // The anchor tile computed last frame (used to avoid re-evaluating when the
+ // cursor hasn't moved to a new tile).
+ private Vector2Int lastAnchor;
+ private bool lastAnchorValid; // false until first raycast succeeds
+ private bool lastPlacementValid; // result of the last local validity check
+
+ // ----- Events -----------------------------------------------------
+
+ ///
+ /// Fired on the local client when the server rejects a placement request.
+ /// Payload is the human-readable rejection message from
+ /// . Subscribe here to display feedback UI.
+ ///
+ public static event System.Action OnRejectionMessageReady;
+
+ // ----- Lifecycle --------------------------------------------------
+
+ private void OnEnable()
+ {
+ TowerPlacementManager.OnPlacementRejected += HandlePlacementRejected;
+ }
+
+ private void OnDisable()
+ {
+ TowerPlacementManager.OnPlacementRejected -= HandlePlacementRejected;
+ CancelPlacement();
+ }
+
+ private void Update()
+ {
+ if (activeDef == null) return; // idle — nothing to do
+
+ var mouse = Mouse.current;
+ if (mouse == null) return; // no mouse device connected
+
+ // Right-click cancels placement.
+ if (mouse.rightButton.wasPressedThisFrame)
+ {
+ CancelPlacement();
+ return;
+ }
+
+ // Raycast mouse position against the BuildablePlane.
+ if (!TryGetBuildablePlaneHit(mouse.position.ReadValue(), out Vector3 hitPoint))
+ {
+ // Cursor is off the buildable plane — hide the ghost but stay in
+ // placement mode so the player can move back onto the plane.
+ SetGhostVisible(false);
+ lastAnchorValid = false;
+ return;
+ }
+
+ SetGhostVisible(true);
+
+ // Compute the footprint anchor from the hit point.
+ Vector2Int anchor = ComputeAnchor(hitPoint, activeDef.FootprintSize);
+
+ // Position the ghost at the footprint center.
+ Vector3 ghostPos = GridCoordinates.GetFootprintCenterWorld(anchor, activeDef.FootprintSize);
+ ghostPos.y = 0.5f; // lift off the plane so the cube base sits flush
+ ghostGO.transform.position = ghostPos;
+
+ // Only re-evaluate validity when the anchor tile changes, to avoid
+ // re-running grid lookups every frame when the cursor is stationary.
+ if (!lastAnchorValid || anchor != lastAnchor)
+ {
+ lastAnchor = anchor;
+ lastAnchorValid = true;
+ lastPlacementValid = EvaluateLocalValidity(anchor, activeDef);
+ }
+
+ // Update ghost color.
+ ApplyGhostColor(lastPlacementValid);
+
+ // Left-click to attempt placement.
+ if (mouse.leftButton.wasPressedThisFrame)
+ {
+ TrySubmitPlacement(anchor);
+ }
+ }
+
+ // ----- Public API -------------------------------------------------
+
+ ///
+ /// Activates placement mode for the given tower type. The ghost appears
+ /// immediately under the cursor. Call this from HUD tower buttons.
+ ///
+ /// The TowerDefinition to place.
+ /// The type ID registered in
+ /// .
+ public void BeginPlacement(TowerDefinition def, int towerTypeId)
+ {
+ if (def == null)
+ {
+ Debug.LogError("[TowerPlacementController] BeginPlacement called with null " +
+ "TowerDefinition.");
+ return;
+ }
+
+ // Cancel any existing placement before starting a new one.
+ CancelPlacement();
+
+ activeDef = def;
+ activeTowerTypeId = towerTypeId;
+
+ CreateGhost(def);
+ }
+
+ ///
+ /// Cancels placement mode and destroys the ghost. Safe to call when idle.
+ ///
+ public void CancelPlacement()
+ {
+ activeDef = null;
+ activeTowerTypeId = 0;
+ lastAnchorValid = false;
+
+ DestroyGhost();
+ }
+
+ ///
+ /// True when placement mode is currently active.
+ ///
+ public bool IsPlacing => activeDef != null;
+
+ // ----- Ghost management -------------------------------------------
+
+ private void CreateGhost(TowerDefinition def)
+ {
+ if (def.TowerPrefab == null)
+ {
+ Debug.LogWarning($"[TowerPlacementController] '{def.DisplayName}' has no " +
+ $"TowerPrefab — ghost cannot be created.");
+ return;
+ }
+
+ ghostGO = Instantiate(def.TowerPrefab);
+ ghostGO.name = $"Ghost_{def.name}";
+
+ // Disable all NetworkObject, TowerInstance, and Collider components on
+ // the ghost — it must not participate in networking or physics.
+ DisableGhostComponents();
+
+ ghostRenderers = ghostGO.GetComponentsInChildren();
+
+ // Apply ghost valid material to all renderers initially.
+ // ApplyGhostColor will update each frame.
+ if (settings != null && settings.GhostValidMaterial != null)
+ {
+ foreach (var rend in ghostRenderers)
+ rend.sharedMaterial = settings.GhostValidMaterial;
+ }
+
+ // Start invisible; shown on the first successful raycast.
+ SetGhostVisible(false);
+ }
+
+ private void DestroyGhost()
+ {
+ if (ghostGO != null)
+ {
+ Destroy(ghostGO);
+ ghostGO = null;
+ }
+ ghostRenderers = System.Array.Empty();
+ }
+
+ private void SetGhostVisible(bool visible)
+ {
+ if (ghostGO != null)
+ ghostGO.SetActive(visible);
+ }
+
+ ///
+ /// Disables components on the ghost that must not run: NetworkObject,
+ /// TowerInstance, Colliders, and Rigidbodies. The ghost is purely visual.
+ ///
+ private void DisableGhostComponents()
+ {
+ // NetworkObject — must be disabled so NGO doesn't try to register it.
+ var netObj = ghostGO.GetComponent();
+ if (netObj != null) netObj.enabled = false;
+
+ // TowerInstance — must not stamp grids or fire OnNetworkSpawn.
+ var towerInstance = ghostGO.GetComponent();
+ if (towerInstance != null) towerInstance.enabled = false;
+
+ // Colliders — ghost must not block raycasts or physics queries.
+ foreach (var col in ghostGO.GetComponentsInChildren())
+ col.enabled = false;
+
+ // Rigidbodies — ghost must not fall or interact with physics.
+ foreach (var rb in ghostGO.GetComponentsInChildren())
+ rb.isKinematic = true;
+ }
+
+ ///
+ /// Sets the ghost material color using .
+ /// Switches between valid (white) and invalid (red) materials.
+ ///
+ private void ApplyGhostColor(bool valid)
+ {
+ if (settings == null) return;
+
+ Material ghostMat = valid ? settings.GhostValidMaterial : settings.GhostInvalidMaterial;
+ if (ghostMat == null) return;
+
+ foreach (var rend in ghostRenderers)
+ {
+ rend.sharedMaterial = ghostMat;
+ // Property block is set empty here — color comes from the material itself.
+ // If the ghost materials use _Color, override it via the block instead:
+ // ghostPropertyBlock.SetColor(ColorPropertyId, valid ? Color.white : Color.red);
+ // rend.SetPropertyBlock(ghostPropertyBlock);
+ }
+ }
+
+ // ----- Raycasting -------------------------------------------------
+
+ private bool TryGetBuildablePlaneHit(Vector2 screenPos, out Vector3 hitPoint)
+ {
+ hitPoint = Vector3.zero;
+
+ var cam = Camera.main;
+ if (cam == null) return false;
+
+ Ray ray = cam.ScreenPointToRay(new Vector3(screenPos.x, screenPos.y, 0f));
+ if (Physics.Raycast(ray, out RaycastHit hit, raycastMaxDistance, buildablePlaneLayerMask))
+ {
+ hitPoint = hit.point;
+ return true;
+ }
+ return false;
+ }
+
+ // ----- Anchor computation -----------------------------------------
+
+ ///
+ /// Converts a world hit point to the footprint anchor tile (SW corner) such
+ /// that the footprint center is as close as possible to the hit point.
+ ///
+ ///
+ /// For a 2×2 footprint: anchor = (Round(hitX - 0.5), Round(hitZ - 0.5))
+ /// For a 1×1 footprint: anchor = (Round(hitX), Round(hitZ))
+ /// For a 3×3 footprint: anchor = (Round(hitX - 1.0), Round(hitZ - 1.0))
+ ///
+ private static Vector2Int ComputeAnchor(Vector3 hitPoint, Vector2Int footprintSize)
+ {
+ float t = GridCoordinates.TILE_SIZE;
+ float halfW = (footprintSize.x - 1) * 0.5f;
+ float halfH = (footprintSize.y - 1) * 0.5f;
+
+ int anchorX = Mathf.RoundToInt(hitPoint.x / t - halfW);
+ int anchorY = Mathf.RoundToInt(hitPoint.z / t - halfH);
+ return new Vector2Int(anchorX, anchorY);
+ }
+
+ // ----- Local validity check ---------------------------------------
+
+ ///
+ /// Evaluates the local (client-side) placement validity. Checks ownership,
+ /// placement state, and occupancy. Does NOT check gold or run a path BFS —
+ /// those are server-side only.
+ ///
+ private bool EvaluateLocalValidity(Vector2Int anchor, TowerDefinition def)
+ {
+ var loader = LevelLoader.Instance;
+ if (loader == null || !loader.IsLoaded) return false;
+
+ PlayerSlot localSlot = GetLocalPlayerSlot();
+
+ foreach (var tile in GridCoordinates.GetFootprintTiles(anchor, def.FootprintSize))
+ {
+ if (loader.GetOwner(tile) != localSlot) return false;
+ if (loader.GetPlacement(tile) != PlacementState.Buildable) return false;
+ if (loader.IsOccupied(tile)) return false;
+ }
+
+ return true;
+ }
+
+ // ----- Placement submission ---------------------------------------
+
+ private void TrySubmitPlacement(Vector2Int anchor)
+ {
+ var manager = TowerPlacementManager.Instance;
+ if (manager == null)
+ {
+ Debug.LogWarning("[TowerPlacementController] No TowerPlacementManager in " +
+ "scene. Cannot submit placement request.");
+ return;
+ }
+
+ // Send the RPC regardless of local validity state — the server is
+ // authoritative. The local check drives the ghost color only.
+ // The server will reject and send back a reason if invalid.
+ manager.RequestPlaceTowerRpc(anchor.x, anchor.y, activeTowerTypeId);
+
+ // Exit placement mode immediately after submitting. If the server
+ // rejects, the rejection message fires via HandlePlacementRejected.
+ // If it accepts, the TowerInstance NetworkObject spawns and the
+ // placed tower appears — no ghost lingering is needed.
+ CancelPlacement();
+ }
+
+ // ----- Rejection feedback -----------------------------------------
+
+ private void HandlePlacementRejected(PlacementRejectionReason reason)
+ {
+ if (settings == null)
+ {
+ Debug.LogWarning($"[TowerPlacementController] Placement rejected: {reason} " +
+ $"(no TowerPlacementSettings assigned — cannot show message).");
+ return;
+ }
+
+ string message = settings.GetRejectionMessage(reason);
+
+ Debug.Log($"[TowerPlacementController] Placement rejected: {reason} → \"{message}\"");
+
+ // Fire the event so HUD components can display the message on screen.
+ // The HUD that subscribes and renders this is implemented in a later path.
+ OnRejectionMessageReady?.Invoke(message);
+ }
+
+ // ----- Player slot ------------------------------------------------
+
+ ///
+ /// Returns the local player's PlayerSlot.
+ /// STUB: Uses the same trivial client-ID → slot mapping as
+ /// TowerPlacementManager.ClientIdToPlayerSlot. Will be replaced
+ /// when MatchState carries the authoritative assignment.
+ ///
+ private static PlayerSlot GetLocalPlayerSlot()
+ {
+ var nm = Unity.Netcode.NetworkManager.Singleton;
+ if (nm == null || !nm.IsClient) return PlayerSlot.None;
+
+ ulong clientId = nm.LocalClientId;
+ byte slotByte = (byte)(clientId + 1);
+ if (slotByte < 1 || slotByte > 9) return PlayerSlot.None;
+ return (PlayerSlot)slotByte;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/TowerPlacementController.cs.meta b/Assets/_Project/Scripts/Gameplay/TowerPlacementController.cs.meta
new file mode 100644
index 0000000..2d64acb
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/TowerPlacementController.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 124253f2cbc2d9f46befb6e1763cd6b9
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/TowerPlacementManager.cs b/Assets/_Project/Scripts/Gameplay/TowerPlacementManager.cs
new file mode 100644
index 0000000..f65af84
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/TowerPlacementManager.cs
@@ -0,0 +1,547 @@
+// Assets/_Project/Scripts/Gameplay/TowerPlacementManager.cs
+using System.Collections.Generic;
+using Unity.Netcode;
+using UnityEngine;
+using TD.Core;
+using TD.Levels;
+using TD.Towers;
+
+namespace TD.Gameplay
+{
+ ///
+ /// Server-authoritative manager for tower placement requests. Receives placement
+ /// requests from clients via RPC, validates them in order, and either spawns the
+ /// tower or rejects the request with a reason code.
+ ///
+ ///
+ /// Queue-based processing. Incoming RPCs enqueue a
+ /// rather than validating inline. Each server
+ /// Update drains up to requests. At 60 fps this
+ /// gives ~180 validations/second, comfortably above the worst-case 90/second
+ /// (9 players × 10 placements/second). Queuing keeps the server frame budget
+ /// predictable regardless of burst traffic.
+ ///
+ /// Server-only logic. All validation and mutation runs on the server.
+ /// Clients learn about accepted placements when the
+ /// NetworkObject spawns (NGO replicates it automatically). Clients learn about
+ /// rejections via .
+ ///
+ /// Validation order:
+ ///
+ /// - Ownership — every footprint tile must be owned by the requesting player.
+ /// - Placement state — every footprint tile must be Buildable and unoccupied.
+ /// - Gold — the placing player must have enough gold.
+ /// - Path — a BFS confirms every spawner in the placing player's zone still reaches
+ /// an exit after the footprint is stamped as non-walkable.
+ ///
+ ///
+ /// Path-check BFS. The server temporarily stamps the footprint,
+ /// runs BFS per spawner, then un-stamps if the check fails. This is O(tiles in zone)
+ /// per spawner per request — acceptable for low-frequency gameplay actions and the
+ /// queue-rate-limited processing model.
+ ///
+ /// Builder range check. Deliberately omitted in Path B. The builder
+ /// system does not exist yet. When Path D is implemented, add a range check between
+ /// steps 2 and 3 above, gated on the requesting player's Builder position.
+ ///
+ public class TowerPlacementManager : NetworkBehaviour
+ {
+ // ----- Singleton --------------------------------------------------
+
+ ///
+ /// The active TowerPlacementManager. Null before the scene loads or after it unloads.
+ /// Always null-check before use. Only meaningful on the server; clients route all
+ /// placement through RPC.
+ ///
+ public static TowerPlacementManager Instance { get; private set; }
+
+ // ----- Inspector --------------------------------------------------
+
+ [Tooltip("Maximum number of placement requests processed per server Update tick. " +
+ "At 60 fps, 3 requests/frame = 180 validations/second, well above the " +
+ "worst-case 90/second (9 players × 10 placements/second).")]
+ [SerializeField] private int requestsPerFrame = 3;
+
+ [Tooltip("Tower definitions available in this match, indexed by TowerTypeId. " +
+ "Populate this with every TowerDefinition asset the current race roster " +
+ "contains. Index 0 is reserved; valid IDs start at 1. " +
+ "(Temporary: will be driven by RaceDefinition once Path E is complete.)")]
+ [SerializeField] private TowerDefinition[] towerDefinitions = new TowerDefinition[0];
+
+ // ----- Internal request queue -------------------------------------
+
+ private struct PlacementRequest
+ {
+ public ulong SenderClientId;
+ public Vector2Int Anchor; // SW corner of the footprint, in world-tile coords.
+ public int TowerTypeId; // Index into towerDefinitions[].
+ }
+
+ private readonly Queue pendingRequests = new Queue();
+
+ // Reusable scratch collections for BFS — allocated once and cleared per
+ // check to avoid per-frame GC pressure. Only used on the server.
+ private readonly Queue bfsQueue = new Queue();
+ private readonly HashSet bfsVisited = new HashSet();
+
+ // ----- Lifecycle --------------------------------------------------
+
+ public override void OnNetworkSpawn()
+ {
+ if (IsServer)
+ {
+ if (Instance != null && Instance != this)
+ {
+ Debug.LogError("[TowerPlacementManager] Multiple instances detected. " +
+ "Only one TowerPlacementManager should exist per scene.");
+ }
+ Instance = this;
+ Debug.Log("[TowerPlacementManager] Server ready.");
+ }
+ }
+
+ public override void OnNetworkDespawn()
+ {
+ if (Instance == this) Instance = null;
+ }
+
+ // ----- Server Update — queue drain --------------------------------
+
+ private void Update()
+ {
+ if (!IsServer) return;
+
+ int processed = 0;
+ while (pendingRequests.Count > 0 && processed < requestsPerFrame)
+ {
+ ProcessRequest(pendingRequests.Dequeue());
+ processed++;
+ }
+ }
+
+ // ----- RPC: client → server (placement request) -------------------
+
+ ///
+ /// Client entry point. Call this on the local client to request placing a tower.
+ /// The server will validate and either spawn the tower (visible to all clients)
+ /// or call back with .
+ ///
+ /// X component of the footprint anchor tile (world-tile coords).
+ /// Y component of the footprint anchor tile (world-tile coords).
+ /// Index into the server's towerDefinitions array.
+ [Rpc(SendTo.Server)]
+ public void RequestPlaceTowerRpc(int anchorX, int anchorY, int towerTypeId,
+ RpcParams rpcParams = default)
+ {
+ pendingRequests.Enqueue(new PlacementRequest
+ {
+ SenderClientId = rpcParams.Receive.SenderClientId,
+ Anchor = new Vector2Int(anchorX, anchorY),
+ TowerTypeId = towerTypeId,
+ });
+ }
+
+ // ----- RPC: server → client (rejection notification) --------------
+
+ ///
+ /// Sent by the server to the placing client when a placement request is rejected.
+ /// The client uses the reason to display a feedback message to the player.
+ ///
+ [Rpc(SendTo.SpecifiedInParams)]
+ private void PlacementRejectedRpc(PlacementRejectionReason reason,
+ RpcParams rpcParams = default)
+ {
+ // This executes on the target client.
+ Debug.Log($"[TowerPlacementManager] Placement rejected: {reason}");
+
+ // TowerPlacementController listens for this via the static event below.
+ OnPlacementRejected?.Invoke(reason);
+ }
+
+ ///
+ /// Fired on the local client when the server rejects this client's placement request.
+ /// subscribes to this to display rejection
+ /// feedback messages.
+ ///
+ public static event System.Action OnPlacementRejected;
+
+ // ----- Core validation and placement logic ------------------------
+
+ private void ProcessRequest(PlacementRequest req)
+ {
+ // Resolve the TowerDefinition first — needed by every subsequent check.
+ if (!TryGetDefinition(req.TowerTypeId, out TowerDefinition def))
+ {
+ Reject(req, PlacementRejectionReason.InvalidTowerType);
+ return;
+ }
+
+ var loader = LevelLoader.Instance;
+ if (loader == null || !loader.IsLoaded)
+ {
+ Reject(req, PlacementRejectionReason.ServerError);
+ return;
+ }
+
+ // Collect the footprint tiles once; used by all subsequent checks.
+ var footprint = new List(def.FootprintSize.x * def.FootprintSize.y);
+ foreach (var tile in GridCoordinates.GetFootprintTiles(req.Anchor, def.FootprintSize))
+ footprint.Add(tile);
+
+ // ------------------------------------------------------------------
+ // Check 1: Ownership
+ // Every footprint tile must be owned by the placing player's zone.
+ // ------------------------------------------------------------------
+ PlayerSlot placingSlot = ClientIdToPlayerSlot(req.SenderClientId);
+ if (placingSlot == PlayerSlot.None)
+ {
+ Reject(req, PlacementRejectionReason.ServerError);
+ return;
+ }
+
+ foreach (var tile in footprint)
+ {
+ if (loader.GetOwner(tile) != placingSlot)
+ {
+ Reject(req, PlacementRejectionReason.WrongOwner);
+ return;
+ }
+ }
+
+ // ------------------------------------------------------------------
+ // Check 2: Placement state + occupancy
+ // Every footprint tile must be Buildable and not already occupied.
+ // ------------------------------------------------------------------
+ foreach (var tile in footprint)
+ {
+ if (loader.GetPlacement(tile) != PlacementState.Buildable)
+ {
+ Reject(req, PlacementRejectionReason.TileNotBuildable);
+ return;
+ }
+ if (loader.IsOccupied(tile))
+ {
+ Reject(req, PlacementRejectionReason.TileOccupied);
+ return;
+ }
+ }
+
+ // ------------------------------------------------------------------
+ // Check 3: Build range
+ // Tower must be within the placing player's builder's build range.
+ // Cheap to check; runs before gold and path so we don't burn cycles
+ // on out-of-range placements.
+ // ------------------------------------------------------------------
+ var builder = Builder.GetForClient(req.SenderClientId);
+ if (builder == null)
+ {
+ // No builder spawned for this client — server-side error, not a player-facing one.
+ Reject(req, PlacementRejectionReason.ServerError);
+ return;
+ }
+ if (!builder.IsTileWithinBuildRange(req.Anchor, def.FootprintSize))
+ {
+ Reject(req, PlacementRejectionReason.OutOfRange);
+ return;
+ }
+
+ // ------------------------------------------------------------------
+ // Check 4: Gold
+ // Validate before the path check — cheaper, and no point running BFS
+ // if the player can't afford the tower.
+ // ------------------------------------------------------------------
+ var goldManager = PlayerGoldManager.GetForClient(req.SenderClientId);
+ if (goldManager == null || goldManager.CurrentGold < def.GoldCost)
+ {
+ Reject(req, PlacementRejectionReason.InsufficientGold);
+ return;
+ }
+
+ // ------------------------------------------------------------------
+ // Check 5: Path validity
+ // Temporarily stamp the footprint, run BFS per spawner in the placing
+ // player's zone, then un-stamp if any spawner loses its exit route.
+ // ------------------------------------------------------------------
+ StampFootprint(loader, footprint, walkable: false, occupied: true);
+
+ bool pathValid = CheckPathValidity(loader, placingSlot);
+
+ if (!pathValid)
+ {
+ // Un-stamp — the placement is rejected, grid stays as it was.
+ StampFootprint(loader, footprint, walkable: true, occupied: false);
+ Reject(req, PlacementRejectionReason.BlocksPath);
+ return;
+ }
+
+ // ------------------------------------------------------------------
+ // All checks passed — commit the placement.
+ // The footprint stamp is already applied (walkable=false, occupied=true).
+ // Deduct gold and spawn the tower NetworkObject.
+ // ------------------------------------------------------------------
+ goldManager.DeductGold(def.GoldCost);
+
+ SpawnTower(def, req.Anchor, placingSlot);
+
+ Debug.Log($"[TowerPlacementManager] Placed '{def.DisplayName}' for " +
+ $"client {req.SenderClientId} ({placingSlot}) at anchor {req.Anchor}.");
+ }
+
+ // ----- Path-validity BFS ------------------------------------------
+
+ ///
+ /// Returns true if every spawner in 's zone can still
+ /// reach an exit tile (any leak exit tile OR any goal tile) via walkable tiles,
+ /// given the current state of LevelLoader's runtime walkability grid.
+ ///
+ ///
+ /// Mirrors bake-time P5-4. Runs a BFS per spawner against the runtime walkability
+ /// grid. Reuses and scratch
+ /// collections (cleared between BFS runs) to avoid GC allocation per call.
+ ///
+ private bool CheckPathValidity(LevelLoader loader, PlayerSlot slot)
+ {
+ var levelData = loader.LevelData;
+
+ // Find the PlayerZoneData for this slot.
+ PlayerZoneData zoneData = null;
+ foreach (var zone in levelData.PlayerZones)
+ {
+ if (zone.Owner == slot) { zoneData = zone; break; }
+ }
+ if (zoneData == null || zoneData.Spawners == null || zoneData.Spawners.Length == 0)
+ {
+ // No spawners means nothing to block — treat as valid.
+ return true;
+ }
+
+ // Build the exit tile set: union of all leak exit tiles and all goal tiles.
+ // This is built fresh per call because it doesn't change within a match
+ // (tiles never move), but the allocation cost is small and correctness
+ // is more important than micro-optimization here.
+ var exitTiles = BuildExitTileSet(levelData, slot);
+ if (exitTiles.Count == 0)
+ {
+ // Zone has no exits at all — this would have been caught at bake time (P5-8).
+ // Treat as valid so a bake-side error doesn't cause all placements to fail.
+ Debug.LogWarning($"[TowerPlacementManager] Zone {slot} has no exit tiles. " +
+ $"This should have been caught at bake time (P5-8).");
+ return true;
+ }
+
+ // BFS per spawner: each spawner's tile area is the BFS seed set.
+ foreach (var spawner in zoneData.Spawners)
+ {
+ if (!SpawnerCanReachExit(loader, spawner, exitTiles))
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Builds the set of tiles that count as "exits" for the given player zone:
+ /// all tiles of every LeakExit FROM this zone, plus all goal tiles.
+ ///
+ private HashSet BuildExitTileSet(LevelData levelData, PlayerSlot slot)
+ {
+ var exits = new HashSet();
+
+ // Leak exit tiles for this zone.
+ foreach (var zone in levelData.PlayerZones)
+ {
+ if (zone.Owner != slot) continue;
+ if (zone.LeakExits == null) continue;
+ foreach (var leak in zone.LeakExits)
+ foreach (var tile in leak.TileArea)
+ exits.Add(tile);
+ }
+
+ // Goal tiles (all goals count as exits for the final defender zone).
+ if (levelData.Goals != null)
+ foreach (var goal in levelData.Goals)
+ foreach (var tile in goal.TileArea)
+ exits.Add(tile);
+
+ return exits;
+ }
+
+ ///
+ /// BFS from 's tile area. Returns true if any exit tile
+ /// is reachable via walkable tiles. Uses the shared scratch queue and visited set.
+ ///
+ private bool SpawnerCanReachExit(LevelLoader loader, SpawnerData spawner,
+ HashSet exitTiles)
+ {
+ bfsQueue.Clear();
+ bfsVisited.Clear();
+
+ // Seed the BFS with the spawner's full tile area (not just its center tile),
+ // matching bake-time P5-4 exactly.
+ foreach (var tile in spawner.TileArea)
+ {
+ if (bfsVisited.Add(tile))
+ bfsQueue.Enqueue(tile);
+ }
+
+ while (bfsQueue.Count > 0)
+ {
+ var current = bfsQueue.Dequeue();
+
+ if (exitTiles.Contains(current))
+ return true;
+
+ foreach (var neighbor in GridCoordinates.GetNeighbors(current))
+ {
+ if (bfsVisited.Contains(neighbor)) continue;
+ if (!loader.IsWalkable(neighbor)) continue;
+ bfsVisited.Add(neighbor);
+ bfsQueue.Enqueue(neighbor);
+ }
+ }
+
+ return false;
+ }
+
+ // ----- Helpers ----------------------------------------------------
+
+ ///
+ /// Stamps or un-stamps all tiles in on both the
+ /// walkability and occupancy grids simultaneously. Always update both together.
+ ///
+ private static void StampFootprint(LevelLoader loader, List footprint,
+ bool walkable, bool occupied)
+ {
+ foreach (var tile in footprint)
+ {
+ loader.SetWalkable(tile, walkable);
+ loader.SetOccupied(tile, occupied);
+ }
+ }
+
+ ///
+ /// Spawns the tower NetworkObject at the footprint center and records the
+ /// placing player's slot on the component.
+ ///
+ private void SpawnTower(TowerDefinition def, Vector2Int anchor, PlayerSlot owner)
+ {
+ if (def.TowerPrefab == null)
+ {
+ Debug.LogError($"[TowerPlacementManager] TowerDefinition '{def.DisplayName}' " +
+ $"has no TowerPrefab assigned. Cannot spawn.");
+ return;
+ }
+
+ Vector3 spawnPos = GridCoordinates.GetFootprintCenterWorld(anchor, def.FootprintSize);
+ // Towers sit on the buildable plane (Y=0); raise slightly so the mesh base
+ // sits flush rather than half-clipped. A cube of scale 1 needs +0.5 on Y.
+ spawnPos.y = 0.5f;
+
+ var go = Instantiate(def.TowerPrefab, spawnPos, Quaternion.identity);
+ var instance = go.GetComponent();
+ if (instance == null)
+ {
+ Debug.LogError($"[TowerPlacementManager] TowerPrefab '{def.TowerPrefab.name}' " +
+ $"is missing a TowerInstance component.");
+ Destroy(go);
+ return;
+ }
+
+ // Set data before network spawn so TowerInstance.OnNetworkSpawn sees it.
+ instance.InitializeServer(def, anchor, owner);
+
+ var netObj = go.GetComponent();
+ if (netObj == null)
+ {
+ Debug.LogError($"[TowerPlacementManager] TowerPrefab '{def.TowerPrefab.name}' " +
+ $"is missing a NetworkObject component.");
+ Destroy(go);
+ return;
+ }
+ netObj.Spawn(destroyWithScene: true);
+ }
+
+ // ----- Utility ----------------------------------------------------
+
+ private static bool TryGetDefinition(int typeId, out TowerDefinition def)
+ {
+ def = null;
+ // typeId 0 is reserved; valid IDs start at 1.
+ if (typeId <= 0) return false;
+ // Instance check for the static helper path — callers that have a
+ // direct reference use the instance array directly.
+ if (Instance == null) return false;
+ if (typeId >= Instance.towerDefinitions.Length) return false;
+ def = Instance.towerDefinitions[typeId];
+ return def != null;
+ }
+
+ ///
+ /// Maps a client ID to the PlayerSlot assigned to that client.
+ ///
+ ///
+ /// STUB: Currently uses a trivial mapping where client 0 = Player1, client 1 = Player2,
+ /// etc. This will be replaced when MatchState / PlayerMatchState is implemented and
+ /// carries the authoritative client-to-slot assignment.
+ ///
+ private static PlayerSlot ClientIdToPlayerSlot(ulong clientId)
+ {
+ // NGO client IDs start at 0 (host). PlayerSlot values start at 1.
+ // Cast is safe for up to 9 players; beyond that returns None.
+ byte slotByte = (byte)(clientId + 1);
+ if (slotByte < 1 || slotByte > 9) return PlayerSlot.None;
+ return (PlayerSlot)slotByte;
+ }
+
+ private void Reject(PlacementRequest req, PlacementRejectionReason reason)
+ {
+ Debug.Log($"[TowerPlacementManager] Rejected request from client " +
+ $"{req.SenderClientId} at anchor {req.Anchor}: {reason}");
+
+ // Send the rejection RPC back to only the requesting client.
+ PlacementRejectedRpc(reason,
+ new RpcParams
+ {
+ Send = new RpcSendParams
+ {
+ Target = RpcTarget.Single(req.SenderClientId, RpcTargetUse.Temp)
+ }
+ });
+ }
+ }
+
+ ///
+ /// Reason codes sent to the client when the server rejects a placement request.
+ /// Used by to display the appropriate
+ /// feedback message to the player.
+ ///
+ public enum PlacementRejectionReason
+ {
+ /// One or more footprint tiles belong to a different player's zone.
+ WrongOwner,
+
+ /// One or more footprint tiles are not in a Buildable state
+ /// (they are Restricted or Outside the map).
+ TileNotBuildable,
+
+ /// One or more footprint tiles are already occupied by an existing tower.
+ TileOccupied,
+
+ /// The placing player does not have enough gold.
+ InsufficientGold,
+
+ /// The placing player's builder is too far from the requested location.
+ OutOfRange,
+
+ /// Placing this tower would block all valid paths from at least one
+ /// spawner to its exit. The maze must remain passable.
+ BlocksPath,
+
+ /// The requested tower type ID is not in the server's definition list.
+ InvalidTowerType,
+
+ /// An unexpected server-side error occurred (e.g., LevelLoader not loaded,
+ /// client not mapped to a PlayerSlot). Check server logs.
+ ServerError,
+ }
+}
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/TowerPlacementManager.cs.meta b/Assets/_Project/Scripts/Gameplay/TowerPlacementManager.cs.meta
new file mode 100644
index 0000000..912398a
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/TowerPlacementManager.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: d369496ba7887b844b1c220b524a507d
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/TowerPlacementSettings.cs b/Assets/_Project/Scripts/Gameplay/TowerPlacementSettings.cs
new file mode 100644
index 0000000..6db99cd
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/TowerPlacementSettings.cs
@@ -0,0 +1,90 @@
+// Assets/_Project/Scripts/Gameplay/TowerPlacementSettings.cs
+using UnityEngine;
+
+namespace TD.Gameplay
+{
+ ///
+ /// Project-wide settings for the tower placement system. One asset shared across all
+ /// tower types — assign it to in the scene.
+ ///
+ ///
+ /// Why not on TowerDefinition? Ghost materials and rejection messages are
+ /// identical for every tower type. Putting them on TowerDefinition would mean 100+
+ /// assets each holding two redundant material references that must be kept in sync.
+ /// A single shared settings asset is easier to maintain and impossible to accidentally
+ /// de-sync.
+ ///
+ /// Rejection messages. These are the strings shown on screen when the
+ /// server rejects a placement. Kept here so designers can tune the wording without
+ /// touching code.
+ ///
+ [CreateAssetMenu(fileName = "TowerPlacementSettings",
+ menuName = "TD/Tower Placement Settings",
+ order = 3)]
+ public class TowerPlacementSettings : ScriptableObject
+ {
+ // ----- Ghost visuals -----------------------------------------------
+
+ [Header("Ghost Materials")]
+ [Tooltip("Material applied to the placement ghost when placement is valid. " +
+ "Should be a transparent/unlit white material so the tower mesh is " +
+ "recognizable but clearly distinct from a placed tower.")]
+ public Material GhostValidMaterial;
+
+ [Tooltip("Material applied to the placement ghost when placement is invalid. " +
+ "Should be a transparent/unlit red material.")]
+ public Material GhostInvalidMaterial;
+
+ // ----- Rejection messages ------------------------------------------
+
+ [Header("Rejection Messages")]
+ [Tooltip("Shown when the server rejects a placement because tiles belong to " +
+ "another player's zone.")]
+ public string MessageWrongOwner = "You can only place towers in your own zone.";
+
+ [Tooltip("Shown when the server rejects because one or more tiles are " +
+ "Restricted or Outside the map.")]
+ public string MessageTileNotBuildable = "That location is not buildable.";
+
+ [Tooltip("Shown when the server rejects because a tile is already occupied " +
+ "by an existing tower.")]
+ public string MessageTileOccupied = "A tower already occupies that location.";
+
+ [Tooltip("Shown when the server rejects because the player cannot afford " +
+ "the tower.")]
+ public string MessageInsufficientGold = "Not enough gold.";
+
+ [Tooltip("Shown when the server rejects because the builder is too far away " +
+ "from the requested location.")]
+ public string MessageOutOfRange = "Your builder is too far away.";
+
+ [Tooltip("Shown when the server rejects because placing the tower would block " +
+ "all valid paths through the player's zone.")]
+ public string MessageBlocksPath = "That placement would block the path.";
+
+ [Tooltip("Shown for unexpected server-side errors (invalid tower type, etc.). " +
+ "Should rarely appear in normal play.")]
+ public string MessageServerError = "Placement failed. Please try again.";
+
+ // ----- Helpers -----------------------------------------------------
+
+ ///
+ /// Returns the human-readable rejection message for the given reason.
+ ///
+ public string GetRejectionMessage(PlacementRejectionReason reason)
+ {
+ switch (reason)
+ {
+ case PlacementRejectionReason.WrongOwner: return MessageWrongOwner;
+ case PlacementRejectionReason.TileNotBuildable: return MessageTileNotBuildable;
+ case PlacementRejectionReason.TileOccupied: return MessageTileOccupied;
+ case PlacementRejectionReason.InsufficientGold: return MessageInsufficientGold;
+ case PlacementRejectionReason.OutOfRange: return MessageOutOfRange;
+ case PlacementRejectionReason.BlocksPath: return MessageBlocksPath;
+ case PlacementRejectionReason.InvalidTowerType:
+ case PlacementRejectionReason.ServerError:
+ default: return MessageServerError;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/TowerPlacementSettings.cs.meta b/Assets/_Project/Scripts/Gameplay/TowerPlacementSettings.cs.meta
new file mode 100644
index 0000000..d3b364d
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/TowerPlacementSettings.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 141062cbf116c924e8b63dd458262795
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/TowerPlacementTestTrigger.cs b/Assets/_Project/Scripts/Gameplay/TowerPlacementTestTrigger.cs
new file mode 100644
index 0000000..92d5de7
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/TowerPlacementTestTrigger.cs
@@ -0,0 +1,37 @@
+// Assets/_Project/Scripts/Gameplay/TowerPlacementTestTrigger.cs
+using UnityEngine;
+using UnityEngine.InputSystem;
+using TD.Towers;
+
+namespace TD.Gameplay
+{
+ ///
+ /// TEMPORARY test helper. Press 1 to begin placing the assigned tower.
+ /// Will be replaced by HUD tower buttons in a later path. Delete this script
+ /// once the HUD is wired up.
+ ///
+ public class TowerPlacementTestTrigger : MonoBehaviour
+ {
+ [Tooltip("The TowerDefinition to place when '1' is pressed.")]
+ [SerializeField] private TowerDefinition towerToPlace;
+
+ [Tooltip("The tower type ID. Must match the index of this TowerDefinition in " +
+ "TowerPlacementManager.towerDefinitions[]. Default 1 (slot 0 is reserved).")]
+ [SerializeField] private int towerTypeId = 1;
+
+ [Tooltip("The TowerPlacementController to drive.")]
+ [SerializeField] private TowerPlacementController controller;
+
+ private void Update()
+ {
+ var kb = Keyboard.current;
+ if (kb == null || controller == null || towerToPlace == null) return;
+
+ if (kb.digit1Key.wasPressedThisFrame)
+ {
+ controller.BeginPlacement(towerToPlace, towerTypeId);
+ Debug.Log($"[TestTrigger] BeginPlacement({towerToPlace.DisplayName}, id={towerTypeId})");
+ }
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Gameplay/TowerPlacementTestTrigger.cs.meta b/Assets/_Project/Scripts/Gameplay/TowerPlacementTestTrigger.cs.meta
new file mode 100644
index 0000000..ddbc3a4
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/TowerPlacementTestTrigger.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: ea0e3a4681be19e4e9c359c1123bf68d
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/TowerRegistry.cs b/Assets/_Project/Scripts/Gameplay/TowerRegistry.cs
new file mode 100644
index 0000000..9761603
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/TowerRegistry.cs
@@ -0,0 +1,131 @@
+// Assets/_Project/Scripts/Gameplay/TowerRegistry.cs
+using System.Collections.Generic;
+using UnityEngine;
+using TD.Towers;
+
+namespace TD.Gameplay
+{
+ ///
+ /// Scene singleton that holds every available in the
+ /// current match and lets any code look one up by asset name.
+ ///
+ ///
+ /// Why this exists. replicates a tower's
+ /// definition by name (a FixedString64Bytes over the network), then resolves
+ /// the full ScriptableObject locally on every client. TowerRegistry is the lookup
+ /// table that makes that resolution possible without hard-coding asset paths.
+ ///
+ /// Auto-discovery. On Awake, all assets
+ /// under Resources/TowerDefinitions/ are loaded automatically. No inspector
+ /// drag-and-drop required — add a new asset to that folder and it is registered at
+ /// runtime with no other changes needed. This scales cleanly to 100+ tower types.
+ ///
+ /// Path E upgrade path. In Path E the registry will filter to only the
+ /// definitions belonging to the active match's RaceDefinition rosters. For now
+ /// all assets in the Resources folder are registered.
+ ///
+ /// Plain MonoBehaviour. Not a NetworkBehaviour — the registry is
+ /// identical on every peer (same assets, same names), so there is nothing to sync.
+ ///
+ public class TowerRegistry : MonoBehaviour
+ {
+ // ----- Singleton --------------------------------------------------
+
+ ///
+ /// The active TowerRegistry. Null before Awake or after the scene unloads.
+ /// Always null-check before use.
+ ///
+ public static TowerRegistry Instance { get; private set; }
+
+ // ----- Constants --------------------------------------------------
+
+ ///
+ /// Resources-relative folder path that TowerDefinition assets must live under
+ /// to be auto-discovered. Create this folder if it doesn't exist.
+ /// Full path: Assets/Resources/TowerDefinitions/
+ ///
+ private const string ResourcesFolder = "TowerDefinitions";
+
+ // ----- Internal lookup table --------------------------------------
+
+ // Keyed by TowerDefinition.name (the asset name, not DisplayName).
+ private readonly Dictionary byName
+ = new Dictionary();
+
+ // ----- Lifecycle --------------------------------------------------
+
+ private void Awake()
+ {
+ if (Instance != null && Instance != this)
+ {
+ Debug.LogError("[TowerRegistry] Multiple instances detected. " +
+ "Only one TowerRegistry should exist per scene.");
+ return;
+ }
+ Instance = this;
+
+ BuildLookupTable();
+ }
+
+ private void OnDestroy()
+ {
+ if (Instance == this) Instance = null;
+ }
+
+ // ----- Public API -------------------------------------------------
+
+ ///
+ /// Returns the whose asset name equals
+ /// , or null if no match is found.
+ ///
+ public TowerDefinition Get(string assetName)
+ {
+ byName.TryGetValue(assetName, out var def);
+ return def;
+ }
+
+ ///
+ /// Returns all registered tower definitions. Enumerates the internal
+ /// dictionary values — do not modify the returned collection.
+ ///
+ public IEnumerable All => byName.Values;
+
+ // ----- Private ----------------------------------------------------
+
+ private void BuildLookupTable()
+ {
+ byName.Clear();
+
+ // Resources.LoadAll finds every TowerDefinition asset anywhere under
+ // Assets/Resources/TowerDefinitions/ (including sub-folders).
+ // No manual registration needed — drop an asset in the folder and it
+ // is available on the next play session.
+ var loaded = Resources.LoadAll(ResourcesFolder);
+
+ if (loaded.Length == 0)
+ {
+ Debug.LogWarning($"[TowerRegistry] No TowerDefinition assets found under " +
+ $"Resources/{ResourcesFolder}/. " +
+ $"Create the folder and add TowerDefinition assets to it.");
+ return;
+ }
+
+ foreach (var def in loaded)
+ {
+ if (def == null) continue;
+
+ if (byName.ContainsKey(def.name))
+ {
+ Debug.LogWarning($"[TowerRegistry] Duplicate asset name '{def.name}'. " +
+ $"Only the first entry will be used. Rename one of the assets.");
+ continue;
+ }
+
+ byName[def.name] = def;
+ }
+
+ Debug.Log($"[TowerRegistry] Auto-discovered and registered " +
+ $"{byName.Count} tower definition(s) from Resources/{ResourcesFolder}/.");
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Gameplay/TowerRegistry.cs.meta b/Assets/_Project/Scripts/Gameplay/TowerRegistry.cs.meta
new file mode 100644
index 0000000..6a7a87c
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/TowerRegistry.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: a9dc0fbbe4422bc479ab8db7658c082b
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Towers.meta b/Assets/_Project/Scripts/Towers.meta
new file mode 100644
index 0000000..e3d488c
--- /dev/null
+++ b/Assets/_Project/Scripts/Towers.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 905624ec4e5c8644492123ac8abf9c13
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Towers/TowerDefinition.cs b/Assets/_Project/Scripts/Towers/TowerDefinition.cs
new file mode 100644
index 0000000..2634f72
--- /dev/null
+++ b/Assets/_Project/Scripts/Towers/TowerDefinition.cs
@@ -0,0 +1,96 @@
+using UnityEngine;
+
+namespace TD.Towers
+{
+ ///
+ /// Data definition for a single tower type. One asset per tower type; shared across all
+ /// instances of that tower in a match. Consumed by tower placement, construction, and
+ /// (eventually) combat systems.
+ ///
+ ///
+ /// TowerDefinitions are authored as ScriptableObject assets and referenced by
+ /// (tower roster) and (which
+ /// definition this placed tower corresponds to). Clients load the full asset locally
+ /// from the project's ScriptableObject pool; only the asset reference (not the full data)
+ /// is replicated over the network.
+ ///
+ /// Fields marked STUBBED are defined here to lock in the data shape but are not yet
+ /// consumed by any runtime system. They will be wired in during the sessions noted.
+ ///
+ [CreateAssetMenu(fileName = "TowerDefinition", menuName = "TD/Tower Definition", order = 2)]
+ public class TowerDefinition : ScriptableObject
+ {
+ // -------------------------------------------------------------------
+ // Identity
+ // -------------------------------------------------------------------
+
+ [Header("Identity")]
+ [Tooltip("Human-readable name shown in the HUD tower grid and context panel.")]
+ public string DisplayName;
+
+ [Tooltip("Short description shown in the HUD context panel when this tower is selected.")]
+ [TextArea(2, 4)]
+ public string Description;
+
+ // -------------------------------------------------------------------
+ // Placement
+ // -------------------------------------------------------------------
+
+ [Header("Placement")]
+ [Tooltip("Footprint size in tiles. Default 2×2. The anchor tile is the south-west corner " +
+ "of the footprint (minimum x, minimum y). Every tile in the footprint must be " +
+ "Buildable and owned by the placing player for placement to succeed.")]
+ public Vector2Int FootprintSize = new Vector2Int(2, 2);
+
+ [Tooltip("Gold cost to place this tower. Deducted from the placing player's pool on " +
+ "successful server-side placement validation.")]
+ public int GoldCost;
+
+ // -------------------------------------------------------------------
+ // Construction
+ // -------------------------------------------------------------------
+
+ [Header("Construction")]
+ [Tooltip("STUBBED — not consumed until Path D (Builder system). " +
+ "Time in seconds from construction start to tower becoming active. " +
+ "A Builder must be within build range for construction to proceed. " +
+ "Set to 0 for instant construction (placeholder behaviour during Path B testing).")]
+ public float BuildTime = 0f;
+
+ // -------------------------------------------------------------------
+ // Visuals
+ // -------------------------------------------------------------------
+
+ [Header("Visuals")]
+ [Tooltip("Prefab instantiated in the world when this tower is placed. Must have a " +
+ "TowerInstance component at its root. During Path B this is a colored cube; " +
+ "replace with a real mesh when art is available.")]
+ public GameObject TowerPrefab;
+
+ // -------------------------------------------------------------------
+ // Combat — STUBBED
+ // -------------------------------------------------------------------
+
+ [Header("Combat (Stubbed — not consumed until combat system is implemented)")]
+ [Tooltip("STUBBED. Damage dealt per hit to a single target.")]
+ public float Damage;
+
+ [Tooltip("STUBBED. Attack range in world units. Enemies within this radius are targetable.")]
+ public float Range;
+
+ [Tooltip("STUBBED. Attacks per second.")]
+ public float FireRate;
+
+ [Tooltip("STUBBED. Radius of splash damage around the impact point. 0 = single target.")]
+ public float SplashRadius;
+
+ [Tooltip("STUBBED. Fraction by which enemy movement speed is multiplied on hit. " +
+ "1.0 = no slow. 0.5 = 50% slow.")]
+ [Range(0f, 1f)]
+ public float SlowFactor = 1f;
+
+ [Tooltip("STUBBED. Projectile prefab fired at targets. Null = hitscan (instant hit, no " +
+ "projectile travel).")]
+ public GameObject ProjectilePrefab;
+ }
+}
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Towers/TowerDefinition.cs.meta b/Assets/_Project/Scripts/Towers/TowerDefinition.cs.meta
new file mode 100644
index 0000000..b767f9b
--- /dev/null
+++ b/Assets/_Project/Scripts/Towers/TowerDefinition.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 7b353a757b6e6774d97e6fb8ba138fcc
\ No newline at end of file
diff --git a/Assets/_Project/Settings/PC_Renderer.asset b/Assets/_Project/Settings/PC_Renderer.asset
index 475b02e..c042b56 100644
--- a/Assets/_Project/Settings/PC_Renderer.asset
+++ b/Assets/_Project/Settings/PC_Renderer.asset
@@ -13,32 +13,28 @@ MonoBehaviour:
m_Name: PC_Renderer
m_EditorClassIdentifier:
debugShaders:
- debugReplacementPS: {fileID: 4800000, guid: cf852408f2e174538bcd9b7fda1c5ae7,
- type: 3}
+ debugReplacementPS: {fileID: 4800000, guid: cf852408f2e174538bcd9b7fda1c5ae7, type: 3}
hdrDebugViewPS: {fileID: 4800000, guid: 573620ae32aec764abd4d728906d2587, type: 3}
- probeVolumeSamplingDebugComputeShader: {fileID: 7200000, guid: 53626a513ea68ce47b59dc1299fe3959,
- type: 3}
+ probeVolumeSamplingDebugComputeShader: {fileID: 7200000, guid: 53626a513ea68ce47b59dc1299fe3959, type: 3}
probeVolumeResources:
- probeVolumeDebugShader: {fileID: 4800000, guid: e5c6678ed2aaa91408dd3df699057aae,
- type: 3}
- probeVolumeFragmentationDebugShader: {fileID: 4800000, guid: 03cfc4915c15d504a9ed85ecc404e607,
- type: 3}
- probeVolumeOffsetDebugShader: {fileID: 4800000, guid: 53a11f4ebaebf4049b3638ef78dc9664,
- type: 3}
- probeVolumeSamplingDebugShader: {fileID: 4800000, guid: 8f96cd657dc40064aa21efcc7e50a2e7,
- type: 3}
- probeSamplingDebugMesh: {fileID: -3555484719484374845, guid: 57d7c4c16e2765b47a4d2069b311bffe,
- type: 3}
- probeSamplingDebugTexture: {fileID: 2800000, guid: 24ec0e140fb444a44ab96ee80844e18e,
- type: 3}
- probeVolumeBlendStatesCS: {fileID: 7200000, guid: b9a23f869c4fd45f19c5ada54dd82176,
- type: 3}
+ probeVolumeDebugShader: {fileID: 4800000, guid: e5c6678ed2aaa91408dd3df699057aae, type: 3}
+ probeVolumeFragmentationDebugShader: {fileID: 4800000, guid: 03cfc4915c15d504a9ed85ecc404e607, type: 3}
+ probeVolumeOffsetDebugShader: {fileID: 4800000, guid: 53a11f4ebaebf4049b3638ef78dc9664, type: 3}
+ probeVolumeSamplingDebugShader: {fileID: 4800000, guid: 8f96cd657dc40064aa21efcc7e50a2e7, type: 3}
+ probeSamplingDebugMesh: {fileID: -3555484719484374845, guid: 57d7c4c16e2765b47a4d2069b311bffe, type: 3}
+ probeSamplingDebugTexture: {fileID: 2800000, guid: 24ec0e140fb444a44ab96ee80844e18e, type: 3}
+ probeVolumeBlendStatesCS: {fileID: 7200000, guid: b9a23f869c4fd45f19c5ada54dd82176, type: 3}
m_RendererFeatures:
- {fileID: 7833122117494664109}
- m_RendererFeatureMap: ad6b866f10d7b46c
+ - {fileID: 1099573597859666308}
+ m_RendererFeatureMap: ad6b866f10d7b46c84d9d2885c78420f
m_UseNativeRenderPass: 1
+ xrSystemData: {fileID: 0}
postProcessData: {fileID: 11400000, guid: 41439944d30ece34e96484bdb6645b55, type: 2}
- m_AssetVersion: 2
+ m_AssetVersion: 3
+ m_PrepassLayerMask:
+ serializedVersion: 2
+ m_Bits: 4294967295
m_OpaqueLayerMask:
serializedVersion: 2
m_Bits: 4294967295
@@ -56,8 +52,31 @@ MonoBehaviour:
m_RenderingMode: 2
m_DepthPrimingMode: 0
m_CopyDepthMode: 0
+ m_DepthAttachmentFormat: 0
+ m_DepthTextureFormat: 0
m_AccurateGbufferNormals: 0
m_IntermediateTextureMode: 0
+--- !u!114 &1099573597859666308
+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: a1614fc811f8f184697d9bee70ab9fe5, type: 3}
+ m_Name: DecalRendererFeature
+ m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.DecalRendererFeature
+ m_Active: 1
+ m_Settings:
+ technique: 0
+ maxDrawDistance: 1000
+ decalLayers: 0
+ dBufferSettings:
+ surfaceData: 2
+ screenSpaceSettings:
+ normalBlend: 0
--- !u!114 &7833122117494664109
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -84,12 +103,3 @@ MonoBehaviour:
BlurQuality: 0
Falloff: 100
SampleCount: -1
- m_BlueNoise256Textures:
- - {fileID: 2800000, guid: 36f118343fc974119bee3d09e2111500, type: 3}
- - {fileID: 2800000, guid: 4b7b083e6b6734e8bb2838b0b50a0bc8, type: 3}
- - {fileID: 2800000, guid: c06cc21c692f94f5fb5206247191eeee, type: 3}
- - {fileID: 2800000, guid: cb76dd40fa7654f9587f6a344f125c9a, type: 3}
- - {fileID: 2800000, guid: e32226222ff144b24bf3a5a451de54bc, type: 3}
- - {fileID: 2800000, guid: 3302065f671a8450b82c9ddf07426f3a, type: 3}
- - {fileID: 2800000, guid: 56a77a3e8d64f47b6afe9e3c95cb57d5, type: 3}
- m_Shader: {fileID: 4800000, guid: 0849e84e3d62649e8882e9d6f056a017, type: 3}
diff --git a/Assets/_Project/Settings/TowerPlacementSettings.asset b/Assets/_Project/Settings/TowerPlacementSettings.asset
new file mode 100644
index 0000000..1138248
--- /dev/null
+++ b/Assets/_Project/Settings/TowerPlacementSettings.asset
@@ -0,0 +1,22 @@
+%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: 141062cbf116c924e8b63dd458262795, type: 3}
+ m_Name: TowerPlacementSettings
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.TowerPlacementSettings
+ GhostValidMaterial: {fileID: 2100000, guid: a5f56f464098e1148b394962593014a2, type: 2}
+ GhostInvalidMaterial: {fileID: 2100000, guid: fa982eef7eda30e4ca8a94a76eb41d7c, type: 2}
+ MessageWrongOwner: You can only place towers in your own zone.
+ MessageTileNotBuildable: That location is not buildable.
+ MessageTileOccupied: A tower already occupies that location.
+ MessageInsufficientGold: Not enough gold.
+ MessageBlocksPath: That placement would block the path.
+ MessageServerError: Placement failed. Please try again.
diff --git a/Assets/_Project/Settings/TowerPlacementSettings.asset.meta b/Assets/_Project/Settings/TowerPlacementSettings.asset.meta
new file mode 100644
index 0000000..8ed405f
--- /dev/null
+++ b/Assets/_Project/Settings/TowerPlacementSettings.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 18f99b4b94d13eb429d4dfb9b0b37b4b
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/ProjectSettings/TagManager.asset b/ProjectSettings/TagManager.asset
index 1d9a303..cec8516 100644
--- a/ProjectSettings/TagManager.asset
+++ b/ProjectSettings/TagManager.asset
@@ -12,7 +12,7 @@ TagManager:
- Water
- UI
- BuildablePlane
- -
+ - TerrainGeometry
-
-
-
diff --git a/ProjectSettings/URPProjectSettings.asset b/ProjectSettings/URPProjectSettings.asset
index 64a8674..6ad5631 100644
--- a/ProjectSettings/URPProjectSettings.asset
+++ b/ProjectSettings/URPProjectSettings.asset
@@ -13,3 +13,4 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_LastMaterialVersion: 10
+ m_ProjectSettingFolderPath: URPDefaultResources