Adding tons of new functionality
Decals, ghost textures, placement functionality, builder stub ins, a new camera system, and more.
This commit is contained in:
parent
56dc775c68
commit
a63cce53e2
54 changed files with 4817 additions and 238 deletions
|
|
@ -19,3 +19,13 @@ MonoBehaviour:
|
||||||
SourcePrefabToOverride: {fileID: 0}
|
SourcePrefabToOverride: {fileID: 0}
|
||||||
SourceHashToOverride: 0
|
SourceHashToOverride: 0
|
||||||
OverridingTargetPrefab: {fileID: 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}
|
||||||
|
|
|
||||||
8
Assets/Resources.meta
Normal file
8
Assets/Resources.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 01de85ee5d8a2014594d9910b1a6ff55
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Resources/TowerDefinitions.meta
Normal file
8
Assets/Resources/TowerDefinitions.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3b182e413a90b2242a104b915d5b9233
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
26
Assets/Resources/TowerDefinitions/BasicTower.asset
Normal file
26
Assets/Resources/TowerDefinitions/BasicTower.asset
Normal file
|
|
@ -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}
|
||||||
8
Assets/Resources/TowerDefinitions/BasicTower.asset.meta
Normal file
8
Assets/Resources/TowerDefinitions/BasicTower.asset.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0f693e29ca953e1439e10cb8f12e4b30
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
149
Assets/_Project/Art/Materials/M_Circle.mat
Normal file
149
Assets/_Project/Art/Materials/M_Circle.mat
Normal file
|
|
@ -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
|
||||||
8
Assets/_Project/Art/Materials/M_Circle.mat.meta
Normal file
8
Assets/_Project/Art/Materials/M_Circle.mat.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f99227cbde481ce47a2527e6bca709d2
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 2100000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
141
Assets/_Project/Art/Materials/M_TowerGhost_Invalid.mat
Normal file
141
Assets/_Project/Art/Materials/M_TowerGhost_Invalid.mat
Normal file
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: fa982eef7eda30e4ca8a94a76eb41d7c
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 2100000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
141
Assets/_Project/Art/Materials/M_TowerGhost_Valid.mat
Normal file
141
Assets/_Project/Art/Materials/M_TowerGhost_Valid.mat
Normal file
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a5f56f464098e1148b394962593014a2
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 2100000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
3
Assets/_Project/Art/Textures/circle_PNG63.png
Normal file
3
Assets/_Project/Art/Textures/circle_PNG63.png
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:2b046c2e7da704ac0f4e53749151f45451e48c33e45b2e72960763caeaf71200
|
||||||
|
size 11068
|
||||||
117
Assets/_Project/Art/Textures/circle_PNG63.png.meta
Normal file
117
Assets/_Project/Art/Textures/circle_PNG63.png.meta
Normal file
|
|
@ -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:
|
||||||
8
Assets/_Project/Prefabs/Builders.meta
Normal file
8
Assets/_Project/Prefabs/Builders.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b1099735adf11d944909ce868bed668d
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
282
Assets/_Project/Prefabs/Builders/Builder_Basic.prefab
Normal file
282
Assets/_Project/Prefabs/Builders/Builder_Basic.prefab
Normal file
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3398cc5831880954487717577f61b6d7
|
||||||
|
PrefabImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -11,6 +11,7 @@ GameObject:
|
||||||
- component: {fileID: 8600750867913649879}
|
- component: {fileID: 8600750867913649879}
|
||||||
- component: {fileID: 2152427255203126265}
|
- component: {fileID: 2152427255203126265}
|
||||||
- component: {fileID: 2918837822014987993}
|
- component: {fileID: 2918837822014987993}
|
||||||
|
- component: {fileID: 7845089877743661692}
|
||||||
m_Layer: 0
|
m_Layer: 0
|
||||||
m_Name: Player
|
m_Name: Player
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
|
|
@ -45,7 +46,7 @@ MonoBehaviour:
|
||||||
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
|
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
|
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
|
||||||
GlobalObjectIdHash: 2568017024
|
GlobalObjectIdHash: 121878297
|
||||||
InScenePlacedSourceGlobalObjectIdHash: 0
|
InScenePlacedSourceGlobalObjectIdHash: 0
|
||||||
DeferredDespawnTick: 0
|
DeferredDespawnTick: 0
|
||||||
Ownership: 1
|
Ownership: 1
|
||||||
|
|
@ -72,3 +73,17 @@ MonoBehaviour:
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.PlayerGoldManager
|
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.PlayerGoldManager
|
||||||
ShowTopMostFoldoutHeaderGroup: 1
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
startingGold: 100
|
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}
|
||||||
|
|
|
||||||
154
Assets/_Project/Prefabs/Towers/Tower_Basic.prefab
Normal file
154
Assets/_Project/Prefabs/Towers/Tower_Basic.prefab
Normal file
|
|
@ -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
|
||||||
7
Assets/_Project/Prefabs/Towers/Tower_Basic.prefab.meta
Normal file
7
Assets/_Project/Prefabs/Towers/Tower_Basic.prefab.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1511641f145758b469e64376d2a0d434
|
||||||
|
PrefabImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -146,7 +146,7 @@ Transform:
|
||||||
m_GameObject: {fileID: 154690529}
|
m_GameObject: {fileID: 154690529}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
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_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
|
|
@ -185,8 +185,8 @@ BoxCollider:
|
||||||
m_ProvidesContacts: 0
|
m_ProvidesContacts: 0
|
||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_Size: {x: 35, y: 1, z: 7}
|
m_Size: {x: 20, y: 1, z: 34}
|
||||||
m_Center: {x: 0, y: 0, z: 0}
|
m_Center: {x: -13.5, y: 0, z: 8.5}
|
||||||
--- !u!1 &167151707
|
--- !u!1 &167151707
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -231,12 +231,124 @@ Transform:
|
||||||
m_GameObject: {fileID: 167151707}
|
m_GameObject: {fileID: 167151707}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
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_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 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
|
--- !u!1 &304575571
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -264,7 +376,7 @@ Transform:
|
||||||
m_GameObject: {fileID: 304575571}
|
m_GameObject: {fileID: 304575571}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
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_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
|
|
@ -284,7 +396,7 @@ MonoBehaviour:
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Levels.SpawnerVolume
|
m_EditorClassIdentifier: Assembly-CSharp::TD.Levels.SpawnerVolume
|
||||||
owner: 1
|
owner: 1
|
||||||
spawnerIdInZone: 0
|
spawnerIdInZone: 0
|
||||||
spawnFacing: 2
|
spawnFacing: 1
|
||||||
placementValidity: 0
|
placementValidity: 0
|
||||||
--- !u!65 &304575574
|
--- !u!65 &304575574
|
||||||
BoxCollider:
|
BoxCollider:
|
||||||
|
|
@ -394,12 +506,12 @@ Transform:
|
||||||
m_GameObject: {fileID: 330585543}
|
m_GameObject: {fileID: 330585543}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
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_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 1239994224}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 45, y: -90, z: 0}
|
||||||
--- !u!114 &330585547
|
--- !u!114 &330585547
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -620,7 +732,7 @@ Transform:
|
||||||
m_GameObject: {fileID: 441239879}
|
m_GameObject: {fileID: 441239879}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
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_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children:
|
m_Children:
|
||||||
|
|
@ -633,6 +745,118 @@ Transform:
|
||||||
- {fileID: 923592499}
|
- {fileID: 923592499}
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 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
|
--- !u!1 &832575517
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -746,8 +970,8 @@ BoxCollider:
|
||||||
m_ProvidesContacts: 0
|
m_ProvidesContacts: 0
|
||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_Size: {x: 70, y: 0.5, z: 39}
|
m_Size: {x: 32, y: 0.5, z: 87}
|
||||||
m_Center: {x: 0.5, y: 0, z: 19}
|
m_Center: {x: 14.5, y: 0, z: 41}
|
||||||
--- !u!1 &1064792475
|
--- !u!1 &1064792475
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -775,7 +999,7 @@ Transform:
|
||||||
m_GameObject: {fileID: 1064792475}
|
m_GameObject: {fileID: 1064792475}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
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_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
|
|
@ -816,8 +1040,8 @@ BoxCollider:
|
||||||
m_ProvidesContacts: 0
|
m_ProvidesContacts: 0
|
||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_Size: {x: 2, y: 1, z: 7}
|
m_Size: {x: 6, y: 1, z: 1}
|
||||||
m_Center: {x: 0, y: 0, z: 0}
|
m_Center: {x: 2, y: 0, z: 2}
|
||||||
--- !u!1 &1078485323
|
--- !u!1 &1078485323
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -845,7 +1069,7 @@ Transform:
|
||||||
m_GameObject: {fileID: 1078485323}
|
m_GameObject: {fileID: 1078485323}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
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_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
|
|
@ -865,7 +1089,7 @@ MonoBehaviour:
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Levels.SpawnerVolume
|
m_EditorClassIdentifier: Assembly-CSharp::TD.Levels.SpawnerVolume
|
||||||
owner: 2
|
owner: 2
|
||||||
spawnerIdInZone: 0
|
spawnerIdInZone: 0
|
||||||
spawnFacing: 1
|
spawnFacing: 3
|
||||||
placementValidity: 0
|
placementValidity: 0
|
||||||
--- !u!65 &1078485326
|
--- !u!65 &1078485326
|
||||||
BoxCollider:
|
BoxCollider:
|
||||||
|
|
@ -886,8 +1110,66 @@ BoxCollider:
|
||||||
m_ProvidesContacts: 0
|
m_ProvidesContacts: 0
|
||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_Size: {x: 7, y: 1, z: 10}
|
m_Size: {x: 7, y: 1, z: 6}
|
||||||
m_Center: {x: 0, y: 0, z: 0}
|
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
|
--- !u!1 &1360337262
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -915,7 +1197,7 @@ Transform:
|
||||||
m_GameObject: {fileID: 1360337262}
|
m_GameObject: {fileID: 1360337262}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
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_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
|
|
@ -953,8 +1235,8 @@ BoxCollider:
|
||||||
m_ProvidesContacts: 0
|
m_ProvidesContacts: 0
|
||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_Size: {x: 3, y: 1, z: 7}
|
m_Size: {x: 7, y: 1, z: 2}
|
||||||
m_Center: {x: 0, y: 0, z: 0}
|
m_Center: {x: 0, y: 0, z: 2.5}
|
||||||
--- !u!1 &1464027360
|
--- !u!1 &1464027360
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -967,7 +1249,7 @@ GameObject:
|
||||||
- component: {fileID: 1464027363}
|
- component: {fileID: 1464027363}
|
||||||
- component: {fileID: 1464027362}
|
- component: {fileID: 1464027362}
|
||||||
- component: {fileID: 1464027361}
|
- component: {fileID: 1464027361}
|
||||||
m_Layer: 0
|
m_Layer: 7
|
||||||
m_Name: Plane
|
m_Name: Plane
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
m_Icon: {fileID: 0}
|
m_Icon: {fileID: 0}
|
||||||
|
|
@ -1061,13 +1343,197 @@ Transform:
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 1464027360}
|
m_GameObject: {fileID: 1464027360}
|
||||||
serializedVersion: 2
|
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_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
m_LocalPosition: {x: 33.17, y: 0, z: 13.07}
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
m_LocalScale: {x: 7, y: 1, z: 3}
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 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
|
--- !u!1 &1682341399
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -1174,6 +1640,118 @@ Transform:
|
||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 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
|
--- !u!1 &1975687919
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -1201,7 +1779,7 @@ Transform:
|
||||||
m_GameObject: {fileID: 1975687919}
|
m_GameObject: {fileID: 1975687919}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
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_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
|
|
@ -1240,16 +1818,22 @@ BoxCollider:
|
||||||
m_ProvidesContacts: 0
|
m_ProvidesContacts: 0
|
||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_Size: {x: 28, y: 1, z: 7}
|
m_Size: {x: 19, y: 1, z: 34}
|
||||||
m_Center: {x: 0, y: 0, z: 0}
|
m_Center: {x: -10.5, y: 0, z: -13.5}
|
||||||
--- !u!1660057539 &9223372036854775807
|
--- !u!1660057539 &9223372036854775807
|
||||||
SceneRoots:
|
SceneRoots:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_Roots:
|
m_Roots:
|
||||||
- {fileID: 330585546}
|
|
||||||
- {fileID: 410087041}
|
- {fileID: 410087041}
|
||||||
- {fileID: 832575519}
|
- {fileID: 832575519}
|
||||||
- {fileID: 1682341402}
|
- {fileID: 1682341402}
|
||||||
- {fileID: 441239881}
|
- {fileID: 441239881}
|
||||||
- {fileID: 1464027364}
|
- {fileID: 1464027364}
|
||||||
- {fileID: 167151709}
|
- {fileID: 167151709}
|
||||||
|
- {fileID: 1507514109}
|
||||||
|
- {fileID: 1538763654}
|
||||||
|
- {fileID: 1597884409}
|
||||||
|
- {fileID: 1239994224}
|
||||||
|
- {fileID: 1789340191}
|
||||||
|
- {fileID: 213124040}
|
||||||
|
- {fileID: 720114043}
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:62cfc398409e4285b50681d79ae4767183db364b3f9feb9072ddf9dd60ec07d9
|
oid sha256:3c98285c3a823f2942e9dff02edf75918e7336b4f1496570f2e32a2ffc80e55e
|
||||||
size 12253
|
size 8745
|
||||||
|
|
|
||||||
121
Assets/_Project/Scripts/Gameplay/BuildRangeIndicator.cs
Normal file
121
Assets/_Project/Scripts/Gameplay/BuildRangeIndicator.cs
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
// Assets/_Project/Scripts/Gameplay/BuildRangeIndicator.cs
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Rendering.Universal;
|
||||||
|
|
||||||
|
namespace TD.Gameplay
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>Sits as a child of the <see cref="Builder"/> GameObject. The
|
||||||
|
/// <see cref="DecalProjector"/> renders a circular texture onto whatever ground
|
||||||
|
/// geometry is below — flat plane or sloped terrain alike, no special handling needed.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Owner-only.</b> 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.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Toggling.</b> The indicator is only visible when the local player is in
|
||||||
|
/// placement mode. It checks <c>TowerPlacementController.IsPlacing</c> each frame
|
||||||
|
/// and toggles the projector accordingly. When sized correctly the projector size
|
||||||
|
/// matches <c>buildRange * 2</c> (diameter).</para>
|
||||||
|
/// </remarks>
|
||||||
|
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<DecalProjector>();
|
||||||
|
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<Builder>();
|
||||||
|
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<TowerPlacementController>();
|
||||||
|
if (cachedPlacementController == null) return false;
|
||||||
|
}
|
||||||
|
return cachedPlacementController.IsPlacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a65b1797079cf2d4e9de7b82e81f2283
|
||||||
303
Assets/_Project/Scripts/Gameplay/Builder.cs
Normal file
303
Assets/_Project/Scripts/Gameplay/Builder.cs
Normal file
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Per-player avatar that gates tower placement by proximity. Server-authoritative
|
||||||
|
/// position; clients submit move requests via Rpc and the server validates and applies.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para><b>Pure visual avatar.</b> 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.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Terrain-aware height.</b> Each frame the server casts a ray straight down
|
||||||
|
/// from the builder against the <see cref="terrainLayerMask"/> and sets Y to
|
||||||
|
/// <c>hit.point.y + heightOffset</c>. If the ray misses, falls back to the buildable
|
||||||
|
/// plane Y. Towers are not on the terrain layer, so they don't influence height.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Range gating.</b> <see cref="IsTileWithinBuildRange"/> is the public query
|
||||||
|
/// that <c>TowerPlacementManager</c> uses to validate placement requests. Builder range
|
||||||
|
/// is measured center-of-builder to center-of-anchor-tile in world units.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Static registry.</b> Like <see cref="PlayerGoldManager"/>, builders register
|
||||||
|
/// themselves in a static dictionary keyed by <c>OwnerClientId</c> on spawn, so server
|
||||||
|
/// gameplay code (notably <c>TowerPlacementManager</c>) can find a player's builder without
|
||||||
|
/// scene traversal.</para>
|
||||||
|
/// </remarks>
|
||||||
|
[RequireComponent(typeof(NetworkObject))]
|
||||||
|
public class Builder : NetworkBehaviour
|
||||||
|
{
|
||||||
|
// ----- Static registry --------------------------------------------
|
||||||
|
|
||||||
|
private static readonly Dictionary<ulong, Builder> s_byClientId
|
||||||
|
= new Dictionary<ulong, Builder>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the Builder owned by the given client, or null if none is currently spawned.
|
||||||
|
/// Safe to call on server or client.
|
||||||
|
/// </summary>
|
||||||
|
public static Builder GetForClient(ulong clientId)
|
||||||
|
{
|
||||||
|
s_byClientId.TryGetValue(clientId, out var builder);
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convenience: the local client's own builder. Returns null on a dedicated server
|
||||||
|
/// or before the local player has spawned.
|
||||||
|
/// </summary>
|
||||||
|
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<Vector3> targetPosition = new NetworkVariable<Vector3>(
|
||||||
|
value: Vector3.zero,
|
||||||
|
readPerm: NetworkVariableReadPermission.Everyone,
|
||||||
|
writePerm: NetworkVariableWritePermission.Server);
|
||||||
|
|
||||||
|
// ----- Public accessors -------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>The builder's current world position (its actual transform position,
|
||||||
|
/// not the target).</summary>
|
||||||
|
public Vector3 CurrentPosition => transform.position;
|
||||||
|
|
||||||
|
/// <summary>The builder's target position. Server moves toward this each frame.</summary>
|
||||||
|
public Vector3 TargetPosition => targetPosition.Value;
|
||||||
|
|
||||||
|
/// <summary>True if the builder has arrived at its target (within
|
||||||
|
/// <see cref="arrivalThreshold"/>).</summary>
|
||||||
|
public bool IsAtTarget =>
|
||||||
|
Vector3.SqrMagnitude(transform.position - targetPosition.Value)
|
||||||
|
< arrivalThreshold * arrivalThreshold;
|
||||||
|
|
||||||
|
/// <summary>Build range in world units.</summary>
|
||||||
|
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<MeshRenderer>())
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Casts a ray straight down at <paramref name="xzPos"/> and returns the hit Y, or
|
||||||
|
/// <see cref="GridCoordinates.BUILDABLE_PLANE_Y"/> if nothing was hit on the
|
||||||
|
/// terrain layer.
|
||||||
|
/// </summary>
|
||||||
|
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 --------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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).
|
||||||
|
/// </summary>
|
||||||
|
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 ----------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owner-only Rpc: a client requests their builder move to a world position.
|
||||||
|
/// Server validates (in-map check) and applies via <see cref="ServerSetMoveTarget"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)]
|
||||||
|
public void RequestMoveRpc(Vector3 worldPos)
|
||||||
|
{
|
||||||
|
ServerSetMoveTarget(worldPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Range query (used by TowerPlacementManager) ----------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// "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."
|
||||||
|
/// </remarks>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Scripts/Gameplay/Builder.cs.meta
Normal file
2
Assets/_Project/Scripts/Gameplay/Builder.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 05b2c04367f8c864bb5e3e03ba42dde5
|
||||||
121
Assets/_Project/Scripts/Gameplay/BuilderInputController.cs
Normal file
121
Assets/_Project/Scripts/Gameplay/BuilderInputController.cs
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
// Assets/_Project/Scripts/Gameplay/BuilderInputController.cs
|
||||||
|
using Unity.Netcode;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.InputSystem;
|
||||||
|
using TD.Core;
|
||||||
|
|
||||||
|
namespace TD.Gameplay
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Owner-only client-side controller for builder input. Handles right-click-to-move,
|
||||||
|
/// deferring to placement mode (right-click cancels placement instead).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para><b>Owner-only.</b> This component lives on the same GameObject as
|
||||||
|
/// <see cref="Builder"/> 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
|
||||||
|
/// <see cref="Builder.RequestMoveRpc"/>.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Right-click priority.</b> If <c>TowerPlacementController.IsPlacing</c> is
|
||||||
|
/// true, right-click cancels placement (handled by <c>TowerPlacementController</c>
|
||||||
|
/// itself). When NOT placing, right-click moves the builder.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Raycast target.</b> 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.</para>
|
||||||
|
/// </remarks>
|
||||||
|
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<Builder>();
|
||||||
|
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<TowerPlacementController>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 85df68e96d71b3f4cb302a197a6a4e05
|
||||||
459
Assets/_Project/Scripts/Gameplay/CameraController.cs
Normal file
459
Assets/_Project/Scripts/Gameplay/CameraController.cs
Normal file
|
|
@ -0,0 +1,459 @@
|
||||||
|
// Assets/_Project/Scripts/Gameplay/CameraController.cs
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.InputSystem;
|
||||||
|
using TD.Core;
|
||||||
|
using TD.Levels;
|
||||||
|
|
||||||
|
namespace TD.Gameplay
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Per-client RTS-style camera controller. Lives on a "camera rig" GameObject (the pivot)
|
||||||
|
/// with a child <see cref="Camera"/> 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para><b>Rig math.</b> The pivot's world rotation is set to <c>(pitch, 0, 0)</c>. The
|
||||||
|
/// camera child sits at local position <c>(0, 0, -dolly)</c> 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).</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Inputs.</b>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><b>WASD or Arrow keys</b> — pan the pivot across the buildable plane.</item>
|
||||||
|
/// <item><b>Mouse near screen edge</b> — edge-pan, when enabled.</item>
|
||||||
|
/// <item><b>Scroll wheel</b> — dolly zoom (cursor-anchored).</item>
|
||||||
|
/// <item><b>Alt + Scroll wheel</b> — adjust pitch.</item>
|
||||||
|
/// </list></para>
|
||||||
|
///
|
||||||
|
/// <para><b>Bounds clamping.</b> The pivot's tile is required to satisfy
|
||||||
|
/// <see cref="LevelLoader.IsInMap"/>. 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.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Speed scaling.</b> Pan speed scales linearly with the current dolly distance, so
|
||||||
|
/// zoomed-out panning covers more ground per second and zoomed-in panning is precise.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Public API for minimap.</b> <see cref="JumpTo"/>, <see cref="BeginDrag"/>,
|
||||||
|
/// <see cref="UpdateDrag"/>, <see cref="EndDrag"/> exist so the minimap (when implemented)
|
||||||
|
/// can drive the camera without this controller knowing about it.</para>
|
||||||
|
/// </remarks>
|
||||||
|
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 -------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>Whether edge-panning is currently active. Wire to settings UI.</summary>
|
||||||
|
public bool EdgePanEnabled
|
||||||
|
{
|
||||||
|
get => edgePanEnabled;
|
||||||
|
set => edgePanEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Snaps the pivot to <paramref name="worldPos"/>. Y is ignored. Used by the
|
||||||
|
/// minimap for click-to-jump.</summary>
|
||||||
|
public void JumpTo(Vector3 worldPos)
|
||||||
|
{
|
||||||
|
Vector3 newPivot = new Vector3(worldPos.x, GridCoordinates.BUILDABLE_PLANE_Y, worldPos.z);
|
||||||
|
TryMovePivotTo(newPivot);
|
||||||
|
ApplyTransform();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Begins external drag mode (e.g., from the minimap). Subsequent
|
||||||
|
/// <see cref="UpdateDrag"/> calls drive the pivot directly, bypassing keyboard/edge
|
||||||
|
/// input until <see cref="EndDrag"/>.</summary>
|
||||||
|
public void BeginDrag() => isExternalDragActive = true;
|
||||||
|
|
||||||
|
/// <summary>Updates the pivot to the dragged world position. Same effect as
|
||||||
|
/// <see cref="JumpTo"/> but intended to be called every frame during a drag.</summary>
|
||||||
|
public void UpdateDrag(Vector3 worldPos) => JumpTo(worldPos);
|
||||||
|
|
||||||
|
/// <summary>Ends external drag mode. Normal input handling resumes.</summary>
|
||||||
|
public void EndDrag() => isExternalDragActive = false;
|
||||||
|
|
||||||
|
// ----- Lifecycle --------------------------------------------------
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
if (cameraChild == null) cameraChild = GetComponentInChildren<Camera>();
|
||||||
|
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 ------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to move the pivot to <paramref name="desired"/>. 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.
|
||||||
|
/// </summary>
|
||||||
|
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 --------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies <see cref="currentPitch"/> to the pivot rotation and
|
||||||
|
/// <see cref="currentDolly"/> to the camera's local Z offset. Yaw stays at 0.
|
||||||
|
/// </summary>
|
||||||
|
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 --------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Casts a ray from the camera through <paramref name="screenPos"/> against the
|
||||||
|
/// buildable plane (Y = <see cref="GridCoordinates.BUILDABLE_PLANE_Y"/>). Returns
|
||||||
|
/// false if the ray is parallel to the plane or the cursor is above the horizon.
|
||||||
|
/// </summary>
|
||||||
|
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 -------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the initial pivot position as the centroid of the local player's owned
|
||||||
|
/// tiles. Falls back to the map center, and finally to <paramref name="fallback"/>
|
||||||
|
/// (the rig's authoring-time position) if neither is available yet.
|
||||||
|
/// </summary>
|
||||||
|
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 ------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the local player's PlayerSlot.
|
||||||
|
/// STUB: same trivial mapping used elsewhere; replaced when MatchState lands.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1e670a36689801e428873c5712ea3679
|
||||||
|
|
@ -74,15 +74,30 @@ namespace TD.Gameplay
|
||||||
|
|
||||||
// The mutable walkability grid. Initialized from LevelData.WalkabilityGrid
|
// The mutable walkability grid. Initialized from LevelData.WalkabilityGrid
|
||||||
// and mutated by tower placement at runtime. Stays in lockstep with the
|
// and mutated by tower placement at runtime. Stays in lockstep with the
|
||||||
// baked grid until towers are placed (none yet, since tower placement
|
// baked grid until towers are placed.
|
||||||
// isn't implemented).
|
|
||||||
//
|
//
|
||||||
// This is intentionally NOT exposed through a property yet -- consumers
|
// Consumers query through IsWalkable(Vector2Int) and mutate through
|
||||||
// will query through IsWalkable(Vector2Int) instead, hiding the array
|
// SetWalkable(Vector2Int, bool), which hide the flat-array indexing.
|
||||||
// indexing. When tower placement needs to mutate it, we'll expose a
|
//
|
||||||
// SetWalkable method then. Easier to add than to take away.
|
// 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;
|
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.
|
// The buildable-plane GameObject we instantiated as our child.
|
||||||
// Cached for inspector debugging and future destruction.
|
// Cached for inspector debugging and future destruction.
|
||||||
private GameObject buildablePlaneGO;
|
private GameObject buildablePlaneGO;
|
||||||
|
|
@ -113,6 +128,7 @@ namespace TD.Gameplay
|
||||||
}
|
}
|
||||||
|
|
||||||
InitializeRuntimeWalkability();
|
InitializeRuntimeWalkability();
|
||||||
|
InitializeRuntimeOccupied();
|
||||||
SpawnBuildablePlane();
|
SpawnBuildablePlane();
|
||||||
|
|
||||||
IsLoaded = true;
|
IsLoaded = true;
|
||||||
|
|
@ -190,6 +206,13 @@ namespace TD.Gameplay
|
||||||
level.WalkabilityGrid, runtimeWalkability, level.WalkabilityGrid.Length);
|
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()
|
private void SpawnBuildablePlane()
|
||||||
{
|
{
|
||||||
// Compute the world-space center and size of the grid.
|
// Compute the world-space center and size of the grid.
|
||||||
|
|
@ -314,6 +337,59 @@ namespace TD.Gameplay
|
||||||
return level.OwnerGrid[idx];
|
return level.OwnerGrid[idx];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if <paramref name="tile"/> is currently occupied by a placed tower footprint.
|
||||||
|
/// Returns false for out-of-bounds tiles. Unlike <see cref="IsWalkable"/>, this grid
|
||||||
|
/// starts all-false and only becomes true when a tower is successfully placed.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The placement ghost uses this (alongside <see cref="GetPlacement"/> and
|
||||||
|
/// <see cref="GetOwner"/>) 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.
|
||||||
|
/// </remarks>
|
||||||
|
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.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the runtime walkability of <paramref name="tile"/>. Called by
|
||||||
|
/// <c>TowerPlacementManager</c> on the server when a tower is accepted (pass
|
||||||
|
/// <c>false</c>) and when a tower is sold/destroyed (pass <c>true</c>).
|
||||||
|
/// No-ops silently for out-of-bounds tiles.
|
||||||
|
/// </summary>
|
||||||
|
public void SetWalkable(Vector2Int tile, bool walkable)
|
||||||
|
{
|
||||||
|
if (!TryFlatIndex(tile, out int idx)) return;
|
||||||
|
runtimeWalkability[idx] = walkable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the runtime occupancy of <paramref name="tile"/>. Called alongside
|
||||||
|
/// <see cref="SetWalkable"/> — always update both grids together so they
|
||||||
|
/// stay in sync. Pass <c>true</c> when a tower is placed, <c>false</c> when
|
||||||
|
/// it is sold or destroyed.
|
||||||
|
/// No-ops silently for out-of-bounds tiles.
|
||||||
|
/// </summary>
|
||||||
|
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
|
// Translates world-tile coordinates to a flat-array index, returning
|
||||||
// false if the tile is out of bounds. Used by all query methods.
|
// false if the tile is out of bounds. Used by all query methods.
|
||||||
private bool TryFlatIndex(Vector2Int tile, out int idx)
|
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
|
// Gizmos run in both edit mode and play mode. In edit mode we use the
|
||||||
// baked walkability/owner grids from the LevelData asset directly,
|
// baked walkability/owner grids from the LevelData asset directly,
|
||||||
// because runtimeWalkability hasn't been initialized yet. In play mode
|
// because runtimeWalkability hasn't been initialized yet. In play mode
|
||||||
// we use runtimeWalkability so the visualization reflects any future
|
// we use runtimeWalkability so the visualization reflects tower stamps.
|
||||||
// tower stamps. Owner and placement grids are immutable, so we read
|
// Owner and placement grids are immutable, so we read them from the
|
||||||
// them from the asset in both modes.
|
// asset in both modes.
|
||||||
|
|
||||||
private void OnDrawGizmos()
|
private void OnDrawGizmos()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
141
Assets/_Project/Scripts/Gameplay/PlayerBuilderSpawner.cs
Normal file
141
Assets/_Project/Scripts/Gameplay/PlayerBuilderSpawner.cs
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
// Assets/_Project/Scripts/Gameplay/PlayerBuilderSpawner.cs
|
||||||
|
using Unity.Netcode;
|
||||||
|
using UnityEngine;
|
||||||
|
using TD.Core;
|
||||||
|
using TD.Levels;
|
||||||
|
|
||||||
|
namespace TD.Gameplay
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Lives on the Player Prefab. On the server, when the player NetworkObject spawns,
|
||||||
|
/// instantiates and spawns a separate <see cref="Builder"/> NetworkObject owned by that
|
||||||
|
/// player. The builder is positioned at the centroid of the player's zone before spawn.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para><b>Why a separate NetworkObject?</b> Multi-builder races (Path E) become "spawn
|
||||||
|
/// N builder NetworkObjects" without restructuring the Player Prefab. See the design
|
||||||
|
/// discussion in Path D scoping.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Server-only.</b> Spawning is server-authoritative. Non-server peers are
|
||||||
|
/// no-ops; they just receive the resulting Builder NetworkObject like any other
|
||||||
|
/// replicated spawn.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Lifetime.</b> The spawned builder is destroyed when the player NetworkObject
|
||||||
|
/// despawns (e.g., disconnect). NGO does this automatically because we set
|
||||||
|
/// <c>destroyWithScene</c> and store no other references — the builder's despawn cleans
|
||||||
|
/// up the static registry in <see cref="Builder.OnNetworkDespawn"/>.</para>
|
||||||
|
/// </remarks>
|
||||||
|
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<NetworkObject>();
|
||||||
|
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<Unity.Netcode.Components.NetworkTransform>();
|
||||||
|
if (netTransform != null)
|
||||||
|
{
|
||||||
|
netTransform.Teleport(spawnPos, Quaternion.identity, go.transform.localScale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Helpers ----------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stub mapping: client 0 = Player1, client 1 = Player2, etc.
|
||||||
|
/// Replaced by MatchState's authoritative assignment when that lands.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0f4c46f8263f72541b0f782b446de941
|
||||||
|
|
@ -143,6 +143,25 @@ namespace TD.Gameplay
|
||||||
currentGold.Value += amount;
|
currentGold.Value += amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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., <c>TowerPlacementManager</c> after
|
||||||
|
/// all checks have passed). Clamps to zero; gold cannot go negative.
|
||||||
|
/// </summary>
|
||||||
|
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 ---------------------------------------------
|
// --- Server-side Rpc ---------------------------------------------
|
||||||
|
|
||||||
// InvokePermission = Owner: only the client that owns this NetworkObject
|
// InvokePermission = Owner: only the client that owns this NetworkObject
|
||||||
|
|
@ -175,4 +194,4 @@ namespace TD.Gameplay
|
||||||
currentGold.Value -= amount;
|
currentGold.Value -= amount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
273
Assets/_Project/Scripts/Gameplay/TowerInstance.cs
Normal file
273
Assets/_Project/Scripts/Gameplay/TowerInstance.cs
Normal file
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Per-tower runtime component. Lives on the tower's NetworkObject prefab root.
|
||||||
|
///
|
||||||
|
/// Responsibilities:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>Hold the network-replicated identity of this tower: which
|
||||||
|
/// <see cref="TowerDefinition"/> it is and which <see cref="PlayerSlot"/> owns it.</item>
|
||||||
|
/// <item>On <see cref="OnNetworkSpawn"/>, stamp the tower's footprint into
|
||||||
|
/// <see cref="LevelLoader"/> on every client so local grids stay in sync
|
||||||
|
/// with the server-authoritative state.</item>
|
||||||
|
/// <item>Apply the owner's player color to the tower mesh, so towers are
|
||||||
|
/// visually distinct by zone during testing.</item>
|
||||||
|
/// </list>
|
||||||
|
/// </remarks>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para><b>Grid stamping split.</b> The server stamps the footprint in
|
||||||
|
/// <c>TowerPlacementManager.ProcessRequest</c> (before <c>NetworkObject.Spawn</c>)
|
||||||
|
/// so the path-validity check in the same frame sees the updated grid. Non-host
|
||||||
|
/// clients stamp in <see cref="OnNetworkSpawn"/> when NGO replicates the
|
||||||
|
/// NetworkObject to them. The server's <see cref="OnNetworkSpawn"/> also runs,
|
||||||
|
/// but by then the footprint is already stamped — <see cref="SetWalkable"/> and
|
||||||
|
/// <see cref="SetOccupied"/> are idempotent writes, so double-stamping is safe.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Definition reference replication.</b> TowerDefinition assets live in the
|
||||||
|
/// project on all clients. We replicate the asset by name via a
|
||||||
|
/// <see cref="NetworkVariable{T}"/> holding a <c>FixedString64Bytes</c>, then look
|
||||||
|
/// up the asset locally. This avoids serializing the full ScriptableObject over the
|
||||||
|
/// network. The lookup uses a <see cref="TowerRegistry"/> singleton that must be
|
||||||
|
/// present in the scene. (Temporary: will be driven by RaceDefinition in Path E.)</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Combat.</b> No combat logic here yet. Combat fields live stubbed on
|
||||||
|
/// <see cref="TowerDefinition"/>; they will be consumed by a future
|
||||||
|
/// <c>TowerCombat</c> component added to the same prefab.</para>
|
||||||
|
/// </remarks>
|
||||||
|
[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<FixedString64Bytes> definitionName =
|
||||||
|
new NetworkVariable<FixedString64Bytes>(
|
||||||
|
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<Vector2Int> anchorTile =
|
||||||
|
new NetworkVariable<Vector2Int>(
|
||||||
|
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<PlayerSlot> ownerSlot =
|
||||||
|
new NetworkVariable<PlayerSlot>(
|
||||||
|
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 -----------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>The TowerDefinition for this tower, resolved locally. Null until
|
||||||
|
/// <see cref="OnNetworkSpawn"/> runs and the definition lookup succeeds.</summary>
|
||||||
|
public TowerDefinition Definition => resolvedDefinition;
|
||||||
|
|
||||||
|
/// <summary>The PlayerSlot that placed this tower.</summary>
|
||||||
|
public PlayerSlot Owner => ownerSlot.Value;
|
||||||
|
|
||||||
|
/// <summary>The footprint anchor tile (SW corner, world-tile coords).</summary>
|
||||||
|
public Vector2Int AnchorTile => anchorTile.Value;
|
||||||
|
|
||||||
|
// ----- Server-only initialization -------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called by <c>TowerPlacementManager</c> on the server immediately after
|
||||||
|
/// instantiation and before <c>NetworkObject.Spawn</c>. Stores the data that
|
||||||
|
/// the server's <see cref="OnNetworkSpawn"/> will copy into the
|
||||||
|
/// NetworkVariables. NetworkVariables themselves are NOT written here —
|
||||||
|
/// see the comment on the pending-init fields above for why.
|
||||||
|
/// </summary>
|
||||||
|
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<MeshRenderer>();
|
||||||
|
foreach (var rend in renderers)
|
||||||
|
rend.SetPropertyBlock(colorPropertyBlock);
|
||||||
|
|
||||||
|
if (renderers.Length == 0)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[TowerInstance] NetworkObject {NetworkObjectId}: " +
|
||||||
|
$"No MeshRenderers found for owner color tinting.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Scripts/Gameplay/TowerInstance.cs.meta
Normal file
2
Assets/_Project/Scripts/Gameplay/TowerInstance.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: fb111fc88b3d6a340a3abde5a1502af3
|
||||||
421
Assets/_Project/Scripts/Gameplay/TowerPlacementController.cs
Normal file
421
Assets/_Project/Scripts/Gameplay/TowerPlacementController.cs
Normal file
|
|
@ -0,0 +1,421 @@
|
||||||
|
// Assets/_Project/Scripts/Gameplay/TowerPlacementController.cs
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.InputSystem;
|
||||||
|
using TD.Core;
|
||||||
|
using TD.Towers;
|
||||||
|
|
||||||
|
namespace TD.Gameplay
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Per-client controller for the tower placement UX. Handles hover raycasts against
|
||||||
|
/// the BuildablePlane collider, drives the placement ghost, and dispatches placement
|
||||||
|
/// requests to <see cref="TowerPlacementManager"/> via RPC.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para><b>Plain MonoBehaviour.</b> 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 <see cref="TowerPlacementManager"/>.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Ghost validity check.</b> 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.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Ghost colors.</b>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>White — all local checks pass.</item>
|
||||||
|
/// <item>Red — any local check fails (wrong zone, not buildable, already occupied).</item>
|
||||||
|
/// </list>
|
||||||
|
/// Green "pending construction" ghost is a separate system implemented in Path D.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Placement activation.</b> The controller is idle until
|
||||||
|
/// <see cref="BeginPlacement"/> is called (e.g., from a HUD tower button). The player
|
||||||
|
/// right-clicks or the placement is confirmed/rejected to return to idle.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Input System.</b> Uses the New Input System package. Mouse position and
|
||||||
|
/// button state are read from <c>Mouse.current</c> each frame.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Player slot.</b> The local player slot is currently a stub
|
||||||
|
/// (client 0 = Player1, etc.) matching <c>TowerPlacementManager.ClientIdToPlayerSlot</c>.
|
||||||
|
/// This will be replaced when MatchState carries the authoritative slot assignment.</para>
|
||||||
|
/// </remarks>
|
||||||
|
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<Renderer>();
|
||||||
|
|
||||||
|
// 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 -----------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired on the local client when the server rejects a placement request.
|
||||||
|
/// Payload is the human-readable rejection message from
|
||||||
|
/// <see cref="TowerPlacementSettings"/>. Subscribe here to display feedback UI.
|
||||||
|
/// </summary>
|
||||||
|
public static event System.Action<string> 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 -------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Activates placement mode for the given tower type. The ghost appears
|
||||||
|
/// immediately under the cursor. Call this from HUD tower buttons.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="def">The TowerDefinition to place.</param>
|
||||||
|
/// <param name="towerTypeId">The type ID registered in
|
||||||
|
/// <see cref="TowerPlacementManager"/>.</param>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cancels placement mode and destroys the ghost. Safe to call when idle.
|
||||||
|
/// </summary>
|
||||||
|
public void CancelPlacement()
|
||||||
|
{
|
||||||
|
activeDef = null;
|
||||||
|
activeTowerTypeId = 0;
|
||||||
|
lastAnchorValid = false;
|
||||||
|
|
||||||
|
DestroyGhost();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when placement mode is currently active.
|
||||||
|
/// </summary>
|
||||||
|
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<Renderer>();
|
||||||
|
|
||||||
|
// 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<Renderer>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetGhostVisible(bool visible)
|
||||||
|
{
|
||||||
|
if (ghostGO != null)
|
||||||
|
ghostGO.SetActive(visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disables components on the ghost that must not run: NetworkObject,
|
||||||
|
/// TowerInstance, Colliders, and Rigidbodies. The ghost is purely visual.
|
||||||
|
/// </summary>
|
||||||
|
private void DisableGhostComponents()
|
||||||
|
{
|
||||||
|
// NetworkObject — must be disabled so NGO doesn't try to register it.
|
||||||
|
var netObj = ghostGO.GetComponent<Unity.Netcode.NetworkObject>();
|
||||||
|
if (netObj != null) netObj.enabled = false;
|
||||||
|
|
||||||
|
// TowerInstance — must not stamp grids or fire OnNetworkSpawn.
|
||||||
|
var towerInstance = ghostGO.GetComponent<TowerInstance>();
|
||||||
|
if (towerInstance != null) towerInstance.enabled = false;
|
||||||
|
|
||||||
|
// Colliders — ghost must not block raycasts or physics queries.
|
||||||
|
foreach (var col in ghostGO.GetComponentsInChildren<Collider>())
|
||||||
|
col.enabled = false;
|
||||||
|
|
||||||
|
// Rigidbodies — ghost must not fall or interact with physics.
|
||||||
|
foreach (var rb in ghostGO.GetComponentsInChildren<Rigidbody>())
|
||||||
|
rb.isKinematic = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the ghost material color using <see cref="MaterialPropertyBlock"/>.
|
||||||
|
/// Switches between valid (white) and invalid (red) materials.
|
||||||
|
/// </summary>
|
||||||
|
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 -----------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 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))
|
||||||
|
/// </remarks>
|
||||||
|
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 ---------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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 ------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the local player's PlayerSlot.
|
||||||
|
/// STUB: Uses the same trivial client-ID → slot mapping as
|
||||||
|
/// <c>TowerPlacementManager.ClientIdToPlayerSlot</c>. Will be replaced
|
||||||
|
/// when MatchState carries the authoritative assignment.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 124253f2cbc2d9f46befb6e1763cd6b9
|
||||||
547
Assets/_Project/Scripts/Gameplay/TowerPlacementManager.cs
Normal file
547
Assets/_Project/Scripts/Gameplay/TowerPlacementManager.cs
Normal file
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para><b>Queue-based processing.</b> Incoming RPCs enqueue a
|
||||||
|
/// <see cref="PlacementRequest"/> rather than validating inline. Each server
|
||||||
|
/// Update drains up to <see cref="RequestsPerFrame"/> 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.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Server-only logic.</b> All validation and mutation runs on the server.
|
||||||
|
/// Clients learn about accepted placements when the <see cref="TowerInstance"/>
|
||||||
|
/// NetworkObject spawns (NGO replicates it automatically). Clients learn about
|
||||||
|
/// rejections via <see cref="PlacementRejectedRpc"/>.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Validation order:</b>
|
||||||
|
/// <list type="number">
|
||||||
|
/// <item>Ownership — every footprint tile must be owned by the requesting player.</item>
|
||||||
|
/// <item>Placement state — every footprint tile must be <c>Buildable</c> and unoccupied.</item>
|
||||||
|
/// <item>Gold — the placing player must have enough gold.</item>
|
||||||
|
/// <item>Path — a BFS confirms every spawner in the placing player's zone still reaches
|
||||||
|
/// an exit after the footprint is stamped as non-walkable.</item>
|
||||||
|
/// </list></para>
|
||||||
|
///
|
||||||
|
/// <para><b>Path-check BFS.</b> 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.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Builder range check.</b> 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.</para>
|
||||||
|
/// </remarks>
|
||||||
|
public class TowerPlacementManager : NetworkBehaviour
|
||||||
|
{
|
||||||
|
// ----- Singleton --------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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<PlacementRequest> pendingRequests = new Queue<PlacementRequest>();
|
||||||
|
|
||||||
|
// 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<Vector2Int> bfsQueue = new Queue<Vector2Int>();
|
||||||
|
private readonly HashSet<Vector2Int> bfsVisited = new HashSet<Vector2Int>();
|
||||||
|
|
||||||
|
// ----- 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) -------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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 <see cref="PlacementRejectedRpc"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="anchorX">X component of the footprint anchor tile (world-tile coords).</param>
|
||||||
|
/// <param name="anchorY">Y component of the footprint anchor tile (world-tile coords).</param>
|
||||||
|
/// <param name="towerTypeId">Index into the server's towerDefinitions array.</param>
|
||||||
|
[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) --------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired on the local client when the server rejects this client's placement request.
|
||||||
|
/// <see cref="TowerPlacementController"/> subscribes to this to display rejection
|
||||||
|
/// feedback messages.
|
||||||
|
/// </summary>
|
||||||
|
public static event System.Action<PlacementRejectionReason> 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<Vector2Int>(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 ------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true if every spawner in <paramref name="placingSlot"/>'s zone can still
|
||||||
|
/// reach an exit tile (any leak exit tile OR any goal tile) via walkable tiles,
|
||||||
|
/// given the current state of <c>LevelLoader</c>'s runtime walkability grid.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Mirrors bake-time P5-4. Runs a BFS per spawner against the runtime walkability
|
||||||
|
/// grid. Reuses <see cref="bfsQueue"/> and <see cref="bfsVisited"/> scratch
|
||||||
|
/// collections (cleared between BFS runs) to avoid GC allocation per call.
|
||||||
|
/// </remarks>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
private HashSet<Vector2Int> BuildExitTileSet(LevelData levelData, PlayerSlot slot)
|
||||||
|
{
|
||||||
|
var exits = new HashSet<Vector2Int>();
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// BFS from <paramref name="spawner"/>'s tile area. Returns true if any exit tile
|
||||||
|
/// is reachable via walkable tiles. Uses the shared scratch queue and visited set.
|
||||||
|
/// </summary>
|
||||||
|
private bool SpawnerCanReachExit(LevelLoader loader, SpawnerData spawner,
|
||||||
|
HashSet<Vector2Int> 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 ----------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stamps or un-stamps all tiles in <paramref name="footprint"/> on both the
|
||||||
|
/// walkability and occupancy grids simultaneously. Always update both together.
|
||||||
|
/// </summary>
|
||||||
|
private static void StampFootprint(LevelLoader loader, List<Vector2Int> footprint,
|
||||||
|
bool walkable, bool occupied)
|
||||||
|
{
|
||||||
|
foreach (var tile in footprint)
|
||||||
|
{
|
||||||
|
loader.SetWalkable(tile, walkable);
|
||||||
|
loader.SetOccupied(tile, occupied);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spawns the tower NetworkObject at the footprint center and records the
|
||||||
|
/// placing player's slot on the <see cref="TowerInstance"/> component.
|
||||||
|
/// </summary>
|
||||||
|
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<TowerInstance>();
|
||||||
|
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<NetworkObject>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps a client ID to the PlayerSlot assigned to that client.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 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.
|
||||||
|
/// </remarks>
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reason codes sent to the client when the server rejects a placement request.
|
||||||
|
/// Used by <see cref="TowerPlacementController"/> to display the appropriate
|
||||||
|
/// feedback message to the player.
|
||||||
|
/// </summary>
|
||||||
|
public enum PlacementRejectionReason
|
||||||
|
{
|
||||||
|
/// <summary>One or more footprint tiles belong to a different player's zone.</summary>
|
||||||
|
WrongOwner,
|
||||||
|
|
||||||
|
/// <summary>One or more footprint tiles are not in a Buildable state
|
||||||
|
/// (they are Restricted or Outside the map).</summary>
|
||||||
|
TileNotBuildable,
|
||||||
|
|
||||||
|
/// <summary>One or more footprint tiles are already occupied by an existing tower.</summary>
|
||||||
|
TileOccupied,
|
||||||
|
|
||||||
|
/// <summary>The placing player does not have enough gold.</summary>
|
||||||
|
InsufficientGold,
|
||||||
|
|
||||||
|
/// <summary>The placing player's builder is too far from the requested location.</summary>
|
||||||
|
OutOfRange,
|
||||||
|
|
||||||
|
/// <summary>Placing this tower would block all valid paths from at least one
|
||||||
|
/// spawner to its exit. The maze must remain passable.</summary>
|
||||||
|
BlocksPath,
|
||||||
|
|
||||||
|
/// <summary>The requested tower type ID is not in the server's definition list.</summary>
|
||||||
|
InvalidTowerType,
|
||||||
|
|
||||||
|
/// <summary>An unexpected server-side error occurred (e.g., LevelLoader not loaded,
|
||||||
|
/// client not mapped to a PlayerSlot). Check server logs.</summary>
|
||||||
|
ServerError,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d369496ba7887b844b1c220b524a507d
|
||||||
90
Assets/_Project/Scripts/Gameplay/TowerPlacementSettings.cs
Normal file
90
Assets/_Project/Scripts/Gameplay/TowerPlacementSettings.cs
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
// Assets/_Project/Scripts/Gameplay/TowerPlacementSettings.cs
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace TD.Gameplay
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Project-wide settings for the tower placement system. One asset shared across all
|
||||||
|
/// tower types — assign it to <see cref="TowerPlacementController"/> in the scene.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para><b>Why not on TowerDefinition?</b> 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.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Rejection messages.</b> These are the strings shown on screen when the
|
||||||
|
/// server rejects a placement. Kept here so designers can tune the wording without
|
||||||
|
/// touching code.</para>
|
||||||
|
/// </remarks>
|
||||||
|
[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 -----------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the human-readable rejection message for the given reason.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 141062cbf116c924e8b63dd458262795
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
// Assets/_Project/Scripts/Gameplay/TowerPlacementTestTrigger.cs
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.InputSystem;
|
||||||
|
using TD.Towers;
|
||||||
|
|
||||||
|
namespace TD.Gameplay
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ea0e3a4681be19e4e9c359c1123bf68d
|
||||||
131
Assets/_Project/Scripts/Gameplay/TowerRegistry.cs
Normal file
131
Assets/_Project/Scripts/Gameplay/TowerRegistry.cs
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
// Assets/_Project/Scripts/Gameplay/TowerRegistry.cs
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
using TD.Towers;
|
||||||
|
|
||||||
|
namespace TD.Gameplay
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Scene singleton that holds every <see cref="TowerDefinition"/> available in the
|
||||||
|
/// current match and lets any code look one up by asset name.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para><b>Why this exists.</b> <see cref="TowerInstance"/> replicates a tower's
|
||||||
|
/// definition by name (a <c>FixedString64Bytes</c> 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.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Auto-discovery.</b> On Awake, all <see cref="TowerDefinition"/> assets
|
||||||
|
/// under <c>Resources/TowerDefinitions/</c> 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.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Path E upgrade path.</b> In Path E the registry will filter to only the
|
||||||
|
/// definitions belonging to the active match's <c>RaceDefinition</c> rosters. For now
|
||||||
|
/// all assets in the Resources folder are registered.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Plain MonoBehaviour.</b> Not a NetworkBehaviour — the registry is
|
||||||
|
/// identical on every peer (same assets, same names), so there is nothing to sync.</para>
|
||||||
|
/// </remarks>
|
||||||
|
public class TowerRegistry : MonoBehaviour
|
||||||
|
{
|
||||||
|
// ----- Singleton --------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The active TowerRegistry. Null before Awake or after the scene unloads.
|
||||||
|
/// Always null-check before use.
|
||||||
|
/// </summary>
|
||||||
|
public static TowerRegistry Instance { get; private set; }
|
||||||
|
|
||||||
|
// ----- Constants --------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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/
|
||||||
|
/// </summary>
|
||||||
|
private const string ResourcesFolder = "TowerDefinitions";
|
||||||
|
|
||||||
|
// ----- Internal lookup table --------------------------------------
|
||||||
|
|
||||||
|
// Keyed by TowerDefinition.name (the asset name, not DisplayName).
|
||||||
|
private readonly Dictionary<string, TowerDefinition> byName
|
||||||
|
= new Dictionary<string, TowerDefinition>();
|
||||||
|
|
||||||
|
// ----- 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 -------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the <see cref="TowerDefinition"/> whose asset name equals
|
||||||
|
/// <paramref name="assetName"/>, or null if no match is found.
|
||||||
|
/// </summary>
|
||||||
|
public TowerDefinition Get(string assetName)
|
||||||
|
{
|
||||||
|
byName.TryGetValue(assetName, out var def);
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all registered tower definitions. Enumerates the internal
|
||||||
|
/// dictionary values — do not modify the returned collection.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<TowerDefinition> 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<TowerDefinition>(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}/.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Scripts/Gameplay/TowerRegistry.cs.meta
Normal file
2
Assets/_Project/Scripts/Gameplay/TowerRegistry.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a9dc0fbbe4422bc479ab8db7658c082b
|
||||||
8
Assets/_Project/Scripts/Towers.meta
Normal file
8
Assets/_Project/Scripts/Towers.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 905624ec4e5c8644492123ac8abf9c13
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
96
Assets/_Project/Scripts/Towers/TowerDefinition.cs
Normal file
96
Assets/_Project/Scripts/Towers/TowerDefinition.cs
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace TD.Towers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// TowerDefinitions are authored as ScriptableObject assets and referenced by
|
||||||
|
/// <see cref="RaceDefinition"/> (tower roster) and <see cref="TowerInstance"/> (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.
|
||||||
|
/// </remarks>
|
||||||
|
[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;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Scripts/Towers/TowerDefinition.cs.meta
Normal file
2
Assets/_Project/Scripts/Towers/TowerDefinition.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7b353a757b6e6774d97e6fb8ba138fcc
|
||||||
|
|
@ -13,32 +13,28 @@ MonoBehaviour:
|
||||||
m_Name: PC_Renderer
|
m_Name: PC_Renderer
|
||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
debugShaders:
|
debugShaders:
|
||||||
debugReplacementPS: {fileID: 4800000, guid: cf852408f2e174538bcd9b7fda1c5ae7,
|
debugReplacementPS: {fileID: 4800000, guid: cf852408f2e174538bcd9b7fda1c5ae7, type: 3}
|
||||||
type: 3}
|
|
||||||
hdrDebugViewPS: {fileID: 4800000, guid: 573620ae32aec764abd4d728906d2587, type: 3}
|
hdrDebugViewPS: {fileID: 4800000, guid: 573620ae32aec764abd4d728906d2587, type: 3}
|
||||||
probeVolumeSamplingDebugComputeShader: {fileID: 7200000, guid: 53626a513ea68ce47b59dc1299fe3959,
|
probeVolumeSamplingDebugComputeShader: {fileID: 7200000, guid: 53626a513ea68ce47b59dc1299fe3959, type: 3}
|
||||||
type: 3}
|
|
||||||
probeVolumeResources:
|
probeVolumeResources:
|
||||||
probeVolumeDebugShader: {fileID: 4800000, guid: e5c6678ed2aaa91408dd3df699057aae,
|
probeVolumeDebugShader: {fileID: 4800000, guid: e5c6678ed2aaa91408dd3df699057aae, type: 3}
|
||||||
type: 3}
|
probeVolumeFragmentationDebugShader: {fileID: 4800000, guid: 03cfc4915c15d504a9ed85ecc404e607, type: 3}
|
||||||
probeVolumeFragmentationDebugShader: {fileID: 4800000, guid: 03cfc4915c15d504a9ed85ecc404e607,
|
probeVolumeOffsetDebugShader: {fileID: 4800000, guid: 53a11f4ebaebf4049b3638ef78dc9664, type: 3}
|
||||||
type: 3}
|
probeVolumeSamplingDebugShader: {fileID: 4800000, guid: 8f96cd657dc40064aa21efcc7e50a2e7, type: 3}
|
||||||
probeVolumeOffsetDebugShader: {fileID: 4800000, guid: 53a11f4ebaebf4049b3638ef78dc9664,
|
probeSamplingDebugMesh: {fileID: -3555484719484374845, guid: 57d7c4c16e2765b47a4d2069b311bffe, type: 3}
|
||||||
type: 3}
|
probeSamplingDebugTexture: {fileID: 2800000, guid: 24ec0e140fb444a44ab96ee80844e18e, type: 3}
|
||||||
probeVolumeSamplingDebugShader: {fileID: 4800000, guid: 8f96cd657dc40064aa21efcc7e50a2e7,
|
probeVolumeBlendStatesCS: {fileID: 7200000, guid: b9a23f869c4fd45f19c5ada54dd82176, type: 3}
|
||||||
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:
|
m_RendererFeatures:
|
||||||
- {fileID: 7833122117494664109}
|
- {fileID: 7833122117494664109}
|
||||||
m_RendererFeatureMap: ad6b866f10d7b46c
|
- {fileID: 1099573597859666308}
|
||||||
|
m_RendererFeatureMap: ad6b866f10d7b46c84d9d2885c78420f
|
||||||
m_UseNativeRenderPass: 1
|
m_UseNativeRenderPass: 1
|
||||||
|
xrSystemData: {fileID: 0}
|
||||||
postProcessData: {fileID: 11400000, guid: 41439944d30ece34e96484bdb6645b55, type: 2}
|
postProcessData: {fileID: 11400000, guid: 41439944d30ece34e96484bdb6645b55, type: 2}
|
||||||
m_AssetVersion: 2
|
m_AssetVersion: 3
|
||||||
|
m_PrepassLayerMask:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 4294967295
|
||||||
m_OpaqueLayerMask:
|
m_OpaqueLayerMask:
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_Bits: 4294967295
|
m_Bits: 4294967295
|
||||||
|
|
@ -56,8 +52,31 @@ MonoBehaviour:
|
||||||
m_RenderingMode: 2
|
m_RenderingMode: 2
|
||||||
m_DepthPrimingMode: 0
|
m_DepthPrimingMode: 0
|
||||||
m_CopyDepthMode: 0
|
m_CopyDepthMode: 0
|
||||||
|
m_DepthAttachmentFormat: 0
|
||||||
|
m_DepthTextureFormat: 0
|
||||||
m_AccurateGbufferNormals: 0
|
m_AccurateGbufferNormals: 0
|
||||||
m_IntermediateTextureMode: 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
|
--- !u!114 &7833122117494664109
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -84,12 +103,3 @@ MonoBehaviour:
|
||||||
BlurQuality: 0
|
BlurQuality: 0
|
||||||
Falloff: 100
|
Falloff: 100
|
||||||
SampleCount: -1
|
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}
|
|
||||||
|
|
|
||||||
22
Assets/_Project/Settings/TowerPlacementSettings.asset
Normal file
22
Assets/_Project/Settings/TowerPlacementSettings.asset
Normal file
|
|
@ -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.
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 18f99b4b94d13eb429d4dfb9b0b37b4b
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -12,7 +12,7 @@ TagManager:
|
||||||
- Water
|
- Water
|
||||||
- UI
|
- UI
|
||||||
- BuildablePlane
|
- BuildablePlane
|
||||||
-
|
- TerrainGeometry
|
||||||
-
|
-
|
||||||
-
|
-
|
||||||
-
|
-
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,4 @@ MonoBehaviour:
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
m_LastMaterialVersion: 10
|
m_LastMaterialVersion: 10
|
||||||
|
m_ProjectSettingFolderPath: URPDefaultResources
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue