From a63cce53e2c0551b5233483c4dd6be32db64ce49 Mon Sep 17 00:00:00 2001 From: Matt F Date: Mon, 4 May 2026 00:01:30 -0700 Subject: [PATCH] Adding tons of new functionality Decals, ghost textures, placement functionality, builder stub ins, a new camera system, and more. --- Assets/DefaultNetworkPrefabs.asset | 10 + Assets/Resources.meta | 8 + Assets/Resources/TowerDefinitions.meta | 8 + .../TowerDefinitions/BasicTower.asset | 26 + .../TowerDefinitions/BasicTower.asset.meta | 8 + Assets/_Project/Art/Materials/M_Circle.mat | 149 ++++ .../_Project/Art/Materials/M_Circle.mat.meta | 8 + .../Art/Materials/M_TowerGhost_Invalid.mat | 141 ++++ .../Materials/M_TowerGhost_Invalid.mat.meta | 8 + .../Art/Materials/M_TowerGhost_Valid.mat | 141 ++++ .../Art/Materials/M_TowerGhost_Valid.mat.meta | 8 + Assets/_Project/Art/Textures/circle_PNG63.png | 3 + .../Art/Textures/circle_PNG63.png.meta | 117 ++++ Assets/_Project/Prefabs/Builders.meta | 8 + .../Prefabs/Builders/Builder_Basic.prefab | 282 ++++++++ .../Builders/Builder_Basic.prefab.meta | 7 + Assets/_Project/Prefabs/Player/Player.prefab | 17 +- .../Prefabs/Towers/Tower_Basic.prefab | 154 +++++ .../Prefabs/Towers/Tower_Basic.prefab.meta | 7 + Assets/_Project/Scenes/Levels/Main.unity | 642 +++++++++++++++++- Assets/_Project/Scenes/Levels/TestLevel.asset | 311 ++++----- .../Scenes/Levels/TestLevel_Thumbnail.png | 4 +- .../Scripts/Gameplay/BuildRangeIndicator.cs | 121 ++++ .../Gameplay/BuildRangeIndicator.cs.meta | 2 + Assets/_Project/Scripts/Gameplay/Builder.cs | 303 +++++++++ .../_Project/Scripts/Gameplay/Builder.cs.meta | 2 + .../Gameplay/BuilderInputController.cs | 121 ++++ .../Gameplay/BuilderInputController.cs.meta | 2 + .../Scripts/Gameplay/CameraController.cs | 459 +++++++++++++ .../Scripts/Gameplay/CameraController.cs.meta | 2 + .../_Project/Scripts/Gameplay/LevelLoader.cs | 94 ++- .../Scripts/Gameplay/PlayerBuilderSpawner.cs | 141 ++++ .../Gameplay/PlayerBuilderSpawner.cs.meta | 2 + .../Scripts/Gameplay/PlayerGoldManager.cs | 21 +- .../Scripts/Gameplay/TowerInstance.cs | 273 ++++++++ .../Scripts/Gameplay/TowerInstance.cs.meta | 2 + .../Gameplay/TowerPlacementController.cs | 421 ++++++++++++ .../Gameplay/TowerPlacementController.cs.meta | 2 + .../Scripts/Gameplay/TowerPlacementManager.cs | 547 +++++++++++++++ .../Gameplay/TowerPlacementManager.cs.meta | 2 + .../Gameplay/TowerPlacementSettings.cs | 90 +++ .../Gameplay/TowerPlacementSettings.cs.meta | 2 + .../Gameplay/TowerPlacementTestTrigger.cs | 37 + .../TowerPlacementTestTrigger.cs.meta | 2 + .../Scripts/Gameplay/TowerRegistry.cs | 131 ++++ .../Scripts/Gameplay/TowerRegistry.cs.meta | 2 + Assets/_Project/Scripts/Towers.meta | 8 + .../Scripts/Towers/TowerDefinition.cs | 96 +++ .../Scripts/Towers/TowerDefinition.cs.meta | 2 + Assets/_Project/Settings/PC_Renderer.asset | 68 +- .../Settings/TowerPlacementSettings.asset | 22 + .../TowerPlacementSettings.asset.meta | 8 + ProjectSettings/TagManager.asset | 2 +- ProjectSettings/URPProjectSettings.asset | 1 + 54 files changed, 4817 insertions(+), 238 deletions(-) create mode 100644 Assets/Resources.meta create mode 100644 Assets/Resources/TowerDefinitions.meta create mode 100644 Assets/Resources/TowerDefinitions/BasicTower.asset create mode 100644 Assets/Resources/TowerDefinitions/BasicTower.asset.meta create mode 100644 Assets/_Project/Art/Materials/M_Circle.mat create mode 100644 Assets/_Project/Art/Materials/M_Circle.mat.meta create mode 100644 Assets/_Project/Art/Materials/M_TowerGhost_Invalid.mat create mode 100644 Assets/_Project/Art/Materials/M_TowerGhost_Invalid.mat.meta create mode 100644 Assets/_Project/Art/Materials/M_TowerGhost_Valid.mat create mode 100644 Assets/_Project/Art/Materials/M_TowerGhost_Valid.mat.meta create mode 100644 Assets/_Project/Art/Textures/circle_PNG63.png create mode 100644 Assets/_Project/Art/Textures/circle_PNG63.png.meta create mode 100644 Assets/_Project/Prefabs/Builders.meta create mode 100644 Assets/_Project/Prefabs/Builders/Builder_Basic.prefab create mode 100644 Assets/_Project/Prefabs/Builders/Builder_Basic.prefab.meta create mode 100644 Assets/_Project/Prefabs/Towers/Tower_Basic.prefab create mode 100644 Assets/_Project/Prefabs/Towers/Tower_Basic.prefab.meta create mode 100644 Assets/_Project/Scripts/Gameplay/BuildRangeIndicator.cs create mode 100644 Assets/_Project/Scripts/Gameplay/BuildRangeIndicator.cs.meta create mode 100644 Assets/_Project/Scripts/Gameplay/Builder.cs create mode 100644 Assets/_Project/Scripts/Gameplay/Builder.cs.meta create mode 100644 Assets/_Project/Scripts/Gameplay/BuilderInputController.cs create mode 100644 Assets/_Project/Scripts/Gameplay/BuilderInputController.cs.meta create mode 100644 Assets/_Project/Scripts/Gameplay/CameraController.cs create mode 100644 Assets/_Project/Scripts/Gameplay/CameraController.cs.meta create mode 100644 Assets/_Project/Scripts/Gameplay/PlayerBuilderSpawner.cs create mode 100644 Assets/_Project/Scripts/Gameplay/PlayerBuilderSpawner.cs.meta create mode 100644 Assets/_Project/Scripts/Gameplay/TowerInstance.cs create mode 100644 Assets/_Project/Scripts/Gameplay/TowerInstance.cs.meta create mode 100644 Assets/_Project/Scripts/Gameplay/TowerPlacementController.cs create mode 100644 Assets/_Project/Scripts/Gameplay/TowerPlacementController.cs.meta create mode 100644 Assets/_Project/Scripts/Gameplay/TowerPlacementManager.cs create mode 100644 Assets/_Project/Scripts/Gameplay/TowerPlacementManager.cs.meta create mode 100644 Assets/_Project/Scripts/Gameplay/TowerPlacementSettings.cs create mode 100644 Assets/_Project/Scripts/Gameplay/TowerPlacementSettings.cs.meta create mode 100644 Assets/_Project/Scripts/Gameplay/TowerPlacementTestTrigger.cs create mode 100644 Assets/_Project/Scripts/Gameplay/TowerPlacementTestTrigger.cs.meta create mode 100644 Assets/_Project/Scripts/Gameplay/TowerRegistry.cs create mode 100644 Assets/_Project/Scripts/Gameplay/TowerRegistry.cs.meta create mode 100644 Assets/_Project/Scripts/Towers.meta create mode 100644 Assets/_Project/Scripts/Towers/TowerDefinition.cs create mode 100644 Assets/_Project/Scripts/Towers/TowerDefinition.cs.meta create mode 100644 Assets/_Project/Settings/TowerPlacementSettings.asset create mode 100644 Assets/_Project/Settings/TowerPlacementSettings.asset.meta 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