Adding a ton of funcitonality to the builder's movement and build queue
This commit is contained in:
parent
a63cce53e2
commit
f05734e19b
31 changed files with 3104 additions and 339 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -105,3 +105,5 @@ InitTestScene*.unity*
|
||||||
# Auto-generated cache in Assets folder
|
# Auto-generated cache in Assets folder
|
||||||
/[Aa]ssets/[Ss]ceneDependencyCache*
|
/[Aa]ssets/[Ss]ceneDependencyCache*
|
||||||
/Assets/_Recovery
|
/Assets/_Recovery
|
||||||
|
/Assets/Kevin Iglesias
|
||||||
|
Assets/Kevin Iglesias.meta
|
||||||
|
|
|
||||||
|
|
@ -29,3 +29,8 @@ MonoBehaviour:
|
||||||
SourcePrefabToOverride: {fileID: 0}
|
SourcePrefabToOverride: {fileID: 0}
|
||||||
SourceHashToOverride: 0
|
SourceHashToOverride: 0
|
||||||
OverridingTargetPrefab: {fileID: 0}
|
OverridingTargetPrefab: {fileID: 0}
|
||||||
|
- Override: 0
|
||||||
|
Prefab: {fileID: 7720770984308489338, guid: dff852699e2897b4494fcbc7f7e547d6, type: 3}
|
||||||
|
SourcePrefabToOverride: {fileID: 0}
|
||||||
|
SourceHashToOverride: 0
|
||||||
|
OverridingTargetPrefab: {fileID: 0}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ MonoBehaviour:
|
||||||
Description:
|
Description:
|
||||||
FootprintSize: {x: 2, y: 2}
|
FootprintSize: {x: 2, y: 2}
|
||||||
GoldCost: 25
|
GoldCost: 25
|
||||||
BuildTime: 0
|
BuildTime: 4
|
||||||
TowerPrefab: {fileID: 6482414459531823157, guid: 1511641f145758b469e64376d2a0d434, type: 3}
|
TowerPrefab: {fileID: 6482414459531823157, guid: 1511641f145758b469e64376d2a0d434, type: 3}
|
||||||
Damage: 0
|
Damage: 0
|
||||||
Range: 0
|
Range: 0
|
||||||
|
|
|
||||||
137
Assets/_Project/Art/Materials/M_BuildSite_Constructing.mat
Normal file
137
Assets/_Project/Art/Materials/M_BuildSite_Constructing.mat
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &-1223089589360192280
|
||||||
|
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_BuildSite_Constructing
|
||||||
|
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||||
|
m_Parent: {fileID: 0}
|
||||||
|
m_ModifiedSerializedProperties: 0
|
||||||
|
m_ValidKeywords: []
|
||||||
|
m_InvalidKeywords: []
|
||||||
|
m_LightmapFlags: 4
|
||||||
|
m_EnableInstancingVariants: 0
|
||||||
|
m_DoubleSidedGI: 0
|
||||||
|
m_CustomRenderQueue: 2000
|
||||||
|
stringTagMap:
|
||||||
|
RenderType: Opaque
|
||||||
|
disabledShaderPasses:
|
||||||
|
- MOTIONVECTORS
|
||||||
|
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: 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
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 88f1dd7b174716645953857b38fb6948
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 2100000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
141
Assets/_Project/Art/Materials/M_BuildSite_Queued.mat
Normal file
141
Assets/_Project/Art/Materials/M_BuildSite_Queued.mat
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &-1223089589360192280
|
||||||
|
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_BuildSite_Queued
|
||||||
|
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: 0.068549484, g: 1, b: 0, a: 0.19607843}
|
||||||
|
- _Color: {r: 0.06854946, g: 1, b: 0, a: 0.19607843}
|
||||||
|
- _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: f8d951a6841d3f74098bb31255379774
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 2100000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
141
Assets/_Project/Art/Materials/M_SelectionRing.mat
Normal file
141
Assets/_Project/Art/Materials/M_SelectionRing.mat
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &-5281183676241053304
|
||||||
|
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_SelectionRing
|
||||||
|
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: 0, g: 1, b: 0.026485443, a: 0.30588236}
|
||||||
|
- _Color: {r: 0, g: 1, b: 0.026485443, a: 0.30588236}
|
||||||
|
- _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_SelectionRing.mat.meta
Normal file
8
Assets/_Project/Art/Materials/M_SelectionRing.mat.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 81d0983426a4a31478788e89e22b0e80
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 2100000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -133,8 +133,8 @@ Material:
|
||||||
- _XRMotionVectorsPass: 1
|
- _XRMotionVectorsPass: 1
|
||||||
- _ZWrite: 0
|
- _ZWrite: 0
|
||||||
m_Colors:
|
m_Colors:
|
||||||
- _BaseColor: {r: 1, g: 0, b: 0, a: 0.47058824}
|
- _BaseColor: {r: 1, g: 0, b: 0, a: 0.19607843}
|
||||||
- _Color: {r: 1, g: 0, b: 0, a: 0.47058824}
|
- _Color: {r: 1, g: 0, b: 0, a: 0.19607843}
|
||||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||||
m_BuildTextureStacks: []
|
m_BuildTextureStacks: []
|
||||||
|
|
|
||||||
|
|
@ -133,8 +133,8 @@ Material:
|
||||||
- _XRMotionVectorsPass: 1
|
- _XRMotionVectorsPass: 1
|
||||||
- _ZWrite: 0
|
- _ZWrite: 0
|
||||||
m_Colors:
|
m_Colors:
|
||||||
- _BaseColor: {r: 1, g: 1, b: 1, a: 0.47058824}
|
- _BaseColor: {r: 1, g: 1, b: 1, a: 0.19607843}
|
||||||
- _Color: {r: 1, g: 1, b: 1, a: 0.47058824}
|
- _Color: {r: 1, g: 1, b: 1, a: 0.19607843}
|
||||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||||
m_BuildTextureStacks: []
|
m_BuildTextureStacks: []
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ Transform:
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children:
|
m_Children:
|
||||||
- {fileID: 2153758330548988791}
|
- {fileID: 2153758330548988791}
|
||||||
|
- {fileID: 5176306400449771234}
|
||||||
|
- {fileID: 6565619444702228235}
|
||||||
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!33 &1354786839850046103
|
--- !u!33 &1354786839850046103
|
||||||
|
|
@ -108,7 +110,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: 2050641840
|
GlobalObjectIdHash: 472470935
|
||||||
InScenePlacedSourceGlobalObjectIdHash: 0
|
InScenePlacedSourceGlobalObjectIdHash: 0
|
||||||
DeferredDespawnTick: 0
|
DeferredDespawnTick: 0
|
||||||
Ownership: 1
|
Ownership: 1
|
||||||
|
|
@ -182,12 +184,17 @@ MonoBehaviour:
|
||||||
ShowTopMostFoldoutHeaderGroup: 1
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
moveSpeed: 8
|
moveSpeed: 8
|
||||||
arrivalThreshold: 0.05
|
arrivalThreshold: 0.05
|
||||||
|
turnRateDegPerSec: 540
|
||||||
heightOffset: 2
|
heightOffset: 2
|
||||||
terrainRaycastMaxDistance: 100
|
terrainRaycastMaxDistance: 100
|
||||||
terrainLayerMask:
|
terrainLayerMask:
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_Bits: 128
|
m_Bits: 128
|
||||||
buildRange: 6
|
buildRange: 6
|
||||||
|
maxQueueDepth: 32
|
||||||
|
buildSiteVisualPrefab: {fileID: 7720770984308489338, guid: dff852699e2897b4494fcbc7f7e547d6, type: 3}
|
||||||
|
tintedRenderers:
|
||||||
|
- {fileID: 4167417797825706430}
|
||||||
--- !u!114 &4533726421250799861
|
--- !u!114 &4533726421250799861
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -204,6 +211,12 @@ MonoBehaviour:
|
||||||
buildablePlaneLayerMask:
|
buildablePlaneLayerMask:
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_Bits: 64
|
m_Bits: 64
|
||||||
|
selectionLayerMask:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 256
|
||||||
|
buildSiteLayerMask:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 512
|
||||||
raycastMaxDistance: 500
|
raycastMaxDistance: 500
|
||||||
--- !u!114 &6467759961575585905
|
--- !u!114 &6467759961575585905
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
|
|
@ -219,6 +232,109 @@ MonoBehaviour:
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.BuildRangeIndicator
|
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.BuildRangeIndicator
|
||||||
projector: {fileID: 2082893476690950776}
|
projector: {fileID: 2082893476690950776}
|
||||||
projectionDepth: 50
|
projectionDepth: 50
|
||||||
|
--- !u!1 &2558028744543194000
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 6565619444702228235}
|
||||||
|
- component: {fileID: 1724910192658818315}
|
||||||
|
- component: {fileID: 6010362400907743827}
|
||||||
|
- component: {fileID: 6997342110466460015}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: SelectionRing
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!4 &6565619444702228235
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 2558028744543194000}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 0, y: 0.05, z: 0}
|
||||||
|
m_LocalScale: {x: 2, y: 0.02, z: 2}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 5490805221566030526}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!33 &1724910192658818315
|
||||||
|
MeshFilter:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 2558028744543194000}
|
||||||
|
m_Mesh: {fileID: 10206, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
|
--- !u!23 &6010362400907743827
|
||||||
|
MeshRenderer:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 2558028744543194000}
|
||||||
|
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: 81d0983426a4a31478788e89e22b0e80, 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 &6997342110466460015
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 2558028744543194000}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 67895f626233fdc499dffbbfcc225530, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.SelectionRingVisual
|
||||||
--- !u!1 &4357234114074764669
|
--- !u!1 &4357234114074764669
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -280,3 +396,56 @@ MonoBehaviour:
|
||||||
m_VisibleInScene: 1
|
m_VisibleInScene: 1
|
||||||
version: 1
|
version: 1
|
||||||
m_DecalLayerMask: 1
|
m_DecalLayerMask: 1
|
||||||
|
--- !u!1 &6563645777727655090
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 5176306400449771234}
|
||||||
|
- component: {fileID: 5557638594792396607}
|
||||||
|
m_Layer: 8
|
||||||
|
m_Name: SelectionTrigger
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!4 &5176306400449771234
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 6563645777727655090}
|
||||||
|
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!65 &5557638594792396607
|
||||||
|
BoxCollider:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 6563645777727655090}
|
||||||
|
m_Material: {fileID: 0}
|
||||||
|
m_IncludeLayers:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 0
|
||||||
|
m_ExcludeLayers:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 0
|
||||||
|
m_LayerOverridePriority: 0
|
||||||
|
m_IsTrigger: 1
|
||||||
|
m_ProvidesContacts: 0
|
||||||
|
m_Enabled: 1
|
||||||
|
serializedVersion: 3
|
||||||
|
m_Size: {x: 1, y: 2, z: 1}
|
||||||
|
m_Center: {x: 0, y: 0, z: 0}
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ MonoBehaviour:
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.PlayerGoldManager
|
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.PlayerGoldManager
|
||||||
ShowTopMostFoldoutHeaderGroup: 1
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
startingGold: 100
|
startingGold: 1000
|
||||||
--- !u!114 &7845089877743661692
|
--- !u!114 &7845089877743661692
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
|
||||||
247
Assets/_Project/Prefabs/Towers/BuildSiteVisual.prefab
Normal file
247
Assets/_Project/Prefabs/Towers/BuildSiteVisual.prefab
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!1 &2381228775386571742
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 2082013748313019226}
|
||||||
|
- component: {fileID: 7119837420184958461}
|
||||||
|
- component: {fileID: 8833246162440246558}
|
||||||
|
- component: {fileID: 1909402251021719060}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: Scale Target
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!4 &2082013748313019226
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 2381228775386571742}
|
||||||
|
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: 1531733800731892084}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!33 &7119837420184958461
|
||||||
|
MeshFilter:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 2381228775386571742}
|
||||||
|
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
|
--- !u!23 &8833246162440246558
|
||||||
|
MeshRenderer:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 2381228775386571742}
|
||||||
|
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 &1909402251021719060
|
||||||
|
BoxCollider:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 2381228775386571742}
|
||||||
|
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!1 &5570322131082283203
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 1059634071631389929}
|
||||||
|
- component: {fileID: 1627450194292928283}
|
||||||
|
m_Layer: 9
|
||||||
|
m_Name: ClickTarget
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!4 &1059634071631389929
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 5570322131082283203}
|
||||||
|
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: 1531733800731892084}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!65 &1627450194292928283
|
||||||
|
BoxCollider:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 5570322131082283203}
|
||||||
|
m_Material: {fileID: 0}
|
||||||
|
m_IncludeLayers:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 0
|
||||||
|
m_ExcludeLayers:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 0
|
||||||
|
m_LayerOverridePriority: 0
|
||||||
|
m_IsTrigger: 1
|
||||||
|
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!1 &7720770984308489338
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 1531733800731892084}
|
||||||
|
- component: {fileID: 9075933591925717035}
|
||||||
|
- component: {fileID: 7845454079079718139}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: BuildSiteVisual
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!4 &1531733800731892084
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 7720770984308489338}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 10.09691, y: 0.5, z: 20.24222}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children:
|
||||||
|
- {fileID: 2082013748313019226}
|
||||||
|
- {fileID: 1059634071631389929}
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!114 &9075933591925717035
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 7720770984308489338}
|
||||||
|
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: 3616792119
|
||||||
|
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 &7845454079079718139
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 7720770984308489338}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: a236b3b34c8dd784db3bed4e6b0f44f9, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.BuildSiteVisual
|
||||||
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
|
tintedRenderers: []
|
||||||
|
scaleTarget: {fileID: 2082013748313019226}
|
||||||
|
queuedMaterial: {fileID: 2100000, guid: f8d951a6841d3f74098bb31255379774, type: 2}
|
||||||
|
constructingMaterial: {fileID: 2100000, guid: 88f1dd7b174716645953857b38fb6948, type: 2}
|
||||||
|
pausedMaterial: {fileID: 0}
|
||||||
|
stageCount: 4
|
||||||
|
queuedYScale: 0.15
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: dff852699e2897b4494fcbc7f7e547d6
|
||||||
|
PrefabImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -237,118 +237,6 @@ 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 &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
|
||||||
|
|
@ -745,7 +633,7 @@ 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
|
--- !u!1 &611926972
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
|
@ -753,24 +641,24 @@ GameObject:
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
serializedVersion: 6
|
serializedVersion: 6
|
||||||
m_Component:
|
m_Component:
|
||||||
- component: {fileID: 720114043}
|
- component: {fileID: 611926976}
|
||||||
- component: {fileID: 720114042}
|
- component: {fileID: 611926975}
|
||||||
- component: {fileID: 720114041}
|
- component: {fileID: 611926974}
|
||||||
- component: {fileID: 720114040}
|
- component: {fileID: 611926973}
|
||||||
m_Layer: 7
|
m_Layer: 7
|
||||||
m_Name: Cube (2)
|
m_Name: Cube (5)
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
m_Icon: {fileID: 0}
|
m_Icon: {fileID: 0}
|
||||||
m_NavMeshLayer: 0
|
m_NavMeshLayer: 0
|
||||||
m_StaticEditorFlags: 0
|
m_StaticEditorFlags: 0
|
||||||
m_IsActive: 1
|
m_IsActive: 1
|
||||||
--- !u!65 &720114040
|
--- !u!65 &611926973
|
||||||
BoxCollider:
|
BoxCollider:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 720114039}
|
m_GameObject: {fileID: 611926972}
|
||||||
m_Material: {fileID: 0}
|
m_Material: {fileID: 0}
|
||||||
m_IncludeLayers:
|
m_IncludeLayers:
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
|
|
@ -785,13 +673,13 @@ BoxCollider:
|
||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_Size: {x: 1, y: 1, z: 1}
|
m_Size: {x: 1, y: 1, z: 1}
|
||||||
m_Center: {x: 0, y: 0, z: 0}
|
m_Center: {x: 0, y: 0, z: 0}
|
||||||
--- !u!23 &720114041
|
--- !u!23 &611926974
|
||||||
MeshRenderer:
|
MeshRenderer:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 720114039}
|
m_GameObject: {fileID: 611926972}
|
||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
m_CastShadows: 1
|
m_CastShadows: 1
|
||||||
m_ReceiveShadows: 1
|
m_ReceiveShadows: 1
|
||||||
|
|
@ -834,29 +722,29 @@ MeshRenderer:
|
||||||
m_SortingOrder: 0
|
m_SortingOrder: 0
|
||||||
m_MaskInteraction: 0
|
m_MaskInteraction: 0
|
||||||
m_AdditionalVertexStreams: {fileID: 0}
|
m_AdditionalVertexStreams: {fileID: 0}
|
||||||
--- !u!33 &720114042
|
--- !u!33 &611926975
|
||||||
MeshFilter:
|
MeshFilter:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 720114039}
|
m_GameObject: {fileID: 611926972}
|
||||||
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
|
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
--- !u!4 &720114043
|
--- !u!4 &611926976
|
||||||
Transform:
|
Transform:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 720114039}
|
m_GameObject: {fileID: 611926972}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
m_LocalRotation: {x: -0, y: 0.70710576, z: -0, w: 0.70710784}
|
||||||
m_LocalPosition: {x: 14, y: 3, z: 14}
|
m_LocalPosition: {x: 39, y: 2, z: 40}
|
||||||
m_LocalScale: {x: 2, y: 1, z: 2}
|
m_LocalScale: {x: 100, y: 5, z: 20}
|
||||||
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: 90, z: 0}
|
||||||
--- !u!1 &832575517
|
--- !u!1 &832575517
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -1112,6 +1000,50 @@ BoxCollider:
|
||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_Size: {x: 7, y: 1, z: 6}
|
m_Size: {x: 7, y: 1, z: 6}
|
||||||
m_Center: {x: 0, y: 0, z: 2}
|
m_Center: {x: 0, y: 0, z: 2}
|
||||||
|
--- !u!1 &1222526236
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 1222526238}
|
||||||
|
- component: {fileID: 1222526237}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: SelectionState
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!114 &1222526237
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1222526236}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: dc307e7e94967894584e8e6050fc38cf, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.SelectionState
|
||||||
|
--- !u!4 &1222526238
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1222526236}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 11.62873, y: 0.5, z: 54.78663}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!1 &1239994222
|
--- !u!1 &1239994222
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -1640,7 +1572,7 @@ 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
|
--- !u!1 &1949204941
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
|
@ -1648,24 +1580,24 @@ GameObject:
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
serializedVersion: 6
|
serializedVersion: 6
|
||||||
m_Component:
|
m_Component:
|
||||||
- component: {fileID: 1789340191}
|
- component: {fileID: 1949204945}
|
||||||
- component: {fileID: 1789340190}
|
- component: {fileID: 1949204944}
|
||||||
- component: {fileID: 1789340189}
|
- component: {fileID: 1949204943}
|
||||||
- component: {fileID: 1789340188}
|
- component: {fileID: 1949204942}
|
||||||
m_Layer: 7
|
m_Layer: 7
|
||||||
m_Name: Cube
|
m_Name: Cube (4)
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
m_Icon: {fileID: 0}
|
m_Icon: {fileID: 0}
|
||||||
m_NavMeshLayer: 0
|
m_NavMeshLayer: 0
|
||||||
m_StaticEditorFlags: 0
|
m_StaticEditorFlags: 0
|
||||||
m_IsActive: 1
|
m_IsActive: 1
|
||||||
--- !u!65 &1789340188
|
--- !u!65 &1949204942
|
||||||
BoxCollider:
|
BoxCollider:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 1789340187}
|
m_GameObject: {fileID: 1949204941}
|
||||||
m_Material: {fileID: 0}
|
m_Material: {fileID: 0}
|
||||||
m_IncludeLayers:
|
m_IncludeLayers:
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
|
|
@ -1680,13 +1612,13 @@ BoxCollider:
|
||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_Size: {x: 1, y: 1, z: 1}
|
m_Size: {x: 1, y: 1, z: 1}
|
||||||
m_Center: {x: 0, y: 0, z: 0}
|
m_Center: {x: 0, y: 0, z: 0}
|
||||||
--- !u!23 &1789340189
|
--- !u!23 &1949204943
|
||||||
MeshRenderer:
|
MeshRenderer:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 1789340187}
|
m_GameObject: {fileID: 1949204941}
|
||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
m_CastShadows: 1
|
m_CastShadows: 1
|
||||||
m_ReceiveShadows: 1
|
m_ReceiveShadows: 1
|
||||||
|
|
@ -1729,29 +1661,29 @@ MeshRenderer:
|
||||||
m_SortingOrder: 0
|
m_SortingOrder: 0
|
||||||
m_MaskInteraction: 0
|
m_MaskInteraction: 0
|
||||||
m_AdditionalVertexStreams: {fileID: 0}
|
m_AdditionalVertexStreams: {fileID: 0}
|
||||||
--- !u!33 &1789340190
|
--- !u!33 &1949204944
|
||||||
MeshFilter:
|
MeshFilter:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 1789340187}
|
m_GameObject: {fileID: 1949204941}
|
||||||
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
|
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
--- !u!4 &1789340191
|
--- !u!4 &1949204945
|
||||||
Transform:
|
Transform:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 1789340187}
|
m_GameObject: {fileID: 1949204941}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
m_LocalRotation: {x: -0, y: 0.70710576, z: -0, w: 0.70710784}
|
||||||
m_LocalPosition: {x: 9, y: 0.5, z: 9}
|
m_LocalPosition: {x: -11, y: 2, z: 40}
|
||||||
m_LocalScale: {x: 2, y: 1, z: 2}
|
m_LocalScale: {x: 100, y: 5, z: 20}
|
||||||
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: 90, z: 0}
|
||||||
--- !u!1 &1975687919
|
--- !u!1 &1975687919
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -1820,6 +1752,118 @@ BoxCollider:
|
||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_Size: {x: 19, y: 1, z: 34}
|
m_Size: {x: 19, y: 1, z: 34}
|
||||||
m_Center: {x: -10.5, y: 0, z: -13.5}
|
m_Center: {x: -10.5, y: 0, z: -13.5}
|
||||||
|
--- !u!1 &2024858685
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 2024858689}
|
||||||
|
- component: {fileID: 2024858688}
|
||||||
|
- component: {fileID: 2024858687}
|
||||||
|
- component: {fileID: 2024858686}
|
||||||
|
m_Layer: 7
|
||||||
|
m_Name: Cube (3)
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!65 &2024858686
|
||||||
|
BoxCollider:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 2024858685}
|
||||||
|
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 &2024858687
|
||||||
|
MeshRenderer:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 2024858685}
|
||||||
|
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 &2024858688
|
||||||
|
MeshFilter:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 2024858685}
|
||||||
|
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
|
--- !u!4 &2024858689
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 2024858685}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 14, y: 2, z: 89}
|
||||||
|
m_LocalScale: {x: 50, y: 5, z: 5}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!1660057539 &9223372036854775807
|
--- !u!1660057539 &9223372036854775807
|
||||||
SceneRoots:
|
SceneRoots:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -1834,6 +1878,7 @@ SceneRoots:
|
||||||
- {fileID: 1538763654}
|
- {fileID: 1538763654}
|
||||||
- {fileID: 1597884409}
|
- {fileID: 1597884409}
|
||||||
- {fileID: 1239994224}
|
- {fileID: 1239994224}
|
||||||
- {fileID: 1789340191}
|
- {fileID: 2024858689}
|
||||||
- {fileID: 213124040}
|
- {fileID: 1949204945}
|
||||||
- {fileID: 720114043}
|
- {fileID: 611926976}
|
||||||
|
- {fileID: 1222526238}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ MonoBehaviour:
|
||||||
MapThumbnail: {fileID: 21300000, guid: d2e652d3e1c53454d80d3c1ec7888998, type: 3}
|
MapThumbnail: {fileID: 21300000, guid: d2e652d3e1c53454d80d3c1ec7888998, type: 3}
|
||||||
ScenePath: Assets/_Project/Scenes/Levels/Main.unity
|
ScenePath: Assets/_Project/Scenes/Levels/Main.unity
|
||||||
AuthoringHash: 18f981c8a12a79f122c2dad6fb2dab16c7921e01c9cd7bb6aed99d09d60ad2ac
|
AuthoringHash: 18f981c8a12a79f122c2dad6fb2dab16c7921e01c9cd7bb6aed99d09d60ad2ac
|
||||||
LastBakeTimestamp: 2026-05-03T21:39:37.7056732Z
|
LastBakeTimestamp: 2026-05-06T02:41:47.3544992Z
|
||||||
LastBakeOutcome: 1
|
LastBakeOutcome: 1
|
||||||
LastBakeWarningCount: 2
|
LastBakeWarningCount: 2
|
||||||
GridOriginTile: {x: 0, y: 0}
|
GridOriginTile: {x: 0, y: 0}
|
||||||
|
|
|
||||||
182
Assets/_Project/Scripts/Gameplay/BuildJob.cs
Normal file
182
Assets/_Project/Scripts/Gameplay/BuildJob.cs
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
// Assets/_Project/Scripts/Gameplay/BuildJob.cs
|
||||||
|
using System;
|
||||||
|
using Unity.Netcode;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace TD.Gameplay
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// One entry in a <see cref="Builder"/>'s build queue. Replicated as part of a
|
||||||
|
/// <see cref="NetworkList{T}"/> on the Builder so all clients can render queued
|
||||||
|
/// ghosts and progress without per-job RPCs.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para><b>Identity.</b> <see cref="JobId"/> is a server-assigned, monotonically
|
||||||
|
/// increasing identifier so cancel/lookup operations can target a specific job
|
||||||
|
/// regardless of its current index in the NetworkList. Index-based addressing
|
||||||
|
/// breaks the moment any job is removed.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Stage transitions.</b> Jobs progress
|
||||||
|
/// <see cref="BuildStage.Queued"/> → <see cref="BuildStage.Constructing"/> →
|
||||||
|
/// (removed when complete). Only the head of the queue can transition to
|
||||||
|
/// Constructing; tail jobs stay Queued until they reach the head.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Time fields.</b> <see cref="ConstructionStartServerTime"/> is set on
|
||||||
|
/// the server using <c>NetworkManager.ServerTime.TimeAsFloat</c> at the moment
|
||||||
|
/// construction begins. Clients compute current stage as
|
||||||
|
/// <c>floor(elapsed / (BuildTime / 4))</c> against the same server time, so all
|
||||||
|
/// peers see identical staging without per-stage RPC chatter.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>INetworkSerializable.</b> Required for use in
|
||||||
|
/// <see cref="NetworkList{T}"/>. Serializes only the minimum needed fields;
|
||||||
|
/// derived data (footprint size, gold cost) is looked up from the
|
||||||
|
/// TowerDefinition by <see cref="TowerTypeId"/> on each peer.</para>
|
||||||
|
/// </remarks>
|
||||||
|
[Serializable]
|
||||||
|
public struct BuildJob : INetworkSerializable, IEquatable<BuildJob>
|
||||||
|
{
|
||||||
|
// ----- Persistent fields ------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>Server-assigned unique ID. Stable across NetworkList reorderings.</summary>
|
||||||
|
public ulong JobId;
|
||||||
|
|
||||||
|
/// <summary>Footprint anchor (SW corner, world-tile coords).</summary>
|
||||||
|
public Vector2Int Anchor;
|
||||||
|
|
||||||
|
/// <summary>Index into <c>TowerPlacementManager.towerDefinitions[]</c>.</summary>
|
||||||
|
public int TowerTypeId;
|
||||||
|
|
||||||
|
/// <summary>Current stage. See <see cref="BuildStage"/>.</summary>
|
||||||
|
public BuildStage Stage;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server time (seconds) at which the current Constructing run began.
|
||||||
|
/// -1 while the job is Queued or Paused (no active timer running).
|
||||||
|
/// Read by clients to compute the current visual stage locally as
|
||||||
|
/// <c>(now - ConstructionStartServerTime) + AccumulatedConstructionTime</c>.
|
||||||
|
/// </summary>
|
||||||
|
public float ConstructionStartServerTime;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Construction time accumulated across previous Constructing runs.
|
||||||
|
/// Used to preserve progress across pause/resume cycles.
|
||||||
|
/// At pause, set to <c>elapsed_in_current_run + previous_accumulated</c>.
|
||||||
|
/// At resume, <c>ConstructionStartServerTime</c> is reset to "now" and
|
||||||
|
/// total progress is computed as
|
||||||
|
/// <c>(now - ConstructionStartServerTime) + AccumulatedConstructionTime</c>.
|
||||||
|
/// 0 for jobs that have never been paused.
|
||||||
|
/// </summary>
|
||||||
|
public float AccumulatedConstructionTime;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gold the player paid when this job was queued. Used to refund on
|
||||||
|
/// cancellation. Stored on the job (not looked up from the definition)
|
||||||
|
/// so that a future "tower price change mid-match" mechanic refunds
|
||||||
|
/// what was actually paid.
|
||||||
|
/// </summary>
|
||||||
|
public int GoldSpent;
|
||||||
|
|
||||||
|
// ----- Convenience constructors -----------------------------------
|
||||||
|
|
||||||
|
public static BuildJob CreateQueued(ulong jobId, Vector2Int anchor, int towerTypeId, int goldSpent)
|
||||||
|
{
|
||||||
|
return new BuildJob
|
||||||
|
{
|
||||||
|
JobId = jobId,
|
||||||
|
Anchor = anchor,
|
||||||
|
TowerTypeId = towerTypeId,
|
||||||
|
Stage = BuildStage.Queued,
|
||||||
|
ConstructionStartServerTime = -1f,
|
||||||
|
AccumulatedConstructionTime = 0f,
|
||||||
|
GoldSpent = goldSpent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- INetworkSerializable ---------------------------------------
|
||||||
|
|
||||||
|
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
|
||||||
|
{
|
||||||
|
serializer.SerializeValue(ref JobId);
|
||||||
|
serializer.SerializeValue(ref Anchor);
|
||||||
|
serializer.SerializeValue(ref TowerTypeId);
|
||||||
|
|
||||||
|
// BuildStage is a byte-backed enum; serialize as byte for forward-compat
|
||||||
|
// and to avoid relying on default enum serialization assumptions.
|
||||||
|
byte stageByte = (byte)Stage;
|
||||||
|
serializer.SerializeValue(ref stageByte);
|
||||||
|
Stage = (BuildStage)stageByte;
|
||||||
|
|
||||||
|
serializer.SerializeValue(ref ConstructionStartServerTime);
|
||||||
|
serializer.SerializeValue(ref AccumulatedConstructionTime);
|
||||||
|
serializer.SerializeValue(ref GoldSpent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- IEquatable -------------------------------------------------
|
||||||
|
//
|
||||||
|
// NetworkList<T> requires IEquatable<T> in NGO 2.x. Critically, NGO's
|
||||||
|
// indexer setter (jobs[i] = newValue) short-circuits the write when
|
||||||
|
// Equals returns true — see NGO 2.7.0 changelog. If we only compared
|
||||||
|
// by JobId, mutating Stage or ConstructionStartServerTime on a job and
|
||||||
|
// writing it back would be silently dropped, leaving the list with the
|
||||||
|
// old struct values forever. Compare every field that we ever mutate
|
||||||
|
// after creation.
|
||||||
|
//
|
||||||
|
// GetHashCode uses JobId only because that's the unique identity and is
|
||||||
|
// sufficient for hashing — the full-field comparison only happens on
|
||||||
|
// hash collision (or where IEquatable bypasses the hash entirely,
|
||||||
|
// which is the case in NetworkList's indexer setter).
|
||||||
|
|
||||||
|
public bool Equals(BuildJob other)
|
||||||
|
{
|
||||||
|
return JobId == other.JobId
|
||||||
|
&& Anchor == other.Anchor
|
||||||
|
&& TowerTypeId == other.TowerTypeId
|
||||||
|
&& Stage == other.Stage
|
||||||
|
&& ConstructionStartServerTime == other.ConstructionStartServerTime
|
||||||
|
&& AccumulatedConstructionTime == other.AccumulatedConstructionTime
|
||||||
|
&& GoldSpent == other.GoldSpent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj) => obj is BuildJob other && Equals(other);
|
||||||
|
|
||||||
|
public override int GetHashCode() => JobId.GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lifecycle stage of a <see cref="BuildJob"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Backed by byte to keep the serialized payload small and to allow the byte
|
||||||
|
/// round-trip in <see cref="BuildJob.NetworkSerialize{T}"/>.
|
||||||
|
/// </remarks>
|
||||||
|
public enum BuildStage : byte
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Job is in the queue but the builder has not yet arrived at it.
|
||||||
|
/// Tile is occupied (no other tower can be queued/placed there) but
|
||||||
|
/// remains walkable — enemies pass through queued ghosts because the
|
||||||
|
/// ghost represents intent, not a structure.
|
||||||
|
/// </summary>
|
||||||
|
Queued = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builder has arrived and construction is in progress. Tile is
|
||||||
|
/// occupied AND non-walkable — this is the moment the maze actually
|
||||||
|
/// changes. Path re-validation runs at the transition into this stage.
|
||||||
|
/// </summary>
|
||||||
|
Constructing = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Construction was interrupted by the builder being moved away.
|
||||||
|
/// Tile remains occupied and non-walkable (the half-built tower is
|
||||||
|
/// still physical, still blocks enemies). The cube's Y-scale is frozen
|
||||||
|
/// at the level it reached when paused. Resume returns to Constructing
|
||||||
|
/// stage with previously-accumulated progress preserved.
|
||||||
|
///
|
||||||
|
/// While the head job is Paused, the queue contains exactly this one
|
||||||
|
/// job — all other queued jobs are refunded and removed at the moment
|
||||||
|
/// of pausing.
|
||||||
|
/// </summary>
|
||||||
|
Paused = 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Scripts/Gameplay/BuildJob.cs.meta
Normal file
2
Assets/_Project/Scripts/Gameplay/BuildJob.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5185b25b41381004293388438523c10b
|
||||||
478
Assets/_Project/Scripts/Gameplay/BuildSiteVisual.cs
Normal file
478
Assets/_Project/Scripts/Gameplay/BuildSiteVisual.cs
Normal file
|
|
@ -0,0 +1,478 @@
|
||||||
|
// Assets/_Project/Scripts/Gameplay/BuildSiteVisual.cs
|
||||||
|
using Unity.Collections;
|
||||||
|
using Unity.Netcode;
|
||||||
|
using UnityEngine;
|
||||||
|
using TD.Core;
|
||||||
|
using TD.Towers;
|
||||||
|
|
||||||
|
namespace TD.Gameplay
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Visual representation of an in-flight <see cref="BuildJob"/>: the green
|
||||||
|
/// queued ghost, and the staged construction animation (4 stages of growing
|
||||||
|
/// height for the testing cube). One NetworkObject per active job. Despawned
|
||||||
|
/// when the job is cancelled or when the real <see cref="TowerInstance"/>
|
||||||
|
/// takes its place at construction-complete.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para><b>Why a separate prefab from TowerInstance.</b> The build-site visual
|
||||||
|
/// has different rendering (transparent green or partial-height cube), no combat,
|
||||||
|
/// no grid-stamping (the Builder owns those state transitions), and a much
|
||||||
|
/// shorter lifecycle. Sharing a prefab with TowerInstance would mean adding
|
||||||
|
/// "am I real or a ghost" branching to every TowerInstance code path. Two
|
||||||
|
/// prefabs, two responsibilities.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Stage replication.</b> Stage and ConstructionStartServerTime are
|
||||||
|
/// replicated as NetworkVariables so all peers compute identical visuals locally
|
||||||
|
/// from <c>NetworkManager.ServerTime.TimeAsFloat</c>. Only the server writes;
|
||||||
|
/// clients read.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Visual model.</b> The prefab inspector points at a re-tinted copy
|
||||||
|
/// of the tower's mesh (the same testing cube). At Stage = Queued, the visual
|
||||||
|
/// is a translucent green cube at full footprint scale but reduced Y. At
|
||||||
|
/// Stage = Constructing, the cube grows in 4 sub-stages over BuildTime.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>No grid stamping here.</b> Walkability and occupancy stamps are
|
||||||
|
/// driven by the Builder (queue-time stamps occupied=true / walkable=true,
|
||||||
|
/// construction-start stamps walkable=false, completion is unchanged because
|
||||||
|
/// TowerInstance takes over). Adding stamping here would create double-write
|
||||||
|
/// races with the Builder. See lessons in the project context doc.</para>
|
||||||
|
/// </remarks>
|
||||||
|
[RequireComponent(typeof(NetworkObject))]
|
||||||
|
public class BuildSiteVisual : NetworkBehaviour
|
||||||
|
{
|
||||||
|
// ----- Inspector --------------------------------------------------
|
||||||
|
|
||||||
|
[Header("Visuals")]
|
||||||
|
[Tooltip("Renderers tinted with the queued-ghost color. Typically every " +
|
||||||
|
"MeshRenderer on the prefab. Auto-populated from children if empty.")]
|
||||||
|
[SerializeField] private MeshRenderer[] tintedRenderers;
|
||||||
|
|
||||||
|
[Tooltip("Transform that gets Y-scaled to represent construction progress. " +
|
||||||
|
"Typically the visual mesh's transform. The growth axis is local Y.")]
|
||||||
|
[SerializeField] private Transform scaleTarget;
|
||||||
|
|
||||||
|
[Tooltip("Material applied while the job is Queued (translucent green).")]
|
||||||
|
[SerializeField] private Material queuedMaterial;
|
||||||
|
|
||||||
|
[Tooltip("Material applied while the job is Constructing (opaque, tinted by owner).")]
|
||||||
|
[SerializeField] private Material constructingMaterial;
|
||||||
|
|
||||||
|
[Tooltip("Material applied while the job is Paused. Visually distinct from " +
|
||||||
|
"Constructing so players can tell at a glance which builds need a builder. " +
|
||||||
|
"Suggested: muted/grey-tinted variant of the constructing material.")]
|
||||||
|
[SerializeField] private Material pausedMaterial;
|
||||||
|
|
||||||
|
[Header("Construction stages")]
|
||||||
|
[Tooltip("Number of discrete growth stages while constructing. 4 matches the " +
|
||||||
|
"design doc (1/4 → 2/4 → 3/4 → 4/4 height).")]
|
||||||
|
[SerializeField] private int stageCount = 4;
|
||||||
|
|
||||||
|
[Tooltip("Y-scale applied to scaleTarget when Stage == Queued. Visually " +
|
||||||
|
"distinct from any constructing height so the queued ghost reads as " +
|
||||||
|
"'intent, not progress'.")]
|
||||||
|
[SerializeField] private float queuedYScale = 0.15f;
|
||||||
|
|
||||||
|
// ----- Networked state --------------------------------------------
|
||||||
|
|
||||||
|
// Replicated definition name so clients can resolve the source TowerDefinition
|
||||||
|
// (matches TowerInstance's pattern). Used for footprint size only — the visual
|
||||||
|
// prefab is the same regardless of tower type for now.
|
||||||
|
private readonly NetworkVariable<FixedString64Bytes> definitionName =
|
||||||
|
new NetworkVariable<FixedString64Bytes>(
|
||||||
|
default,
|
||||||
|
readPerm: NetworkVariableReadPermission.Everyone,
|
||||||
|
writePerm: NetworkVariableWritePermission.Server);
|
||||||
|
|
||||||
|
// Replicated owner slot for color tinting. Mirrors TowerInstance.
|
||||||
|
private readonly NetworkVariable<PlayerSlot> ownerSlot =
|
||||||
|
new NetworkVariable<PlayerSlot>(
|
||||||
|
PlayerSlot.None,
|
||||||
|
readPerm: NetworkVariableReadPermission.Everyone,
|
||||||
|
writePerm: NetworkVariableWritePermission.Server);
|
||||||
|
|
||||||
|
// Anchor tile (SW corner of the footprint, world-tile coords). Replicated so
|
||||||
|
// shelved visuals are self-describing — when a player clicks one to resume,
|
||||||
|
// the server can rebuild a BuildJob from these fields without consulting any
|
||||||
|
// separate registry.
|
||||||
|
private readonly NetworkVariable<Vector2Int> anchor =
|
||||||
|
new NetworkVariable<Vector2Int>(
|
||||||
|
Vector2Int.zero,
|
||||||
|
readPerm: NetworkVariableReadPermission.Everyone,
|
||||||
|
writePerm: NetworkVariableWritePermission.Server);
|
||||||
|
|
||||||
|
// Tower type ID (index into TowerPlacementManager.towerDefinitions[]). Replicated
|
||||||
|
// for the same self-describing reason as Anchor.
|
||||||
|
private readonly NetworkVariable<int> towerTypeId =
|
||||||
|
new NetworkVariable<int>(
|
||||||
|
0,
|
||||||
|
readPerm: NetworkVariableReadPermission.Everyone,
|
||||||
|
writePerm: NetworkVariableWritePermission.Server);
|
||||||
|
|
||||||
|
// Gold the player paid to queue this build. Carried on the visual so resume
|
||||||
|
// can refund the correct amount if the player later cancels. (Cancellation
|
||||||
|
// gesture deferred to HUD; this field is the data dependency.)
|
||||||
|
private readonly NetworkVariable<int> goldSpent =
|
||||||
|
new NetworkVariable<int>(
|
||||||
|
0,
|
||||||
|
readPerm: NetworkVariableReadPermission.Everyone,
|
||||||
|
writePerm: NetworkVariableWritePermission.Server);
|
||||||
|
|
||||||
|
// True iff this build site has been "shelved" — removed from its owning Builder's
|
||||||
|
// queue and now a standalone object waiting to be resumed via right-click.
|
||||||
|
// Shelved visuals are responsible for their own grid-state cleanup on despawn
|
||||||
|
// (since the Builder no longer tracks them in jobIdToVisual).
|
||||||
|
private readonly NetworkVariable<bool> isShelved =
|
||||||
|
new NetworkVariable<bool>(
|
||||||
|
false,
|
||||||
|
readPerm: NetworkVariableReadPermission.Everyone,
|
||||||
|
writePerm: NetworkVariableWritePermission.Server);
|
||||||
|
|
||||||
|
// Current stage. Drives material swap and Y-scale animation.
|
||||||
|
private readonly NetworkVariable<BuildStage> currentStage =
|
||||||
|
new NetworkVariable<BuildStage>(
|
||||||
|
BuildStage.Queued,
|
||||||
|
readPerm: NetworkVariableReadPermission.Everyone,
|
||||||
|
writePerm: NetworkVariableWritePermission.Server);
|
||||||
|
|
||||||
|
// Server time at which the current Constructing run began. -1 while Queued or Paused.
|
||||||
|
// Used together with accumulatedConstructionTime to compute total progress:
|
||||||
|
// total = (now - constructionStartServerTime) + accumulatedConstructionTime.
|
||||||
|
private readonly NetworkVariable<float> constructionStartServerTime =
|
||||||
|
new NetworkVariable<float>(
|
||||||
|
-1f,
|
||||||
|
readPerm: NetworkVariableReadPermission.Everyone,
|
||||||
|
writePerm: NetworkVariableWritePermission.Server);
|
||||||
|
|
||||||
|
// Construction time accumulated across previous Constructing runs (for pause/resume).
|
||||||
|
// 0 for jobs that have never been paused. At pause, set to the elapsed time of the
|
||||||
|
// current run added to whatever was already accumulated.
|
||||||
|
private readonly NetworkVariable<float> accumulatedConstructionTime =
|
||||||
|
new NetworkVariable<float>(
|
||||||
|
0f,
|
||||||
|
readPerm: NetworkVariableReadPermission.Everyone,
|
||||||
|
writePerm: NetworkVariableWritePermission.Server);
|
||||||
|
|
||||||
|
// BuildTime is replicated rather than looked up so clients don't need to
|
||||||
|
// resolve the TowerDefinition before they can render progress.
|
||||||
|
// (Resolution can race with the first stage update otherwise.)
|
||||||
|
private readonly NetworkVariable<float> buildTime =
|
||||||
|
new NetworkVariable<float>(
|
||||||
|
0f,
|
||||||
|
readPerm: NetworkVariableReadPermission.Everyone,
|
||||||
|
writePerm: NetworkVariableWritePermission.Server);
|
||||||
|
|
||||||
|
// ----- Public accessors (read by the input controller on click) --
|
||||||
|
|
||||||
|
// NOTE: ownership identity comes from the inherited NetworkBehaviour.OwnerClientId,
|
||||||
|
// which is correct because we use SpawnWithOwnership in Builder.SpawnBuildSiteVisual.
|
||||||
|
// No redundant NetworkVariable for ownership.
|
||||||
|
|
||||||
|
/// <summary>The current build stage. Read by clients for click-target tests.</summary>
|
||||||
|
public BuildStage CurrentStage => currentStage.Value;
|
||||||
|
|
||||||
|
/// <summary>True iff this visual has been shelved (no longer in its Builder's queue).</summary>
|
||||||
|
public bool IsShelved => isShelved.Value;
|
||||||
|
|
||||||
|
/// <summary>Footprint anchor (SW corner). Used by server to rebuild a BuildJob on resume.</summary>
|
||||||
|
public Vector2Int Anchor => anchor.Value;
|
||||||
|
|
||||||
|
/// <summary>Tower type ID. Used by server to rebuild a BuildJob on resume.</summary>
|
||||||
|
public int TowerTypeId => towerTypeId.Value;
|
||||||
|
|
||||||
|
/// <summary>Gold paid for this build. Used by server to rebuild a BuildJob on resume.</summary>
|
||||||
|
public int GoldSpent => goldSpent.Value;
|
||||||
|
|
||||||
|
/// <summary>Accumulated construction time (across pause/resume cycles). Used on resume.</summary>
|
||||||
|
public float AccumulatedConstructionTime => accumulatedConstructionTime.Value;
|
||||||
|
|
||||||
|
// ----- Pre-spawn init data (server) -------------------------------
|
||||||
|
|
||||||
|
private string pendingDefName;
|
||||||
|
private PlayerSlot pendingOwner = PlayerSlot.None;
|
||||||
|
private float pendingBuildTime;
|
||||||
|
private Vector2Int pendingAnchor;
|
||||||
|
private int pendingTowerTypeId;
|
||||||
|
private int pendingGoldSpent;
|
||||||
|
private bool hasPendingInit;
|
||||||
|
|
||||||
|
// ----- Lifecycle --------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server-only: stores the data that <see cref="OnNetworkSpawn"/> will write
|
||||||
|
/// into NetworkVariables. Must be called between Instantiate and Spawn().
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Owner identity is conveyed through NGO ownership (via SpawnWithOwnership),
|
||||||
|
/// not through this method — see <see cref="NetworkBehaviour.OwnerClientId"/>.
|
||||||
|
/// </remarks>
|
||||||
|
public void InitializeServer(TowerDefinition def, PlayerSlot owner,
|
||||||
|
Vector2Int anchorTile, int towerTypeIdValue,
|
||||||
|
int goldSpentValue)
|
||||||
|
{
|
||||||
|
var nm = NetworkManager.Singleton;
|
||||||
|
if (nm == null || !nm.IsServer)
|
||||||
|
{
|
||||||
|
Debug.LogError("[BuildSiteVisual] InitializeServer called when not running as server.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingDefName = def != null ? def.name : string.Empty;
|
||||||
|
pendingOwner = owner;
|
||||||
|
pendingBuildTime = def != null ? def.BuildTime : 0f;
|
||||||
|
pendingAnchor = anchorTile;
|
||||||
|
pendingTowerTypeId = towerTypeIdValue;
|
||||||
|
pendingGoldSpent = goldSpentValue;
|
||||||
|
hasPendingInit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnNetworkSpawn()
|
||||||
|
{
|
||||||
|
// Auto-populate tinted renderers if not configured in the inspector.
|
||||||
|
if (tintedRenderers == null || tintedRenderers.Length == 0)
|
||||||
|
tintedRenderers = GetComponentsInChildren<MeshRenderer>();
|
||||||
|
|
||||||
|
// Server: now that the NetworkObject is spawned, write the pending init
|
||||||
|
// values into NetworkVariables. NGO captures these into the initial sync
|
||||||
|
// message so clients see correct values on their first OnNetworkSpawn.
|
||||||
|
if (IsServer && hasPendingInit)
|
||||||
|
{
|
||||||
|
definitionName.Value = new FixedString64Bytes(pendingDefName ?? string.Empty);
|
||||||
|
ownerSlot.Value = pendingOwner;
|
||||||
|
buildTime.Value = pendingBuildTime;
|
||||||
|
anchor.Value = pendingAnchor;
|
||||||
|
towerTypeId.Value = pendingTowerTypeId;
|
||||||
|
goldSpent.Value = pendingGoldSpent;
|
||||||
|
hasPendingInit = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to value changes so visual updates are reactive.
|
||||||
|
currentStage.OnValueChanged += HandleStageChanged;
|
||||||
|
|
||||||
|
// Apply initial visual state based on the (now-replicated) values.
|
||||||
|
ApplyStageVisual(currentStage.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnNetworkDespawn()
|
||||||
|
{
|
||||||
|
currentStage.OnValueChanged -= HandleStageChanged;
|
||||||
|
|
||||||
|
// Server-only cleanup: if this visual was shelved at the time it was
|
||||||
|
// despawned (e.g., the player disconnected while a tower was shelved),
|
||||||
|
// restore walkability and occupancy on the footprint. Non-shelved
|
||||||
|
// visuals are owned by their Builder, which handles cleanup via
|
||||||
|
// jobIdToVisual; we mustn't double-free in that case.
|
||||||
|
if (IsServer && isShelved.Value)
|
||||||
|
{
|
||||||
|
RestoreFootprintGridState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server-only: restore walkability=true and occupancy=false on this build site's
|
||||||
|
// footprint. Called when a shelved visual is despawned without going through a
|
||||||
|
// normal "resume → cancel" cycle (e.g., player disconnect cleanup).
|
||||||
|
private void RestoreFootprintGridState()
|
||||||
|
{
|
||||||
|
var loader = LevelLoader.Instance;
|
||||||
|
if (loader == null || !loader.IsLoaded) return;
|
||||||
|
|
||||||
|
var def = TowerPlacementManager.GetDefinition(towerTypeId.Value);
|
||||||
|
if (def == null) return;
|
||||||
|
|
||||||
|
foreach (var tile in GridCoordinates.GetFootprintTiles(
|
||||||
|
anchor.Value, def.FootprintSize))
|
||||||
|
{
|
||||||
|
loader.SetOccupied(tile, false);
|
||||||
|
loader.SetWalkable(tile, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Per-frame visual update (all peers) ------------------------
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
// While constructing, smoothly interpolate Y-scale through the stages
|
||||||
|
// based on server time. This runs on every peer (server + clients) so
|
||||||
|
// visuals stay synchronized regardless of who's looking.
|
||||||
|
// Paused stage does NOT update — Y-scale is frozen at the pause point.
|
||||||
|
if (currentStage.Value != BuildStage.Constructing) return;
|
||||||
|
|
||||||
|
float yScale = ComputeConstructingYScale();
|
||||||
|
ApplyYScale(yScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Server API -------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server-only: marks this visual as shelved. The visual remains in the world
|
||||||
|
/// but no longer belongs to the owning Builder's queue. Stage stays Paused;
|
||||||
|
/// this method just flips the IsShelved flag so the visual takes responsibility
|
||||||
|
/// for its own grid-state cleanup on despawn.
|
||||||
|
/// </summary>
|
||||||
|
public void ServerMarkShelved()
|
||||||
|
{
|
||||||
|
if (!IsServer) return;
|
||||||
|
isShelved.Value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server-only: marks this visual as unshelved (taken back into a Builder's queue).
|
||||||
|
/// Called when the player right-clicks a shelved visual to resume construction.
|
||||||
|
/// Grid-state cleanup is once again the Builder's responsibility via jobIdToVisual.
|
||||||
|
/// </summary>
|
||||||
|
public void ServerMarkUnshelved()
|
||||||
|
{
|
||||||
|
if (!IsServer) return;
|
||||||
|
isShelved.Value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server-only: transitions the visual from Queued (or Paused) to Constructing
|
||||||
|
/// and records the server time for stage progression. Caller is responsible
|
||||||
|
/// for setting <paramref name="accumulatedTimeBeforeThisRun"/> from the BuildJob's
|
||||||
|
/// AccumulatedConstructionTime — non-zero values mean this is a resume.
|
||||||
|
/// </summary>
|
||||||
|
public void ServerBeginConstructing(float accumulatedTimeBeforeThisRun)
|
||||||
|
{
|
||||||
|
if (!IsServer) return;
|
||||||
|
if (currentStage.Value == BuildStage.Constructing) return;
|
||||||
|
|
||||||
|
accumulatedConstructionTime.Value = accumulatedTimeBeforeThisRun;
|
||||||
|
constructionStartServerTime.Value =
|
||||||
|
(float)NetworkManager.Singleton.ServerTime.Time;
|
||||||
|
currentStage.Value = BuildStage.Constructing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server-only: transitions Constructing → Paused AND writes the new
|
||||||
|
/// accumulated construction time. Y-scale freezes at the level matching
|
||||||
|
/// the accumulated time. After this returns, the visual is fully self-
|
||||||
|
/// describing — it carries enough state to be shelved and later resumed.
|
||||||
|
/// </summary>
|
||||||
|
public void ServerPauseAndPersistAccumulated(float totalAccumulated)
|
||||||
|
{
|
||||||
|
if (!IsServer) return;
|
||||||
|
if (currentStage.Value != BuildStage.Constructing) return;
|
||||||
|
|
||||||
|
accumulatedConstructionTime.Value = totalAccumulated;
|
||||||
|
// Reset constructionStartServerTime to -1 so any future progress reads
|
||||||
|
// know there's no active timer running.
|
||||||
|
constructionStartServerTime.Value = -1f;
|
||||||
|
currentStage.Value = BuildStage.Paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Visual state machine ---------------------------------------
|
||||||
|
|
||||||
|
private void HandleStageChanged(BuildStage previous, BuildStage current)
|
||||||
|
{
|
||||||
|
ApplyStageVisual(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyStageVisual(BuildStage stage)
|
||||||
|
{
|
||||||
|
switch (stage)
|
||||||
|
{
|
||||||
|
case BuildStage.Queued:
|
||||||
|
SwapMaterial(queuedMaterial);
|
||||||
|
ApplyYScale(queuedYScale);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BuildStage.Constructing:
|
||||||
|
SwapMaterial(constructingMaterial);
|
||||||
|
ApplyOwnerTint();
|
||||||
|
ApplyYScale(ComputeConstructingYScale());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BuildStage.Paused:
|
||||||
|
// Use paused material if assigned; fall back to constructing
|
||||||
|
// material if not (still readable, just less distinct).
|
||||||
|
SwapMaterial(pausedMaterial != null ? pausedMaterial : constructingMaterial);
|
||||||
|
ApplyOwnerTint();
|
||||||
|
// Freeze Y-scale at whatever the accumulated progress represents.
|
||||||
|
// ComputeConstructingYScale uses accumulatedConstructionTime alone
|
||||||
|
// when constructionStartServerTime is -1 (the pause sentinel).
|
||||||
|
ApplyYScale(ComputePausedYScale());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stage index 0..stageCount-1 based on elapsed server time PLUS any accumulated
|
||||||
|
// time from previous Constructing runs (resume support).
|
||||||
|
// Returned Y-scale is (stageIndex + 1) / stageCount, so stage 0 = 1/4,
|
||||||
|
// stage 1 = 2/4, ..., stage stageCount-1 = 4/4 = full height.
|
||||||
|
private float ComputeConstructingYScale()
|
||||||
|
{
|
||||||
|
float bt = buildTime.Value;
|
||||||
|
if (bt <= 0f || stageCount <= 0) return 1f;
|
||||||
|
|
||||||
|
float currentRunElapsed = (float)NetworkManager.Singleton.ServerTime.Time
|
||||||
|
- constructionStartServerTime.Value;
|
||||||
|
float total = currentRunElapsed + accumulatedConstructionTime.Value;
|
||||||
|
|
||||||
|
float perStage = bt / stageCount;
|
||||||
|
int stageIndex = Mathf.Clamp(
|
||||||
|
Mathf.FloorToInt(total / perStage),
|
||||||
|
0, stageCount - 1);
|
||||||
|
|
||||||
|
return (stageIndex + 1f) / stageCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// While Paused, only the accumulated time matters (no current run is in flight).
|
||||||
|
private float ComputePausedYScale()
|
||||||
|
{
|
||||||
|
float bt = buildTime.Value;
|
||||||
|
if (bt <= 0f || stageCount <= 0) return 1f;
|
||||||
|
|
||||||
|
float total = accumulatedConstructionTime.Value;
|
||||||
|
float perStage = bt / stageCount;
|
||||||
|
int stageIndex = Mathf.Clamp(
|
||||||
|
Mathf.FloorToInt(total / perStage),
|
||||||
|
0, stageCount - 1);
|
||||||
|
|
||||||
|
return (stageIndex + 1f) / stageCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyYScale(float y)
|
||||||
|
{
|
||||||
|
if (scaleTarget == null) return;
|
||||||
|
Vector3 s = scaleTarget.localScale;
|
||||||
|
scaleTarget.localScale = new Vector3(s.x, y, s.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Material handling ------------------------------------------
|
||||||
|
|
||||||
|
private void SwapMaterial(Material mat)
|
||||||
|
{
|
||||||
|
if (mat == null || tintedRenderers == null) return;
|
||||||
|
foreach (var rend in tintedRenderers)
|
||||||
|
{
|
||||||
|
if (rend == null) continue;
|
||||||
|
rend.sharedMaterial = mat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reused per-instance to avoid GC. Lazy because Unity disallows
|
||||||
|
// construction in field initializers.
|
||||||
|
private MaterialPropertyBlock colorPropertyBlock;
|
||||||
|
private static readonly int ColorPropertyId = Shader.PropertyToID("_Color");
|
||||||
|
private static readonly int BaseColorPropertyId = Shader.PropertyToID("_BaseColor");
|
||||||
|
|
||||||
|
private void ApplyOwnerTint()
|
||||||
|
{
|
||||||
|
if (tintedRenderers == null) return;
|
||||||
|
|
||||||
|
Color c = PlayerColors.Get(ownerSlot.Value);
|
||||||
|
c.a = 1f;
|
||||||
|
|
||||||
|
colorPropertyBlock ??= new MaterialPropertyBlock();
|
||||||
|
colorPropertyBlock.SetColor(ColorPropertyId, c);
|
||||||
|
colorPropertyBlock.SetColor(BaseColorPropertyId, c);
|
||||||
|
|
||||||
|
foreach (var rend in tintedRenderers)
|
||||||
|
{
|
||||||
|
if (rend == null) continue;
|
||||||
|
rend.SetPropertyBlock(colorPropertyBlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Scripts/Gameplay/BuildSiteVisual.cs.meta
Normal file
2
Assets/_Project/Scripts/Gameplay/BuildSiteVisual.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a236b3b34c8dd784db3bed4e6b0f44f9
|
||||||
|
|
@ -3,32 +3,46 @@ using System.Collections.Generic;
|
||||||
using Unity.Netcode;
|
using Unity.Netcode;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using TD.Core;
|
using TD.Core;
|
||||||
using TD.Levels;
|
using TD.Towers;
|
||||||
|
|
||||||
namespace TD.Gameplay
|
namespace TD.Gameplay
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Per-player avatar that gates tower placement by proximity. Server-authoritative
|
/// Per-player avatar that gates tower placement by proximity AND drives the build
|
||||||
/// position; clients submit move requests via Rpc and the server validates and applies.
|
/// queue. Server-authoritative position and queue; clients submit move requests
|
||||||
|
/// via Rpc and read replicated NetworkList state to render queued/constructing
|
||||||
|
/// visuals.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para><b>Pure visual avatar.</b> Builders have no collider for gameplay purposes —
|
/// <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
|
/// (no enemy blocking, not targetable). They DO have a small trigger collider on
|
||||||
/// are visible to all players but only their owner can move them.</para>
|
/// the "Selection" physics layer so left-click can select them — that collider
|
||||||
|
/// must not be on layers that participate in placement raycasts or builder
|
||||||
|
/// height sampling. See the prefab setup notes in the project context doc.</para>
|
||||||
///
|
///
|
||||||
/// <para><b>Terrain-aware height.</b> Each frame the server casts a ray straight down
|
/// <para><b>Terrain-aware height.</b> Each frame the server casts a ray straight
|
||||||
/// from the builder against the <see cref="terrainLayerMask"/> and sets Y to
|
/// down from the builder against <see cref="terrainLayerMask"/> and sets Y to
|
||||||
/// <c>hit.point.y + heightOffset</c>. If the ray misses, falls back to the buildable
|
/// <c>hit.point.y + heightOffset</c>. Falls back to <see cref="GridCoordinates.BUILDABLE_PLANE_Y"/>
|
||||||
/// plane Y. Towers are not on the terrain layer, so they don't influence height.</para>
|
/// if the ray misses. Towers must not be on the terrain layer.</para>
|
||||||
///
|
///
|
||||||
/// <para><b>Range gating.</b> <see cref="IsTileWithinBuildRange"/> is the public query
|
/// <para><b>Range gating.</b> <see cref="IsTileWithinBuildRange"/> is the public
|
||||||
/// that <c>TowerPlacementManager</c> uses to validate placement requests. Builder range
|
/// query that <c>TowerPlacementManager</c> uses to validate placement requests.
|
||||||
/// is measured center-of-builder to center-of-anchor-tile in world units.</para>
|
/// Range is measured center-of-builder to nearest-point-of-footprint in world units.</para>
|
||||||
///
|
///
|
||||||
/// <para><b>Static registry.</b> Like <see cref="PlayerGoldManager"/>, builders register
|
/// <para><b>Build queue.</b> Each Builder owns a <see cref="NetworkList{T}"/> of
|
||||||
/// themselves in a static dictionary keyed by <c>OwnerClientId</c> on spawn, so server
|
/// <see cref="BuildJob"/>. The server appends jobs from
|
||||||
/// gameplay code (notably <c>TowerPlacementManager</c>) can find a player's builder without
|
/// <c>TowerPlacementManager.ProcessRequest</c> via <see cref="ServerEnqueueJob"/>.
|
||||||
/// scene traversal.</para>
|
/// Each server tick, if the head job is Queued, the builder walks toward its
|
||||||
|
/// anchor; on arrival, the head job transitions to Constructing (path re-validated
|
||||||
|
/// here; refunded and dropped if the maze would now break). Stages advance by
|
||||||
|
/// time; on completion the visual is replaced with a real <c>TowerInstance</c>
|
||||||
|
/// and the head job is removed. Cancellation drops every job in the queue and
|
||||||
|
/// refunds 100% per the design doc.</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 can find a player's builder without scene
|
||||||
|
/// traversal.</para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[RequireComponent(typeof(NetworkObject))]
|
[RequireComponent(typeof(NetworkObject))]
|
||||||
public class Builder : NetworkBehaviour
|
public class Builder : NetworkBehaviour
|
||||||
|
|
@ -74,6 +88,11 @@ namespace TD.Gameplay
|
||||||
"but smoother.")]
|
"but smoother.")]
|
||||||
[SerializeField] private float arrivalThreshold = 0.05f;
|
[SerializeField] private float arrivalThreshold = 0.05f;
|
||||||
|
|
||||||
|
[Tooltip("Degrees per second the builder rotates to face its movement direction. " +
|
||||||
|
"Lower = lazier turns; higher = snappier. The builder only rotates while " +
|
||||||
|
"moving; it keeps its last facing when idle.")]
|
||||||
|
[SerializeField] private float turnRateDegPerSec = 540f;
|
||||||
|
|
||||||
[Header("Height tracking")]
|
[Header("Height tracking")]
|
||||||
[Tooltip("Vertical offset above the terrain at which the builder hovers. " +
|
[Tooltip("Vertical offset above the terrain at which the builder hovers. " +
|
||||||
"Re-evaluated every server tick by raycasting straight down.")]
|
"Re-evaluated every server tick by raycasting straight down.")]
|
||||||
|
|
@ -93,6 +112,24 @@ namespace TD.Gameplay
|
||||||
"for placement to be allowed, measured in world units (== tiles).")]
|
"for placement to be allowed, measured in world units (== tiles).")]
|
||||||
[SerializeField] private float buildRange = 6f;
|
[SerializeField] private float buildRange = 6f;
|
||||||
|
|
||||||
|
[Header("Build queue")]
|
||||||
|
[Tooltip("Maximum number of pending build jobs. Bounds memory and prevents a player " +
|
||||||
|
"from spamming queue entries faster than the server can process them.")]
|
||||||
|
[SerializeField] private int maxQueueDepth = 32;
|
||||||
|
|
||||||
|
[Tooltip("Build-site visual prefab. Spawned at queue-time as a green ghost; " +
|
||||||
|
"transitions to staged-construction visuals on arrival; despawned on " +
|
||||||
|
"completion (replaced by the real TowerInstance) or cancellation.")]
|
||||||
|
[SerializeField] private GameObject buildSiteVisualPrefab;
|
||||||
|
|
||||||
|
[Header("Visuals")]
|
||||||
|
[Tooltip("Mesh renderers that should be tinted with the owner's player color. " +
|
||||||
|
"Drag in only the builder body's renderers — exclude the SelectionRing, " +
|
||||||
|
"BuildRangeIndicator, or any other visual that has its own color rules. " +
|
||||||
|
"If left empty, the builder will not be tinted (other meshes' colors " +
|
||||||
|
"from the prefab are preserved).")]
|
||||||
|
[SerializeField] private MeshRenderer[] tintedRenderers;
|
||||||
|
|
||||||
// ----- Networked state --------------------------------------------
|
// ----- Networked state --------------------------------------------
|
||||||
|
|
||||||
// Server-authoritative target position. The server moves the builder toward this
|
// Server-authoritative target position. The server moves the builder toward this
|
||||||
|
|
@ -104,6 +141,28 @@ namespace TD.Gameplay
|
||||||
readPerm: NetworkVariableReadPermission.Everyone,
|
readPerm: NetworkVariableReadPermission.Everyone,
|
||||||
writePerm: NetworkVariableWritePermission.Server);
|
writePerm: NetworkVariableWritePermission.Server);
|
||||||
|
|
||||||
|
// The build queue. Replicated as a NetworkList so all clients can render queued
|
||||||
|
// ghosts and progress without per-job RPCs. Server is the only writer.
|
||||||
|
private NetworkList<BuildJob> jobs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read-only access to the current job queue. Clients use this to render
|
||||||
|
/// build-site visuals at queued anchors. Index 0 is the head (active) job.
|
||||||
|
/// </summary>
|
||||||
|
public NetworkList<BuildJob> Jobs => jobs;
|
||||||
|
|
||||||
|
// ----- Server-only state ------------------------------------------
|
||||||
|
//
|
||||||
|
// Tracks the BuildSiteVisual NetworkObject for each active job so we can
|
||||||
|
// despawn it on completion or cancellation. NetworkList itself doesn't
|
||||||
|
// hold object references; we keep the side-table here.
|
||||||
|
private readonly Dictionary<ulong, NetworkObject> jobIdToVisual
|
||||||
|
= new Dictionary<ulong, NetworkObject>();
|
||||||
|
|
||||||
|
// Monotonic job-ID counter on the server. Resets per builder, which is fine —
|
||||||
|
// IDs only need to be unique within one builder's queue.
|
||||||
|
private ulong nextJobId = 1;
|
||||||
|
|
||||||
// ----- Public accessors -------------------------------------------
|
// ----- Public accessors -------------------------------------------
|
||||||
|
|
||||||
/// <summary>The builder's current world position (its actual transform position,
|
/// <summary>The builder's current world position (its actual transform position,
|
||||||
|
|
@ -122,8 +181,39 @@ namespace TD.Gameplay
|
||||||
/// <summary>Build range in world units.</summary>
|
/// <summary>Build range in world units.</summary>
|
||||||
public float BuildRange => buildRange;
|
public float BuildRange => buildRange;
|
||||||
|
|
||||||
|
/// <summary>Maximum jobs allowed in the queue.</summary>
|
||||||
|
public int MaxQueueDepth => maxQueueDepth;
|
||||||
|
|
||||||
|
/// <summary>True if a tile is currently part of any queued or constructing job.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Used by <c>TowerPlacementManager</c> to reject placement on tiles already
|
||||||
|
/// covered by another pending job in this builder's queue. Walks every job
|
||||||
|
/// and footprint — O(jobs × tiles) but bounded by <see cref="MaxQueueDepth"/>.
|
||||||
|
/// </remarks>
|
||||||
|
public bool IsTileInActiveJob(Vector2Int tile)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < jobs.Count; i++)
|
||||||
|
{
|
||||||
|
var job = jobs[i];
|
||||||
|
Vector2Int footprint = ResolveFootprint(job.TowerTypeId);
|
||||||
|
foreach (var t in GridCoordinates.GetFootprintTiles(job.Anchor, footprint))
|
||||||
|
{
|
||||||
|
if (t == tile) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// ----- Lifecycle --------------------------------------------------
|
// ----- Lifecycle --------------------------------------------------
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
// NetworkList must be allocated before NetworkObject.Spawn() (NGO 2.x
|
||||||
|
// discovers it during the spawn handshake). Awake runs before any
|
||||||
|
// network lifecycle method, so this is the right place.
|
||||||
|
jobs = new NetworkList<BuildJob>();
|
||||||
|
}
|
||||||
|
|
||||||
public override void OnNetworkSpawn()
|
public override void OnNetworkSpawn()
|
||||||
{
|
{
|
||||||
s_byClientId[OwnerClientId] = this;
|
s_byClientId[OwnerClientId] = this;
|
||||||
|
|
@ -132,15 +222,46 @@ namespace TD.Gameplay
|
||||||
{
|
{
|
||||||
// Set initial target = current position so the builder doesn't drift on spawn.
|
// 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
|
// The spawner is responsible for placing this builder at a sensible position
|
||||||
// BEFORE Spawn() — see PlayerSpawnHelper / Player.OnNetworkSpawn.
|
// BEFORE Spawn() — see PlayerBuilderSpawner.
|
||||||
targetPosition.Value = transform.position;
|
targetPosition.Value = transform.position;
|
||||||
Debug.Log($"[Builder] Spawned for client {OwnerClientId} at " +
|
Debug.Log($"[Builder] Spawned for client {OwnerClientId} at " +
|
||||||
$"{transform.position}.");
|
$"{transform.position}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyOwnerColor();
|
ApplyOwnerColor();
|
||||||
|
|
||||||
|
// Auto-select on spawn for the local owner. RTS-standard "your unit is
|
||||||
|
// selected by default when it appears" — without this, the player has to
|
||||||
|
// click their own builder before any right-click command works, which is
|
||||||
|
// friction. Players can still deselect (left-click empty space, Escape)
|
||||||
|
// and re-select normally. Owner-gated so remote clients don't accidentally
|
||||||
|
// get someone else's builder selected.
|
||||||
|
if (IsOwner)
|
||||||
|
{
|
||||||
|
SelectionState.Instance?.Select(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void OnNetworkDespawn()
|
||||||
|
{
|
||||||
|
if (s_byClientId.TryGetValue(OwnerClientId, out var registered) && registered == this)
|
||||||
|
s_byClientId.Remove(OwnerClientId);
|
||||||
|
|
||||||
|
// Server-only cleanup: despawn any remaining build-site visuals so they
|
||||||
|
// don't leak when a player disconnects mid-construction.
|
||||||
|
if (IsServer)
|
||||||
|
{
|
||||||
|
foreach (var kv in jobIdToVisual)
|
||||||
|
{
|
||||||
|
if (kv.Value != null && kv.Value.IsSpawned)
|
||||||
|
kv.Value.Despawn(destroy: true);
|
||||||
|
}
|
||||||
|
jobIdToVisual.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// (NetworkList is owned by NGO; no manual Dispose needed in NGO 2.x.)
|
||||||
|
|
||||||
// ----- Owner color tinting ----------------------------------------
|
// ----- Owner color tinting ----------------------------------------
|
||||||
|
|
||||||
// Lazily allocated; reused across renderers. Construction in a field initializer
|
// Lazily allocated; reused across renderers. Construction in a field initializer
|
||||||
|
|
@ -166,14 +287,16 @@ namespace TD.Gameplay
|
||||||
colorPropertyBlock.SetColor(ColorPropertyId, c);
|
colorPropertyBlock.SetColor(ColorPropertyId, c);
|
||||||
colorPropertyBlock.SetColor(BaseColorPropertyId, c);
|
colorPropertyBlock.SetColor(BaseColorPropertyId, c);
|
||||||
|
|
||||||
foreach (var rend in GetComponentsInChildren<MeshRenderer>())
|
// Tint only the renderers explicitly listed in the inspector. Avoids
|
||||||
|
// accidentally re-coloring the SelectionRing or BuildRangeIndicator,
|
||||||
|
// which need their own (non-player) colors. If the list is empty,
|
||||||
|
// skip — the builder shows whatever colors are baked into the prefab.
|
||||||
|
if (tintedRenderers == null) return;
|
||||||
|
foreach (var rend in tintedRenderers)
|
||||||
|
{
|
||||||
|
if (rend == null) continue;
|
||||||
rend.SetPropertyBlock(colorPropertyBlock);
|
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) ---------------------------
|
// ----- Per-frame movement (server only) ---------------------------
|
||||||
|
|
@ -182,7 +305,15 @@ namespace TD.Gameplay
|
||||||
{
|
{
|
||||||
if (!IsServer) return;
|
if (!IsServer) return;
|
||||||
|
|
||||||
// Move toward target on the XZ plane.
|
// Step 1: drive movement target from the queue head, if appropriate.
|
||||||
|
ServerDriveQueue();
|
||||||
|
|
||||||
|
// Step 2: move toward the target on XZ, sample terrain Y.
|
||||||
|
ServerStepMovement();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ServerStepMovement()
|
||||||
|
{
|
||||||
Vector3 current = transform.position;
|
Vector3 current = transform.position;
|
||||||
Vector3 target = targetPosition.Value;
|
Vector3 target = targetPosition.Value;
|
||||||
|
|
||||||
|
|
@ -191,18 +322,35 @@ namespace TD.Gameplay
|
||||||
Vector3 targetXZ = new Vector3(target.x, 0f, target.z);
|
Vector3 targetXZ = new Vector3(target.x, 0f, target.z);
|
||||||
|
|
||||||
Vector3 newXZ;
|
Vector3 newXZ;
|
||||||
|
bool moving;
|
||||||
if (Vector3.SqrMagnitude(currentXZ - targetXZ) <= arrivalThreshold * arrivalThreshold)
|
if (Vector3.SqrMagnitude(currentXZ - targetXZ) <= arrivalThreshold * arrivalThreshold)
|
||||||
{
|
{
|
||||||
newXZ = targetXZ;
|
newXZ = targetXZ;
|
||||||
|
moving = false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
newXZ = Vector3.MoveTowards(currentXZ, targetXZ, moveSpeed * Time.deltaTime);
|
newXZ = Vector3.MoveTowards(currentXZ, targetXZ, moveSpeed * Time.deltaTime);
|
||||||
|
moving = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve Y from terrain.
|
// Resolve Y from terrain.
|
||||||
float groundY = SampleTerrainY(new Vector3(newXZ.x, 0f, newXZ.z));
|
float groundY = SampleTerrainY(new Vector3(newXZ.x, 0f, newXZ.z));
|
||||||
transform.position = new Vector3(newXZ.x, groundY + heightOffset, newXZ.z);
|
transform.position = new Vector3(newXZ.x, groundY + heightOffset, newXZ.z);
|
||||||
|
|
||||||
|
// Smoothly face the movement direction. We rotate on the server only;
|
||||||
|
// NetworkTransform replicates the rotation to clients alongside position.
|
||||||
|
// Skip rotation when stationary so the builder keeps its last facing.
|
||||||
|
if (moving)
|
||||||
|
{
|
||||||
|
Vector3 dir = targetXZ - currentXZ;
|
||||||
|
if (dir.sqrMagnitude > 0.0001f)
|
||||||
|
{
|
||||||
|
Quaternion desired = Quaternion.LookRotation(dir, Vector3.up);
|
||||||
|
transform.rotation = Quaternion.RotateTowards(
|
||||||
|
transform.rotation, desired, turnRateDegPerSec * Time.deltaTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -228,8 +376,7 @@ namespace TD.Gameplay
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Server-side entry point: directly sets the move target. Called by the input
|
/// 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
|
/// controller's Rpc handler after validation. Out-of-map positions are rejected.
|
||||||
/// to the current position (no-op).
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ServerSetMoveTarget(Vector3 worldPos)
|
public void ServerSetMoveTarget(Vector3 worldPos)
|
||||||
{
|
{
|
||||||
|
|
@ -246,8 +393,7 @@ namespace TD.Gameplay
|
||||||
Vector2Int tile = GridCoordinates.WorldToGrid(worldPos);
|
Vector2Int tile = GridCoordinates.WorldToGrid(worldPos);
|
||||||
if (!loader.IsInMap(tile))
|
if (!loader.IsInMap(tile))
|
||||||
{
|
{
|
||||||
// Out-of-map move requests are rejected silently. Could log if useful for
|
// Out-of-map move requests are rejected silently.
|
||||||
// debugging client/server mismatch, but otherwise this is normal.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -268,7 +414,7 @@ namespace TD.Gameplay
|
||||||
ServerSetMoveTarget(worldPos);
|
ServerSetMoveTarget(worldPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- Range query (used by TowerPlacementManager) ----------------
|
// ----- Range query (used by Builder's own queue driver) ----------
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// True if the tower with the given anchor and footprint size is within build range
|
/// True if the tower with the given anchor and footprint size is within build range
|
||||||
|
|
@ -276,9 +422,14 @@ namespace TD.Gameplay
|
||||||
/// nearest point of the tower footprint, in world units.
|
/// nearest point of the tower footprint, in world units.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// "Nearest point of the footprint" rather than "footprint center" so that a tower
|
/// <para>Used by <see cref="DriveHead_Queued"/> as the "have I arrived?" check —
|
||||||
/// is reachable when ANY of its tiles is within range, even if the center is
|
/// the builder walks toward a queued job until it's in range, then begins
|
||||||
/// slightly outside. Aligns with player intuition that "I can reach this tile."
|
/// construction. NOT consulted at queue-time; players can queue any tile in their
|
||||||
|
/// zone regardless of where the builder currently is.</para>
|
||||||
|
///
|
||||||
|
/// <para>"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."</para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public bool IsTileWithinBuildRange(Vector2Int anchor, Vector2Int footprintSize)
|
public bool IsTileWithinBuildRange(Vector2Int anchor, Vector2Int footprintSize)
|
||||||
{
|
{
|
||||||
|
|
@ -299,5 +450,572 @@ namespace TD.Gameplay
|
||||||
return Vector3.SqrMagnitude(builderXZ - nearestPoint)
|
return Vector3.SqrMagnitude(builderXZ - nearestPoint)
|
||||||
<= buildRange * buildRange;
|
<= buildRange * buildRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// BUILD QUEUE (server-side)
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server-only: append a new job to the queue. Caller is responsible for having
|
||||||
|
/// already validated the placement (ownership, gold, range, path) and stamped
|
||||||
|
/// the footprint as occupied (walkable=true). Returns true on success, or false
|
||||||
|
/// if the queue is full.
|
||||||
|
/// </summary>
|
||||||
|
public bool ServerEnqueueJob(Vector2Int anchor, int towerTypeId, int goldSpent,
|
||||||
|
out ulong jobId)
|
||||||
|
{
|
||||||
|
jobId = 0;
|
||||||
|
if (!IsServer) return false;
|
||||||
|
if (jobs.Count >= maxQueueDepth) return false;
|
||||||
|
|
||||||
|
jobId = nextJobId++;
|
||||||
|
var job = BuildJob.CreateQueued(jobId, anchor, towerTypeId, goldSpent);
|
||||||
|
jobs.Add(job);
|
||||||
|
|
||||||
|
// Spawn a green-ghost build-site visual at the anchor.
|
||||||
|
SpawnBuildSiteVisual(job);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SpawnBuildSiteVisual(BuildJob job)
|
||||||
|
{
|
||||||
|
if (buildSiteVisualPrefab == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[Builder] No buildSiteVisualPrefab assigned. " +
|
||||||
|
"Queued ghost will not be visible.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var def = TowerPlacementManager.GetDefinition(job.TowerTypeId);
|
||||||
|
if (def == null)
|
||||||
|
{
|
||||||
|
Debug.LogError($"[Builder] Could not resolve TowerDefinition " +
|
||||||
|
$"{job.TowerTypeId} when spawning build-site visual.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3 spawnPos = GridCoordinates.GetFootprintCenterWorld(
|
||||||
|
job.Anchor, def.FootprintSize);
|
||||||
|
// Sit on the buildable plane at the same elevation real towers will use.
|
||||||
|
spawnPos.y = 0f;
|
||||||
|
|
||||||
|
var go = Instantiate(buildSiteVisualPrefab, spawnPos, Quaternion.identity);
|
||||||
|
var visual = go.GetComponent<BuildSiteVisual>();
|
||||||
|
if (visual == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[Builder] buildSiteVisualPrefab is missing a " +
|
||||||
|
"BuildSiteVisual component.");
|
||||||
|
Destroy(go);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Owner slot: same stub mapping as elsewhere.
|
||||||
|
byte slotByte = (byte)(OwnerClientId + 1);
|
||||||
|
PlayerSlot owner = (slotByte >= 1 && slotByte <= 9)
|
||||||
|
? (PlayerSlot)slotByte
|
||||||
|
: PlayerSlot.None;
|
||||||
|
|
||||||
|
visual.InitializeServer(def, owner, job.Anchor, job.TowerTypeId, job.GoldSpent);
|
||||||
|
|
||||||
|
var netObj = go.GetComponent<NetworkObject>();
|
||||||
|
if (netObj == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[Builder] buildSiteVisualPrefab is missing a NetworkObject.");
|
||||||
|
Destroy(go);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// SpawnWithOwnership rather than plain Spawn so the visual's inherited
|
||||||
|
// NetworkBehaviour.OwnerClientId correctly reports the player's client ID.
|
||||||
|
// This is purely for identity (used by client-side click tests in
|
||||||
|
// BuilderInputController) — the visual has no owner-only Rpcs, so granting
|
||||||
|
// ownership to the player has no security implications. As a bonus, NGO
|
||||||
|
// automatically cleans up owned NetworkObjects when a player disconnects.
|
||||||
|
netObj.SpawnWithOwnership(OwnerClientId, destroyWithScene: true);
|
||||||
|
|
||||||
|
jobIdToVisual[job.JobId] = netObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Per-tick queue drive (server) ------------------------------
|
||||||
|
|
||||||
|
private void ServerDriveQueue()
|
||||||
|
{
|
||||||
|
if (jobs.Count == 0) return;
|
||||||
|
|
||||||
|
var head = jobs[0];
|
||||||
|
|
||||||
|
switch (head.Stage)
|
||||||
|
{
|
||||||
|
case BuildStage.Queued:
|
||||||
|
DriveHead_Queued(ref head);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BuildStage.Constructing:
|
||||||
|
DriveHead_Constructing(ref head);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BuildStage.Paused:
|
||||||
|
// Nothing to do — paused jobs wait for an explicit resume command.
|
||||||
|
// The builder is free to be moved around; targetPosition is not
|
||||||
|
// touched here so player right-click moves are honored.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// While the head job is Queued: walk toward the build site, checking each
|
||||||
|
// tick whether the builder is now in range. On reaching range, re-validate
|
||||||
|
// the path and either kick off construction or fail it. The builder doesn't
|
||||||
|
// need to walk *onto* the build site — being within build-range is enough,
|
||||||
|
// matching player intuition and the Wintermaul-style "reach to build" feel.
|
||||||
|
private void DriveHead_Queued(ref BuildJob head)
|
||||||
|
{
|
||||||
|
var def = TowerPlacementManager.GetDefinition(head.TowerTypeId);
|
||||||
|
if (def == null)
|
||||||
|
{
|
||||||
|
// Lost the definition somehow — drop the job and refund.
|
||||||
|
Debug.LogWarning($"[Builder] Job {head.JobId} has unknown tower type " +
|
||||||
|
$"{head.TowerTypeId}. Dropping and refunding.");
|
||||||
|
ServerCancelHead(refund: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're already in range, kick off construction immediately. Otherwise
|
||||||
|
// walk toward the build-site center; we'll re-check range on subsequent
|
||||||
|
// ticks. The walk target is the footprint center even though we may stop
|
||||||
|
// before reaching it — Vector3.MoveTowards handles the early-stop naturally
|
||||||
|
// when the range check trips.
|
||||||
|
if (!IsTileWithinBuildRange(head.Anchor, def.FootprintSize))
|
||||||
|
{
|
||||||
|
Vector3 siteCenter = GridCoordinates.GetFootprintCenterWorld(
|
||||||
|
head.Anchor, def.FootprintSize);
|
||||||
|
targetPosition.Value = new Vector3(siteCenter.x, 0f, siteCenter.z);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In range. Stop here so the builder doesn't keep walking onto the
|
||||||
|
// build site after construction kicks off. Setting target = current
|
||||||
|
// position freezes the walk at this exact spot.
|
||||||
|
targetPosition.Value = new Vector3(
|
||||||
|
transform.position.x, 0f, transform.position.z);
|
||||||
|
|
||||||
|
// Path-revalidation: the rules differ for fresh-queued vs resume-from-paused.
|
||||||
|
//
|
||||||
|
// FRESH QUEUE (AccumulatedConstructionTime == 0): footprint is currently
|
||||||
|
// walkable (queue stage doesn't stamp it). Stamp it non-walkable, run BFS,
|
||||||
|
// un-stamp on failure.
|
||||||
|
//
|
||||||
|
// RESUME (AccumulatedConstructionTime > 0): footprint is ALREADY non-walkable
|
||||||
|
// (has been since the original Constructing transition; pause didn't restore
|
||||||
|
// walkability because the half-built tower keeps blocking enemies). Don't
|
||||||
|
// stamp anything — just verify the path still works given current grid state.
|
||||||
|
// Path failure on resume is essentially impossible (the path was valid before
|
||||||
|
// pause, and only this player can build in their zone), but the defensive
|
||||||
|
// check is here in case future mechanics change that.
|
||||||
|
var placingSlot = OwnerToSlot(OwnerClientId);
|
||||||
|
bool isResume = head.AccumulatedConstructionTime > 0f;
|
||||||
|
bool pathOk;
|
||||||
|
if (isResume)
|
||||||
|
{
|
||||||
|
pathOk = TowerPlacementManager.Instance != null
|
||||||
|
&& TowerPlacementManager.Instance.ServerVerifyPathStillValid(placingSlot);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pathOk = TowerPlacementManager.Instance != null
|
||||||
|
&& TowerPlacementManager.Instance.ServerCommitConstructionStart(
|
||||||
|
head.Anchor, def.FootprintSize, placingSlot);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pathOk)
|
||||||
|
{
|
||||||
|
// Path would now break. Refund and drop the job. The walkability
|
||||||
|
// rules differ between fresh-queue and resume:
|
||||||
|
// - Fresh queue: ServerCommitConstructionStart already rolled
|
||||||
|
// walkable=true, so the tile is in the same state as a normal
|
||||||
|
// Queued job (occupied=true, walkable=true). Just clear occupancy.
|
||||||
|
// - Resume: walkable was never stamped this go-round but the tile
|
||||||
|
// IS non-walkable from the original construction. We need to
|
||||||
|
// restore walkability since the tower is being fully removed.
|
||||||
|
Debug.Log($"[Builder] Job {head.JobId} dropped at construction-start: " +
|
||||||
|
$"path would break. Refunding {head.GoldSpent} gold.");
|
||||||
|
|
||||||
|
var loader = LevelLoader.Instance;
|
||||||
|
if (loader != null && loader.IsLoaded)
|
||||||
|
{
|
||||||
|
foreach (var tile in GridCoordinates.GetFootprintTiles(head.Anchor, def.FootprintSize))
|
||||||
|
{
|
||||||
|
loader.SetOccupied(tile, false);
|
||||||
|
if (isResume) loader.SetWalkable(tile, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RefundJobGold(head);
|
||||||
|
DespawnVisualAndForget(head.JobId);
|
||||||
|
jobs.RemoveAt(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path OK; transition to Constructing.
|
||||||
|
head.Stage = BuildStage.Constructing;
|
||||||
|
head.ConstructionStartServerTime = (float)NetworkManager.Singleton.ServerTime.Time;
|
||||||
|
jobs[0] = head;
|
||||||
|
|
||||||
|
// Tell the build-site visual to switch to the constructing animation.
|
||||||
|
// For a fresh transition AccumulatedConstructionTime is 0 (no previous progress);
|
||||||
|
// for a resume it carries the elapsed time from prior runs so the visual
|
||||||
|
// and the elapsed-time computation pick up where they left off.
|
||||||
|
if (jobIdToVisual.TryGetValue(head.JobId, out var visualObj) && visualObj != null)
|
||||||
|
{
|
||||||
|
var visual = visualObj.GetComponent<BuildSiteVisual>();
|
||||||
|
if (visual != null) visual.ServerBeginConstructing(head.AccumulatedConstructionTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// While the head job is Constructing: wait for buildTime to elapse, then
|
||||||
|
// promote the build-site to a real TowerInstance and dequeue.
|
||||||
|
private void DriveHead_Constructing(ref BuildJob head)
|
||||||
|
{
|
||||||
|
var def = TowerPlacementManager.GetDefinition(head.TowerTypeId);
|
||||||
|
if (def == null)
|
||||||
|
{
|
||||||
|
// Should be impossible — definitions are validated at queue-time.
|
||||||
|
Debug.LogError($"[Builder] Constructing job {head.JobId} has unknown " +
|
||||||
|
$"tower type {head.TowerTypeId}. Dropping.");
|
||||||
|
ServerCancelHead(refund: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total construction progress = elapsed in current run + previously accumulated
|
||||||
|
// (across prior pause/resume cycles). Pause sets ConstructionStartServerTime to -1
|
||||||
|
// so this code path only runs when the job is genuinely Constructing — but defensive
|
||||||
|
// check anyway.
|
||||||
|
float currentRunElapsed = head.ConstructionStartServerTime > 0f
|
||||||
|
? (float)NetworkManager.Singleton.ServerTime.Time - head.ConstructionStartServerTime
|
||||||
|
: 0f;
|
||||||
|
float total = currentRunElapsed + head.AccumulatedConstructionTime;
|
||||||
|
if (total < def.BuildTime) return;
|
||||||
|
|
||||||
|
// Construction complete. Spawn the real TowerInstance, despawn the visual,
|
||||||
|
// remove the job. The TowerInstance will stamp walkability=false / occupancy=true
|
||||||
|
// in its OnNetworkSpawn — but those bits are ALREADY non-walkable / occupied
|
||||||
|
// from the construction-start commit, so the stamp is a no-op-equivalent
|
||||||
|
// (idempotent writes; see TowerInstance.cs comments).
|
||||||
|
var placingSlot = OwnerToSlot(OwnerClientId);
|
||||||
|
TowerPlacementManager.Instance?.ServerSpawnCompletedTower(
|
||||||
|
def, head.Anchor, placingSlot);
|
||||||
|
|
||||||
|
// Despawn the build-site visual.
|
||||||
|
if (jobIdToVisual.TryGetValue(head.JobId, out var visualObj))
|
||||||
|
{
|
||||||
|
if (visualObj != null && visualObj.IsSpawned)
|
||||||
|
visualObj.Despawn(destroy: true);
|
||||||
|
jobIdToVisual.Remove(head.JobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pop the head.
|
||||||
|
jobs.RemoveAt(0);
|
||||||
|
|
||||||
|
Debug.Log($"[Builder] Job {head.JobId} complete at anchor {head.Anchor}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Cancellation API -------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owner-only Rpc: cancel every job in this builder's queue. Each cancellation
|
||||||
|
/// refunds 100% of the gold paid (per the design doc) and frees the affected
|
||||||
|
/// tiles. Builder stops at its current position.
|
||||||
|
/// </summary>
|
||||||
|
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)]
|
||||||
|
public void RequestCancelAllJobsRpc()
|
||||||
|
{
|
||||||
|
if (!IsServer) return;
|
||||||
|
ServerCancelAllJobs();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server-side: cancel every job in the queue. Public so other server code
|
||||||
|
/// (disconnect cleanup, future "level reset" events) can invoke it directly.
|
||||||
|
/// </summary>
|
||||||
|
public void ServerCancelAllJobs()
|
||||||
|
{
|
||||||
|
// Cancel in reverse so RemoveAt indices stay valid as we go.
|
||||||
|
while (jobs.Count > 0)
|
||||||
|
{
|
||||||
|
ServerCancelJobAt(jobs.Count - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop walking. The current position becomes the new target so the builder
|
||||||
|
// settles at the spot it cancelled at, rather than drifting toward the
|
||||||
|
// anchor of the (now-removed) head job.
|
||||||
|
targetPosition.Value = new Vector3(
|
||||||
|
transform.position.x, 0f, transform.position.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// PAUSE / SHELVE / RESUME (D2 — paused builds are shelved off the queue)
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owner-only Rpc: the player issued a "move to here" command (right-click
|
||||||
|
/// on empty buildable plane). Server applies the new move target and:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>If head is Constructing → pause + shelve the head. The visual
|
||||||
|
/// remains in the world as a shelved standalone object; the queue
|
||||||
|
/// loses this entry. Tail jobs are refunded.</item>
|
||||||
|
/// <item>If head is Queued → refund the entire queue (no progress to preserve).</item>
|
||||||
|
/// <item>Empty queue → just a move command. No queue effects.</item>
|
||||||
|
/// </list>
|
||||||
|
/// </summary>
|
||||||
|
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)]
|
||||||
|
public void RequestMoveAndPauseRpc(Vector3 worldPos)
|
||||||
|
{
|
||||||
|
if (!IsServer) return;
|
||||||
|
|
||||||
|
// Step 1: queue effects (if any).
|
||||||
|
if (jobs.Count > 0)
|
||||||
|
{
|
||||||
|
var head = jobs[0];
|
||||||
|
if (head.Stage == BuildStage.Constructing)
|
||||||
|
{
|
||||||
|
// Pause + shelve the head, then refund tail jobs.
|
||||||
|
ServerPauseAndShelveHead();
|
||||||
|
ServerRefundAllRemainingJobs();
|
||||||
|
}
|
||||||
|
else if (head.Stage == BuildStage.Queued)
|
||||||
|
{
|
||||||
|
// Head never reached Constructing — refund everything.
|
||||||
|
ServerCancelAllJobs();
|
||||||
|
}
|
||||||
|
// Note: Paused-in-queue is no longer a state we can be in (paused
|
||||||
|
// jobs are immediately shelved and removed from jobs[]). If we
|
||||||
|
// somehow get here, treat it like Constructing for safety.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: apply the move target.
|
||||||
|
ServerSetMoveTarget(worldPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owner-only Rpc: the player right-clicked a shelved build site to resume it.
|
||||||
|
/// Server pre-empts the current queue (shelves any active head, refunds all
|
||||||
|
/// remaining jobs), then unshelves the clicked visual into a new BuildJob at
|
||||||
|
/// index 0. Builder will walk back into range and resume construction with
|
||||||
|
/// preserved accumulated time.
|
||||||
|
/// </summary>
|
||||||
|
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)]
|
||||||
|
public void RequestResumeShelvedRpc(NetworkObjectReference visualRef)
|
||||||
|
{
|
||||||
|
if (!IsServer) return;
|
||||||
|
|
||||||
|
// Resolve the visual NetworkObject from the reference.
|
||||||
|
if (!visualRef.TryGet(out NetworkObject visualNetObj) || visualNetObj == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[Builder] RequestResumeShelvedRpc: visual not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var visual = visualNetObj.GetComponent<BuildSiteVisual>();
|
||||||
|
if (visual == null || !visual.IsShelved)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[Builder] RequestResumeShelvedRpc: visual is not shelved.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (visualNetObj.OwnerClientId != OwnerClientId)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[Builder] RequestResumeShelvedRpc: visual owner " +
|
||||||
|
$"{visualNetObj.OwnerClientId} != builder owner {OwnerClientId}.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: pre-empt the current queue.
|
||||||
|
// - If head is Constructing: pause+shelve it (it becomes a new shelved tower).
|
||||||
|
// - Otherwise (Queued head, or empty): refund everything.
|
||||||
|
// The clicked visual is NOT in the queue (it's shelved), so this loop
|
||||||
|
// doesn't touch it.
|
||||||
|
if (jobs.Count > 0)
|
||||||
|
{
|
||||||
|
var head = jobs[0];
|
||||||
|
if (head.Stage == BuildStage.Constructing)
|
||||||
|
{
|
||||||
|
ServerPauseAndShelveHead();
|
||||||
|
ServerRefundAllRemainingJobs();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ServerCancelAllJobs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: unshelve the clicked visual. Reconstruct a BuildJob from its
|
||||||
|
// NetworkVariables, push it as the new head of the queue, and re-register
|
||||||
|
// it in jobIdToVisual so the queue driver finds it.
|
||||||
|
ulong jobId = nextJobId++;
|
||||||
|
var job = BuildJob.CreateQueued(
|
||||||
|
jobId,
|
||||||
|
visual.Anchor,
|
||||||
|
visual.TowerTypeId,
|
||||||
|
visual.GoldSpent);
|
||||||
|
// Carry the accumulated time forward — the resume keeps prior progress.
|
||||||
|
job.AccumulatedConstructionTime = visual.AccumulatedConstructionTime;
|
||||||
|
jobs.Add(job);
|
||||||
|
|
||||||
|
jobIdToVisual[jobId] = visualNetObj;
|
||||||
|
visual.ServerMarkUnshelved();
|
||||||
|
|
||||||
|
// Note: The visual's currentStage is still Paused; the queue driver's
|
||||||
|
// DriveHead_Queued will call ServerBeginConstructing once the builder
|
||||||
|
// arrives, which transitions it to Constructing.
|
||||||
|
|
||||||
|
Debug.Log($"[Builder] Resumed shelved tower at {visual.Anchor} as job " +
|
||||||
|
$"{jobId} (accumulated {visual.AccumulatedConstructionTime:F2}s).");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Pause/shelve/refund helpers --------------------------------
|
||||||
|
|
||||||
|
// Pauses + shelves the head job: Constructing → Paused, then removes from
|
||||||
|
// jobs[] and jobIdToVisual. The visual stays in the world as a standalone
|
||||||
|
// shelved object. Player can later right-click it to resume.
|
||||||
|
private void ServerPauseAndShelveHead()
|
||||||
|
{
|
||||||
|
if (jobs.Count == 0) return;
|
||||||
|
var head = jobs[0];
|
||||||
|
if (head.Stage != BuildStage.Constructing) return;
|
||||||
|
|
||||||
|
// Compute current run's elapsed and add to accumulated.
|
||||||
|
float currentRunElapsed = head.ConstructionStartServerTime > 0f
|
||||||
|
? (float)NetworkManager.Singleton.ServerTime.Time - head.ConstructionStartServerTime
|
||||||
|
: 0f;
|
||||||
|
float totalAccumulated = head.AccumulatedConstructionTime + currentRunElapsed;
|
||||||
|
|
||||||
|
// Tell the visual to enter Paused state, write the accumulated time,
|
||||||
|
// and mark itself as shelved. After this, the visual carries all the
|
||||||
|
// state that used to live on the BuildJob.
|
||||||
|
if (jobIdToVisual.TryGetValue(head.JobId, out var visualObj) && visualObj != null)
|
||||||
|
{
|
||||||
|
var visual = visualObj.GetComponent<BuildSiteVisual>();
|
||||||
|
if (visual != null)
|
||||||
|
{
|
||||||
|
visual.ServerPauseAndPersistAccumulated(totalAccumulated);
|
||||||
|
visual.ServerMarkShelved();
|
||||||
|
}
|
||||||
|
// Remove from our visual tracking — the visual is now self-managing.
|
||||||
|
jobIdToVisual.Remove(head.JobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pop the head from the queue. The visual stays in the world.
|
||||||
|
jobs.RemoveAt(0);
|
||||||
|
|
||||||
|
Debug.Log($"[Builder] Job {head.JobId} paused and shelved at " +
|
||||||
|
$"{totalAccumulated:F2}s of construction.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refunds and removes EVERY job in the queue. Used after pause+shelve to
|
||||||
|
// clear out tail jobs (the shelved head is no longer in jobs[] at this point).
|
||||||
|
// Same as ServerCancelAllJobs but doesn't reset targetPosition (caller does that
|
||||||
|
// by setting an explicit move target afterward).
|
||||||
|
private void ServerRefundAllRemainingJobs()
|
||||||
|
{
|
||||||
|
for (int i = jobs.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
ServerCancelJobAt(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Public state accessor (read by the input controller) -------
|
||||||
|
|
||||||
|
// Note: PausedHeadJobId was removed when paused jobs moved out of the queue
|
||||||
|
// entirely. The input controller now uses the BuildSiteVisual's own state
|
||||||
|
// (visual.IsShelved + ownership) to determine resumeability and sends the
|
||||||
|
// visual's NetworkObjectReference to RequestResumeShelvedRpc.
|
||||||
|
|
||||||
|
// Cancels the head job specifically. Used for the "definition lost" defensive
|
||||||
|
// cases where the TowerDefinition can't be resolved. Restores walkability
|
||||||
|
// appropriately based on the current stage of the job being cancelled.
|
||||||
|
private void ServerCancelHead(bool refund)
|
||||||
|
{
|
||||||
|
if (jobs.Count == 0) return;
|
||||||
|
|
||||||
|
var head = jobs[0];
|
||||||
|
var def = TowerPlacementManager.GetDefinition(head.TowerTypeId);
|
||||||
|
if (def != null)
|
||||||
|
{
|
||||||
|
// Constructing or Paused → tile is non-walkable, restore it.
|
||||||
|
// Queued → tile was never made non-walkable, leave alone.
|
||||||
|
bool wasBlocking = head.Stage == BuildStage.Constructing
|
||||||
|
|| head.Stage == BuildStage.Paused;
|
||||||
|
FreeFootprintTiles(head.Anchor, def.FootprintSize,
|
||||||
|
alsoMakeWalkable: wasBlocking);
|
||||||
|
}
|
||||||
|
if (refund) RefundJobGold(head);
|
||||||
|
DespawnVisualAndForget(head.JobId);
|
||||||
|
jobs.RemoveAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancels the job at index i. Used for cancel-all and any future targeted cancel.
|
||||||
|
private void ServerCancelJobAt(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= jobs.Count) return;
|
||||||
|
|
||||||
|
var job = jobs[index];
|
||||||
|
var def = TowerPlacementManager.GetDefinition(job.TowerTypeId);
|
||||||
|
if (def != null)
|
||||||
|
{
|
||||||
|
// Walkability state by stage:
|
||||||
|
// - Queued: walkable (queue stage doesn't stamp walkability) → don't touch.
|
||||||
|
// - Constructing: non-walkable (stamped at construction-start) → restore.
|
||||||
|
// - Paused: non-walkable (stamped at original construction-start, never
|
||||||
|
// reverted because the half-built tower keeps blocking) → restore.
|
||||||
|
bool wasBlocking = job.Stage == BuildStage.Constructing
|
||||||
|
|| job.Stage == BuildStage.Paused;
|
||||||
|
FreeFootprintTiles(job.Anchor, def.FootprintSize,
|
||||||
|
alsoMakeWalkable: wasBlocking);
|
||||||
|
}
|
||||||
|
|
||||||
|
RefundJobGold(job);
|
||||||
|
DespawnVisualAndForget(job.JobId);
|
||||||
|
jobs.RemoveAt(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FreeFootprintTiles(Vector2Int anchor, Vector2Int footprint,
|
||||||
|
bool alsoMakeWalkable)
|
||||||
|
{
|
||||||
|
var loader = LevelLoader.Instance;
|
||||||
|
if (loader == null || !loader.IsLoaded) return;
|
||||||
|
|
||||||
|
foreach (var t in GridCoordinates.GetFootprintTiles(anchor, footprint))
|
||||||
|
{
|
||||||
|
loader.SetOccupied(t, false);
|
||||||
|
if (alsoMakeWalkable) loader.SetWalkable(t, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefundJobGold(BuildJob job)
|
||||||
|
{
|
||||||
|
var goldManager = PlayerGoldManager.GetForClient(OwnerClientId);
|
||||||
|
if (goldManager == null) return;
|
||||||
|
goldManager.AwardGold(job.GoldSpent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DespawnVisualAndForget(ulong jobId)
|
||||||
|
{
|
||||||
|
if (!jobIdToVisual.TryGetValue(jobId, out var visualObj)) return;
|
||||||
|
if (visualObj != null && visualObj.IsSpawned)
|
||||||
|
visualObj.Despawn(destroy: true);
|
||||||
|
jobIdToVisual.Remove(jobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Helpers ----------------------------------------------------
|
||||||
|
|
||||||
|
private static PlayerSlot OwnerToSlot(ulong clientId)
|
||||||
|
{
|
||||||
|
// STUB — replaced when MatchState lands. Same mapping as elsewhere.
|
||||||
|
byte slotByte = (byte)(clientId + 1);
|
||||||
|
if (slotByte < 1 || slotByte > 9) return PlayerSlot.None;
|
||||||
|
return (PlayerSlot)slotByte;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector2Int ResolveFootprint(int towerTypeId)
|
||||||
|
{
|
||||||
|
var def = TowerPlacementManager.GetDefinition(towerTypeId);
|
||||||
|
return def != null ? def.FootprintSize : new Vector2Int(2, 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7,22 +7,48 @@ using TD.Core;
|
||||||
namespace TD.Gameplay
|
namespace TD.Gameplay
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Owner-only client-side controller for builder input. Handles right-click-to-move,
|
/// Owner-only client-side controller for builder input. Handles selection,
|
||||||
/// deferring to placement mode (right-click cancels placement instead).
|
/// right-click-to-move (with side-effects: pause active construction and
|
||||||
|
/// refund tail jobs), right-click-on-paused-build-site (resume), and
|
||||||
|
/// Escape-to-deselect.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para><b>Owner-only.</b> This component lives on the same GameObject as
|
/// <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
|
/// <see cref="Builder"/> but only its owning client processes input. Non-owner
|
||||||
/// have this component but its Update is a no-op. The owner sends move requests via
|
/// clients have this component but its Update is a no-op.</para>
|
||||||
/// <see cref="Builder.RequestMoveRpc"/>.</para>
|
|
||||||
///
|
///
|
||||||
/// <para><b>Right-click priority.</b> If <c>TowerPlacementController.IsPlacing</c> is
|
/// <para><b>Selection model.</b> Left-click raycast against the
|
||||||
/// true, right-click cancels placement (handled by <c>TowerPlacementController</c>
|
/// <see cref="selectionLayerMask"/> (the builder's selection trigger collider
|
||||||
/// itself). When NOT placing, right-click moves the builder.</para>
|
/// sits on this layer). Hitting the local builder selects it; hitting empty
|
||||||
|
/// space or another collider clears the selection. Selection lives in the
|
||||||
|
/// singleton <see cref="SelectionState"/>.</para>
|
||||||
///
|
///
|
||||||
/// <para><b>Raycast target.</b> The cursor is raycast against the BuildablePlane layer
|
/// <para><b>Right-click — selection required.</b> Without selection, right-click
|
||||||
/// (same as placement). The hit point's XZ is sent as the target; Y is recomputed by
|
/// is a no-op. With selection:
|
||||||
/// the server via terrain raycast.</para>
|
/// <list type="bullet">
|
||||||
|
/// <item>Right-click on the local builder's PAUSED build site (raycast hits
|
||||||
|
/// <see cref="buildSiteLayerMask"/>): resume that job. Builder walks back
|
||||||
|
/// into range; preserved progress picks up where it left off.</item>
|
||||||
|
/// <item>Right-click anywhere else on the buildable plane: move-and-pause.
|
||||||
|
/// Builder walks to the clicked location. Side effects:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>If currently Constructing → pause the active build, refund
|
||||||
|
/// tail jobs.</item>
|
||||||
|
/// <item>If head is Queued → refund the entire queue.</item>
|
||||||
|
/// <item>If head is Paused → no queue change (tail was already
|
||||||
|
/// refunded at pause time).</item>
|
||||||
|
/// </list>
|
||||||
|
/// </item>
|
||||||
|
/// </list></para>
|
||||||
|
///
|
||||||
|
/// <para><b>Escape.</b> Clears selection. No queue cancellation in D2 — that's
|
||||||
|
/// deferred to the HUD path.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Placement mode interaction.</b> If the local TowerPlacementController
|
||||||
|
/// reports IsPlacing == true, all input handled here yields to it (placement-mode
|
||||||
|
/// right-click cancels placement, placement-mode left-click submits). Selection
|
||||||
|
/// raycasts are also suppressed so a placement click doesn't accidentally
|
||||||
|
/// deselect the builder.</para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public class BuilderInputController : NetworkBehaviour
|
public class BuilderInputController : NetworkBehaviour
|
||||||
{
|
{
|
||||||
|
|
@ -32,6 +58,18 @@ namespace TD.Gameplay
|
||||||
"against this layer to determine the move target.")]
|
"against this layer to determine the move target.")]
|
||||||
[SerializeField] private LayerMask buildablePlaneLayerMask;
|
[SerializeField] private LayerMask buildablePlaneLayerMask;
|
||||||
|
|
||||||
|
[Tooltip("Physics layer mask for the builder selection trigger collider. The " +
|
||||||
|
"builder prefab's child selection collider sits on this layer. The mask " +
|
||||||
|
"must NOT overlap with BuildablePlane or TerrainGeometry; selection is " +
|
||||||
|
"a separate concern.")]
|
||||||
|
[SerializeField] private LayerMask selectionLayerMask;
|
||||||
|
|
||||||
|
[Tooltip("Physics layer mask for build-site visual click targets. The " +
|
||||||
|
"BuildSiteVisual prefab carries a small trigger collider on this layer " +
|
||||||
|
"so right-click can identify a paused build site for resume. Must NOT " +
|
||||||
|
"overlap with BuildablePlane, Selection, or TerrainGeometry.")]
|
||||||
|
[SerializeField] private LayerMask buildSiteLayerMask;
|
||||||
|
|
||||||
[Tooltip("Maximum raycast distance for cursor → world conversion.")]
|
[Tooltip("Maximum raycast distance for cursor → world conversion.")]
|
||||||
[SerializeField] private float raycastMaxDistance = 500f;
|
[SerializeField] private float raycastMaxDistance = 500f;
|
||||||
|
|
||||||
|
|
@ -71,21 +109,113 @@ namespace TD.Gameplay
|
||||||
if (!IsOwner) return;
|
if (!IsOwner) return;
|
||||||
|
|
||||||
var mouse = Mouse.current;
|
var mouse = Mouse.current;
|
||||||
|
var keyboard = Keyboard.current;
|
||||||
if (mouse == null) return;
|
if (mouse == null) return;
|
||||||
|
|
||||||
|
bool isPlacing = IsLocalPlayerPlacing();
|
||||||
|
|
||||||
|
// Left-click: selection. Suppressed during placement mode (left-click is
|
||||||
|
// the placement-submit gesture there).
|
||||||
|
if (!isPlacing && mouse.leftButton.wasPressedThisFrame)
|
||||||
|
{
|
||||||
|
HandleLeftClickSelection(mouse.position.ReadValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape: clear selection. Allowed during placement mode too — Escape never
|
||||||
|
// means anything else here, and clearing selection during placement is fine.
|
||||||
|
if (keyboard != null && keyboard.escapeKey.wasPressedThisFrame)
|
||||||
|
{
|
||||||
|
SelectionState.Instance?.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right-click. Suppressed entirely during placement mode (TowerPlacementController
|
||||||
|
// handles right-click as cancel-placement there).
|
||||||
|
if (isPlacing) return;
|
||||||
if (!mouse.rightButton.wasPressedThisFrame) return;
|
if (!mouse.rightButton.wasPressedThisFrame) return;
|
||||||
|
|
||||||
// Defer to placement mode: if the player is placing a tower, right-click cancels
|
HandleRightClick(mouse.position.ReadValue());
|
||||||
// placement rather than moving the builder. TowerPlacementController handles
|
}
|
||||||
// the cancel itself; we just don't process the click here.
|
|
||||||
if (IsLocalPlayerPlacing()) return;
|
|
||||||
|
|
||||||
// Cursor → world.
|
// ----- Selection (left-click) -------------------------------------
|
||||||
if (!TryGetBuildablePlaneHit(mouse.position.ReadValue(), out Vector3 worldPoint))
|
|
||||||
|
private void HandleLeftClickSelection(Vector2 screenPos)
|
||||||
|
{
|
||||||
|
var selection = SelectionState.Instance;
|
||||||
|
if (selection == null) return;
|
||||||
|
|
||||||
|
var cam = Camera.main;
|
||||||
|
if (cam == null) return;
|
||||||
|
|
||||||
|
Ray ray = cam.ScreenPointToRay(new Vector3(screenPos.x, screenPos.y, 0f));
|
||||||
|
if (Physics.Raycast(ray, out RaycastHit hit, raycastMaxDistance, selectionLayerMask))
|
||||||
|
{
|
||||||
|
// Walk up the hierarchy to find a Builder component (the selection
|
||||||
|
// collider may sit on a child of the Builder's root).
|
||||||
|
var hitBuilder = hit.collider.GetComponentInParent<Builder>();
|
||||||
|
|
||||||
|
// Only allow selecting OUR builder. A click on someone else's builder
|
||||||
|
// collider clears our selection rather than selecting theirs.
|
||||||
|
if (hitBuilder != null && hitBuilder == builder)
|
||||||
|
{
|
||||||
|
selection.Select(builder);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clicked empty space or a non-selectable thing — clear selection.
|
||||||
|
selection.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Right-click dispatch ---------------------------------------
|
||||||
|
|
||||||
|
private void HandleRightClick(Vector2 screenPos)
|
||||||
|
{
|
||||||
|
// Right-click is gated by selection: nothing happens unless the local
|
||||||
|
// builder is the selected one.
|
||||||
|
var selection = SelectionState.Instance;
|
||||||
|
if (selection == null || !selection.IsSelected(builder)) return;
|
||||||
|
|
||||||
|
var cam = Camera.main;
|
||||||
|
if (cam == null) return;
|
||||||
|
Ray ray = cam.ScreenPointToRay(new Vector3(screenPos.x, screenPos.y, 0f));
|
||||||
|
|
||||||
|
// Test 1: did we hit a build-site visual? If so, and it belongs to OUR
|
||||||
|
// builder, AND it's currently shelved, this is a resume command.
|
||||||
|
// The BuildSite raycast is checked first because the build-site visual
|
||||||
|
// sits ABOVE the buildable plane visually; a click on the cube should
|
||||||
|
// be interpreted as a build-site click, not a "move to under the cube" click.
|
||||||
|
if (Physics.Raycast(ray, out RaycastHit buildSiteHit, raycastMaxDistance,
|
||||||
|
buildSiteLayerMask))
|
||||||
|
{
|
||||||
|
var visual = buildSiteHit.collider.GetComponentInParent<BuildSiteVisual>();
|
||||||
|
if (visual != null
|
||||||
|
&& visual.IsShelved
|
||||||
|
&& visual.OwnerClientId == NetworkManager.Singleton.LocalClientId)
|
||||||
|
{
|
||||||
|
// Resume command. Send the visual's NetworkObjectReference so the
|
||||||
|
// server can identify which shelved tower the player clicked.
|
||||||
|
// The server handles pre-empting any in-progress queue work.
|
||||||
|
var visualNetObj = visual.GetComponent<NetworkObject>();
|
||||||
|
if (visualNetObj != null)
|
||||||
|
{
|
||||||
|
builder.RequestResumeShelvedRpc(new NetworkObjectReference(visualNetObj));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Hit a build-site visual that wasn't shelved-and-ours. Fall through
|
||||||
|
// to the move case so the click still does something useful (move
|
||||||
|
// the builder to that approximate world position).
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2: hit the buildable plane? Standard move-and-pause command.
|
||||||
|
if (Physics.Raycast(ray, out RaycastHit planeHit, raycastMaxDistance,
|
||||||
|
buildablePlaneLayerMask))
|
||||||
|
{
|
||||||
|
builder.RequestMoveAndPauseRpc(planeHit.point);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Submit to server.
|
// Hit nothing relevant — no-op.
|
||||||
builder.RequestMoveRpc(worldPoint);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- Helpers ----------------------------------------------------
|
// ----- Helpers ----------------------------------------------------
|
||||||
|
|
@ -101,21 +231,5 @@ namespace TD.Gameplay
|
||||||
}
|
}
|
||||||
return cachedPlacementController.IsPlacing;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
126
Assets/_Project/Scripts/Gameplay/SelectionRingVisual.cs
Normal file
126
Assets/_Project/Scripts/Gameplay/SelectionRingVisual.cs
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
// Assets/_Project/Scripts/Gameplay/SelectionRingVisual.cs
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace TD.Gameplay
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Local-only visual indicator for builder selection. Sits as a child of the
|
||||||
|
/// builder prefab. Subscribes to <see cref="SelectionState.OnSelectionChanged"/>
|
||||||
|
/// and toggles the visibility of its own renderers (and any descendant
|
||||||
|
/// renderers) when the parent builder becomes selected/unselected.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para><b>Pure local visualization.</b> No NetworkBehaviour. Selection is a
|
||||||
|
/// UI concept — every client renders selection state for its own player only.
|
||||||
|
/// This component has no networked state.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Why a separate component.</b> Keeps Builder focused on gameplay
|
||||||
|
/// state. The visual prefab structure can change independently
|
||||||
|
/// (disc → ring → decal projector → animated effect) without touching gameplay
|
||||||
|
/// code. The contract is just "renderers visible when selected."</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Why renderer-toggle, not GameObject.SetActive.</b> Disabling our
|
||||||
|
/// own GameObject would prevent OnEnable/Update from running, which would
|
||||||
|
/// break the SelectionState subscription lifecycle (we'd never receive the
|
||||||
|
/// "you're selected again" event). Toggling the renderers' enabled state
|
||||||
|
/// achieves the same visual effect without breaking the event flow.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Prefab setup.</b> Attach this component to a child GameObject of
|
||||||
|
/// the Builder prefab. The child carries (or has descendants carrying) a
|
||||||
|
/// flattened cylinder mesh sitting just above the ground plane, with an
|
||||||
|
/// unlit transparent green material. The component handles initial visibility
|
||||||
|
/// — leave the renderer enabled in the prefab; we'll turn it off in Awake
|
||||||
|
/// until selection fires.</para>
|
||||||
|
/// </remarks>
|
||||||
|
public class SelectionRingVisual : MonoBehaviour
|
||||||
|
{
|
||||||
|
// Cached parent builder, resolved in Awake.
|
||||||
|
private Builder parentBuilder;
|
||||||
|
|
||||||
|
// Cached renderers (this object plus all descendants). Captured once in
|
||||||
|
// Awake to avoid per-event GetComponentsInChildren allocations.
|
||||||
|
private Renderer[] cachedRenderers;
|
||||||
|
|
||||||
|
// Tracks subscription state so OnDisable / OnDestroy unsubscribe correctly,
|
||||||
|
// and so Update can retry subscription if SelectionState wasn't ready at OnEnable.
|
||||||
|
private bool subscribed;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
parentBuilder = GetComponentInParent<Builder>();
|
||||||
|
if (parentBuilder == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[SelectionRingVisual] No Builder component found on " +
|
||||||
|
"self or any parent. Disabling.");
|
||||||
|
enabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedRenderers = GetComponentsInChildren<Renderer>(includeInactive: true);
|
||||||
|
|
||||||
|
// Start hidden. If selection fires later (including the auto-select
|
||||||
|
// in Builder.OnNetworkSpawn), HandleSelectionChanged will turn the
|
||||||
|
// renderers back on.
|
||||||
|
SetRenderersVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
TrySubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
Unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
Unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-frame fallback: if SelectionState wasn't available at OnEnable
|
||||||
|
// (scene load ordering), keep trying. Cost is one null-check per frame
|
||||||
|
// until subscription succeeds, then nothing.
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (!subscribed) TrySubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TrySubscribe()
|
||||||
|
{
|
||||||
|
if (subscribed) return;
|
||||||
|
var sel = SelectionState.Instance;
|
||||||
|
if (sel == null) return;
|
||||||
|
|
||||||
|
sel.OnSelectionChanged += HandleSelectionChanged;
|
||||||
|
subscribed = true;
|
||||||
|
|
||||||
|
// Sync to current state — selection may have happened before we subscribed
|
||||||
|
// (e.g., Builder.OnNetworkSpawn auto-selecting before this Awake runs).
|
||||||
|
HandleSelectionChanged(sel.SelectedBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Unsubscribe()
|
||||||
|
{
|
||||||
|
if (!subscribed) return;
|
||||||
|
var sel = SelectionState.Instance;
|
||||||
|
if (sel != null) sel.OnSelectionChanged -= HandleSelectionChanged;
|
||||||
|
subscribed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleSelectionChanged(Builder newSelection)
|
||||||
|
{
|
||||||
|
SetRenderersVisible(newSelection == parentBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetRenderersVisible(bool visible)
|
||||||
|
{
|
||||||
|
if (cachedRenderers == null) return;
|
||||||
|
foreach (var rend in cachedRenderers)
|
||||||
|
{
|
||||||
|
if (rend != null) rend.enabled = visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 67895f626233fdc499dffbbfcc225530
|
||||||
90
Assets/_Project/Scripts/Gameplay/SelectionState.cs
Normal file
90
Assets/_Project/Scripts/Gameplay/SelectionState.cs
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
// Assets/_Project/Scripts/Gameplay/SelectionState.cs
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace TD.Gameplay
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal scene-local selection state. Holds a reference to whichever
|
||||||
|
/// <see cref="Builder"/> the local player has selected, fires an event when
|
||||||
|
/// the selection changes, and exposes a query for "is this builder selected
|
||||||
|
/// right now?".
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para><b>Scope.</b> D2 only needs this for: "Escape with builder selected
|
||||||
|
/// cancels its queue", and "right-click with builder selected and queue
|
||||||
|
/// established cancels the queue (the right-click is consumed by selection
|
||||||
|
/// instead of issuing a move)". A full selection system that supports world
|
||||||
|
/// highlighting, multi-select, and HUD context panels is deferred to the HUD
|
||||||
|
/// path.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Local-only.</b> Selection is a UI concept, not a gameplay one.
|
||||||
|
/// Other clients have no business knowing whether you've selected your own
|
||||||
|
/// builder. The component is a plain MonoBehaviour and lives in the scene
|
||||||
|
/// alongside other client-side controllers.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Singleton.</b> One per scene, accessed via <see cref="Instance"/>.
|
||||||
|
/// The selection consumer (BuilderInputController) and the selection driver
|
||||||
|
/// (mouse-click raycast) both go through this single source of truth.</para>
|
||||||
|
/// </remarks>
|
||||||
|
public class SelectionState : MonoBehaviour
|
||||||
|
{
|
||||||
|
// ----- Singleton --------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The active SelectionState. Null before the scene loads. Always null-check.
|
||||||
|
/// </summary>
|
||||||
|
public static SelectionState Instance { get; private set; }
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (Instance != null && Instance != this)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[SelectionState] Multiple instances detected. " +
|
||||||
|
"Only one SelectionState should exist per scene.");
|
||||||
|
}
|
||||||
|
Instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
if (Instance == this) Instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Selection state --------------------------------------------
|
||||||
|
|
||||||
|
private Builder selectedBuilder;
|
||||||
|
|
||||||
|
/// <summary>The currently selected builder, or null if nothing is selected.</summary>
|
||||||
|
public Builder SelectedBuilder => selectedBuilder;
|
||||||
|
|
||||||
|
/// <summary>True if any builder is currently selected.</summary>
|
||||||
|
public bool HasSelection => selectedBuilder != null;
|
||||||
|
|
||||||
|
/// <summary>True if <paramref name="b"/> is the currently selected builder.</summary>
|
||||||
|
public bool IsSelected(Builder b) => b != null && selectedBuilder == b;
|
||||||
|
|
||||||
|
// ----- Events -----------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when the selection changes. Argument is the new selection (may be null).
|
||||||
|
/// Subscribe to drive selection-aware UI: highlights, context panels, hotkey hints.
|
||||||
|
/// </summary>
|
||||||
|
public event System.Action<Builder> OnSelectionChanged;
|
||||||
|
|
||||||
|
// ----- Mutators ---------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the selected builder. Pass null to clear.
|
||||||
|
/// Fires <see cref="OnSelectionChanged"/> only if the selection actually changes.
|
||||||
|
/// </summary>
|
||||||
|
public void Select(Builder builder)
|
||||||
|
{
|
||||||
|
if (selectedBuilder == builder) return;
|
||||||
|
selectedBuilder = builder;
|
||||||
|
OnSelectionChanged?.Invoke(selectedBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Clears the selection. Equivalent to Select(null).</summary>
|
||||||
|
public void Clear() => Select(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Scripts/Gameplay/SelectionState.cs.meta
Normal file
2
Assets/_Project/Scripts/Gameplay/SelectionState.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: dc307e7e94967894584e8e6050fc38cf
|
||||||
|
|
@ -12,33 +12,29 @@ namespace TD.Gameplay
|
||||||
/// requests to <see cref="TowerPlacementManager"/> via RPC.
|
/// requests to <see cref="TowerPlacementManager"/> via RPC.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para><b>Plain MonoBehaviour.</b> Placement visuals (ghost, cursor color) are
|
/// <para><b>Plain MonoBehaviour.</b> Placement visuals (cursor ghost, color) are
|
||||||
/// purely cosmetic and local. This component does not need to be a NetworkBehaviour.
|
/// purely cosmetic and local. This component does not need to be a NetworkBehaviour.
|
||||||
/// All server-authoritative logic lives in <see cref="TowerPlacementManager"/>.</para>
|
/// All server-authoritative logic lives in <see cref="TowerPlacementManager"/>.</para>
|
||||||
///
|
///
|
||||||
/// <para><b>Ghost validity check.</b> The ghost checks ownership, placement state,
|
/// <para><b>Ghost validity check.</b> The cursor ghost checks ownership, placement
|
||||||
/// and tile occupancy only. It does NOT run a local path check. If the server rejects
|
/// state, and tile occupancy only. It does NOT run a local path check. If the server
|
||||||
/// because the tower would block the path, the ghost disappears and a rejection
|
/// rejects because the tower would block the path, the cursor ghost disappears and a
|
||||||
/// message is shown. This avoids the complexity of maintaining a client-side BFS
|
/// rejection message is shown.</para>
|
||||||
/// that may be slightly stale.</para>
|
|
||||||
///
|
///
|
||||||
/// <para><b>Ghost colors.</b>
|
/// <para><b>Two ghosts, two systems.</b> The <i>cursor ghost</i> (handled here)
|
||||||
/// <list type="bullet">
|
/// follows the mouse and turns white/red. The <i>build-site visual</i> (in D2,
|
||||||
/// <item>White — all local checks pass.</item>
|
/// handled by Builder + BuildSiteVisual) is the green queued ghost or staged
|
||||||
/// <item>Red — any local check fails (wrong zone, not buildable, already occupied).</item>
|
/// construction visual that appears at a confirmed placement site. They are
|
||||||
/// </list>
|
/// distinct prefabs and lifecycles.</para>
|
||||||
/// Green "pending construction" ghost is a separate system implemented in Path D.</para>
|
|
||||||
///
|
///
|
||||||
/// <para><b>Placement activation.</b> The controller is idle until
|
/// <para><b>Chained queueing.</b> Holding Shift while left-clicking submits the
|
||||||
/// <see cref="BeginPlacement"/> is called (e.g., from a HUD tower button). The player
|
/// placement and stays in placement mode for another submission. Releasing Shift
|
||||||
/// right-clicks or the placement is confirmed/rejected to return to idle.</para>
|
/// before the click submits and exits placement (single-shot). Right-click always
|
||||||
|
/// cancels.</para>
|
||||||
///
|
///
|
||||||
/// <para><b>Input System.</b> Uses the New Input System package. Mouse position and
|
/// <para><b>Input System.</b> Uses the New Input System package. Mouse and modifier
|
||||||
/// button state are read from <c>Mouse.current</c> each frame.</para>
|
/// state are read directly from <c>Mouse.current</c> and <c>Keyboard.current</c>
|
||||||
///
|
/// each frame. No InputAction asset needed for these placement-specific bindings.</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>
|
/// </remarks>
|
||||||
public class TowerPlacementController : MonoBehaviour
|
public class TowerPlacementController : MonoBehaviour
|
||||||
{
|
{
|
||||||
|
|
@ -62,7 +58,7 @@ namespace TD.Gameplay
|
||||||
private TowerDefinition activeDef;
|
private TowerDefinition activeDef;
|
||||||
private int activeTowerTypeId;
|
private int activeTowerTypeId;
|
||||||
|
|
||||||
// The ghost GameObject: the tower prefab instantiated with transparent materials.
|
// The cursor ghost GameObject: the tower prefab instantiated with transparent materials.
|
||||||
// Null when placement mode is inactive.
|
// Null when placement mode is inactive.
|
||||||
private GameObject ghostGO;
|
private GameObject ghostGO;
|
||||||
|
|
||||||
|
|
@ -131,7 +127,7 @@ namespace TD.Gameplay
|
||||||
// Compute the footprint anchor from the hit point.
|
// Compute the footprint anchor from the hit point.
|
||||||
Vector2Int anchor = ComputeAnchor(hitPoint, activeDef.FootprintSize);
|
Vector2Int anchor = ComputeAnchor(hitPoint, activeDef.FootprintSize);
|
||||||
|
|
||||||
// Position the ghost at the footprint center.
|
// Position the cursor ghost at the footprint center.
|
||||||
Vector3 ghostPos = GridCoordinates.GetFootprintCenterWorld(anchor, activeDef.FootprintSize);
|
Vector3 ghostPos = GridCoordinates.GetFootprintCenterWorld(anchor, activeDef.FootprintSize);
|
||||||
ghostPos.y = 0.5f; // lift off the plane so the cube base sits flush
|
ghostPos.y = 0.5f; // lift off the plane so the cube base sits flush
|
||||||
ghostGO.transform.position = ghostPos;
|
ghostGO.transform.position = ghostPos;
|
||||||
|
|
@ -151,7 +147,8 @@ namespace TD.Gameplay
|
||||||
// Left-click to attempt placement.
|
// Left-click to attempt placement.
|
||||||
if (mouse.leftButton.wasPressedThisFrame)
|
if (mouse.leftButton.wasPressedThisFrame)
|
||||||
{
|
{
|
||||||
TrySubmitPlacement(anchor);
|
bool chained = IsShiftHeld();
|
||||||
|
TrySubmitPlacement(anchor, chained);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,9 +158,6 @@ namespace TD.Gameplay
|
||||||
/// Activates placement mode for the given tower type. The ghost appears
|
/// Activates placement mode for the given tower type. The ghost appears
|
||||||
/// immediately under the cursor. Call this from HUD tower buttons.
|
/// immediately under the cursor. Call this from HUD tower buttons.
|
||||||
/// </summary>
|
/// </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)
|
public void BeginPlacement(TowerDefinition def, int towerTypeId)
|
||||||
{
|
{
|
||||||
if (def == null)
|
if (def == null)
|
||||||
|
|
@ -199,6 +193,15 @@ namespace TD.Gameplay
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsPlacing => activeDef != null;
|
public bool IsPlacing => activeDef != null;
|
||||||
|
|
||||||
|
// ----- Modifier helpers -------------------------------------------
|
||||||
|
|
||||||
|
private static bool IsShiftHeld()
|
||||||
|
{
|
||||||
|
var kb = Keyboard.current;
|
||||||
|
if (kb == null) return false;
|
||||||
|
return kb.leftShiftKey.isPressed || kb.rightShiftKey.isPressed;
|
||||||
|
}
|
||||||
|
|
||||||
// ----- Ghost management -------------------------------------------
|
// ----- Ghost management -------------------------------------------
|
||||||
|
|
||||||
private void CreateGhost(TowerDefinition def)
|
private void CreateGhost(TowerDefinition def)
|
||||||
|
|
@ -284,10 +287,6 @@ namespace TD.Gameplay
|
||||||
foreach (var rend in ghostRenderers)
|
foreach (var rend in ghostRenderers)
|
||||||
{
|
{
|
||||||
rend.sharedMaterial = ghostMat;
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -315,11 +314,6 @@ namespace TD.Gameplay
|
||||||
/// Converts a world hit point to the footprint anchor tile (SW corner) such
|
/// 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.
|
/// that the footprint center is as close as possible to the hit point.
|
||||||
/// </summary>
|
/// </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)
|
private static Vector2Int ComputeAnchor(Vector3 hitPoint, Vector2Int footprintSize)
|
||||||
{
|
{
|
||||||
float t = GridCoordinates.TILE_SIZE;
|
float t = GridCoordinates.TILE_SIZE;
|
||||||
|
|
@ -357,7 +351,7 @@ namespace TD.Gameplay
|
||||||
|
|
||||||
// ----- Placement submission ---------------------------------------
|
// ----- Placement submission ---------------------------------------
|
||||||
|
|
||||||
private void TrySubmitPlacement(Vector2Int anchor)
|
private void TrySubmitPlacement(Vector2Int anchor, bool chained)
|
||||||
{
|
{
|
||||||
var manager = TowerPlacementManager.Instance;
|
var manager = TowerPlacementManager.Instance;
|
||||||
if (manager == null)
|
if (manager == null)
|
||||||
|
|
@ -369,13 +363,19 @@ namespace TD.Gameplay
|
||||||
|
|
||||||
// Send the RPC regardless of local validity state — the server is
|
// Send the RPC regardless of local validity state — the server is
|
||||||
// authoritative. The local check drives the ghost color only.
|
// 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);
|
manager.RequestPlaceTowerRpc(anchor.x, anchor.y, activeTowerTypeId);
|
||||||
|
|
||||||
// Exit placement mode immediately after submitting. If the server
|
if (chained)
|
||||||
// rejects, the rejection message fires via HandlePlacementRejected.
|
{
|
||||||
// If it accepts, the TowerInstance NetworkObject spawns and the
|
// Stay in placement mode. The server will stamp occupancy on success,
|
||||||
// placed tower appears — no ghost lingering is needed.
|
// so when EvaluateLocalValidity runs next frame it will correctly
|
||||||
|
// report the just-clicked tile as occupied (cursor turns red there).
|
||||||
|
// Force a re-evaluation by invalidating the cached anchor.
|
||||||
|
lastAnchorValid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single-shot: exit placement mode immediately.
|
||||||
CancelPlacement();
|
CancelPlacement();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -395,7 +395,6 @@ namespace TD.Gameplay
|
||||||
Debug.Log($"[TowerPlacementController] Placement rejected: {reason} → \"{message}\"");
|
Debug.Log($"[TowerPlacementController] Placement rejected: {reason} → \"{message}\"");
|
||||||
|
|
||||||
// Fire the event so HUD components can display the message on screen.
|
// 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);
|
OnRejectionMessageReady?.Invoke(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,39 +10,48 @@ namespace TD.Gameplay
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Server-authoritative manager for tower placement requests. Receives placement
|
/// Server-authoritative manager for tower placement requests. Receives placement
|
||||||
/// requests from clients via RPC, validates them in order, and either spawns the
|
/// requests from clients via RPC, validates them in order, and either enqueues
|
||||||
/// tower or rejects the request with a reason code.
|
/// the placement on the player's <see cref="Builder"/> or rejects the request
|
||||||
|
/// with a reason code.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para><b>Queue-based processing.</b> Incoming RPCs enqueue a
|
/// <para><b>Queue-based RPC processing.</b> Incoming RPCs enqueue a
|
||||||
/// <see cref="PlacementRequest"/> rather than validating inline. Each server
|
/// <see cref="PlacementRequest"/> rather than validating inline. Each server
|
||||||
/// Update drains up to <see cref="RequestsPerFrame"/> requests. At 60 fps this
|
/// Update drains up to <see cref="requestsPerFrame"/> requests. At 60 fps this
|
||||||
/// gives ~180 validations/second, comfortably above the worst-case 90/second
|
/// gives ~180 validations/second, comfortably above the worst-case 90/second
|
||||||
/// (9 players × 10 placements/second). Queuing keeps the server frame budget
|
/// (9 players × 10 placements/second). Queuing keeps the server frame budget
|
||||||
/// predictable regardless of burst traffic.</para>
|
/// predictable regardless of burst traffic.</para>
|
||||||
///
|
///
|
||||||
/// <para><b>Server-only logic.</b> All validation and mutation runs on the server.
|
/// <para><b>Server-only logic.</b> All validation and mutation runs on the server.
|
||||||
/// Clients learn about accepted placements when the <see cref="TowerInstance"/>
|
/// Clients learn about accepted placements when the build-site visual NetworkObject
|
||||||
/// NetworkObject spawns (NGO replicates it automatically). Clients learn about
|
/// spawns (queued ghost) and later when the <see cref="TowerInstance"/> spawns at
|
||||||
/// rejections via <see cref="PlacementRejectedRpc"/>.</para>
|
/// construction-complete. Clients learn about rejections via
|
||||||
|
/// <see cref="PlacementRejectedRpc"/>.</para>
|
||||||
///
|
///
|
||||||
/// <para><b>Validation order:</b>
|
/// <para><b>Validation order:</b>
|
||||||
/// <list type="number">
|
/// <list type="number">
|
||||||
|
/// <item>Tower type — must resolve to a valid TowerDefinition.</item>
|
||||||
/// <item>Ownership — every footprint tile must be owned by the requesting player.</item>
|
/// <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>Placement state + occupancy — every tile must be Buildable and not already occupied.
|
||||||
|
/// Occupancy includes both completed towers AND queued/constructing jobs in any
|
||||||
|
/// builder's queue.</item>
|
||||||
|
/// <item>Builder existence — the placing player must have a spawned builder. Build range
|
||||||
|
/// is NOT checked at queue-time — the queue is precisely the mechanism for deferring
|
||||||
|
/// "go there and build it." Range is enforced at construction-start in Builder.</item>
|
||||||
/// <item>Gold — the placing player must have enough gold.</item>
|
/// <item>Gold — the placing player must have enough gold.</item>
|
||||||
|
/// <item>Queue capacity — the placing player's builder queue must have room.</item>
|
||||||
/// <item>Path — a BFS confirms every spawner in the placing player's zone still reaches
|
/// <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>
|
/// an exit after the footprint is stamped as non-walkable. Note that QUEUED towers
|
||||||
|
/// do not participate in this BFS — only completed/constructing towers — because
|
||||||
|
/// queued ghosts are intent, not structures.</item>
|
||||||
/// </list></para>
|
/// </list></para>
|
||||||
///
|
///
|
||||||
/// <para><b>Path-check BFS.</b> The server temporarily stamps the footprint,
|
/// <para><b>D2 build-queue flow.</b> On success, ProcessRequest does NOT spawn the tower.
|
||||||
/// runs BFS per spawner, then un-stamps if the check fails. This is O(tiles in zone)
|
/// Instead it deducts gold, stamps occupancy=true (walkable stays true), and appends a
|
||||||
/// per spawner per request — acceptable for low-frequency gameplay actions and the
|
/// BuildJob to the player's builder. The Builder owns walking to the site, transitioning
|
||||||
/// queue-rate-limited processing model.</para>
|
/// to Constructing (which re-validates the path and stamps walkable=false), running the
|
||||||
///
|
/// staged construction animation, and finally calling
|
||||||
/// <para><b>Builder range check.</b> Deliberately omitted in Path B. The builder
|
/// <see cref="ServerSpawnCompletedTower"/> to spawn the real TowerInstance.</para>
|
||||||
/// 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>
|
/// </remarks>
|
||||||
public class TowerPlacementManager : NetworkBehaviour
|
public class TowerPlacementManager : NetworkBehaviour
|
||||||
{
|
{
|
||||||
|
|
@ -123,12 +132,10 @@ namespace TD.Gameplay
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Client entry point. Call this on the local client to request placing a tower.
|
/// 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)
|
/// The server will validate and either enqueue the placement (which spawns a
|
||||||
/// or call back with <see cref="PlacementRejectedRpc"/>.
|
/// build-site visual visible to all clients) or call back with
|
||||||
|
/// <see cref="PlacementRejectedRpc"/>.
|
||||||
/// </summary>
|
/// </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)]
|
[Rpc(SendTo.Server)]
|
||||||
public void RequestPlaceTowerRpc(int anchorX, int anchorY, int towerTypeId,
|
public void RequestPlaceTowerRpc(int anchorX, int anchorY, int towerTypeId,
|
||||||
RpcParams rpcParams = default)
|
RpcParams rpcParams = default)
|
||||||
|
|
@ -210,7 +217,10 @@ namespace TD.Gameplay
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Check 2: Placement state + occupancy
|
// Check 2: Placement state + occupancy
|
||||||
// Every footprint tile must be Buildable and not already occupied.
|
// Every footprint tile must be Buildable and not already occupied. Note
|
||||||
|
// that the occupancy grid was stamped at queue-time for ALL pending jobs
|
||||||
|
// in any builder's queue, so a single IsOccupied check correctly rejects
|
||||||
|
// overlap with a queued ghost (in this builder's queue OR another's).
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
foreach (var tile in footprint)
|
foreach (var tile in footprint)
|
||||||
{
|
{
|
||||||
|
|
@ -227,10 +237,13 @@ namespace TD.Gameplay
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Check 3: Build range
|
// Check 3: Builder must exist
|
||||||
// Tower must be within the placing player's builder's build range.
|
// The placing player needs a spawned builder to take ownership of the
|
||||||
// Cheap to check; runs before gold and path so we don't burn cycles
|
// resulting BuildJob, but build range is NOT checked here. The whole
|
||||||
// on out-of-range placements.
|
// point of a queue is to defer "go there and build it" — if the builder
|
||||||
|
// is out of range at queue-time, it will walk to the site when the job
|
||||||
|
// reaches the head of the queue. Range is enforced at construction-start
|
||||||
|
// (in Builder.DriveHead_Queued) which is the moment range actually matters.
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
var builder = Builder.GetForClient(req.SenderClientId);
|
var builder = Builder.GetForClient(req.SenderClientId);
|
||||||
if (builder == null)
|
if (builder == null)
|
||||||
|
|
@ -239,11 +252,6 @@ namespace TD.Gameplay
|
||||||
Reject(req, PlacementRejectionReason.ServerError);
|
Reject(req, PlacementRejectionReason.ServerError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!builder.IsTileWithinBuildRange(req.Anchor, def.FootprintSize))
|
|
||||||
{
|
|
||||||
Reject(req, PlacementRejectionReason.OutOfRange);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Check 4: Gold
|
// Check 4: Gold
|
||||||
|
|
@ -258,35 +266,135 @@ namespace TD.Gameplay
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Check 5: Path validity
|
// Check 5: Queue capacity
|
||||||
// Temporarily stamp the footprint, run BFS per spawner in the placing
|
// The placing player's builder must have room for one more job.
|
||||||
// player's zone, then un-stamp if any spawner loses its exit route.
|
// Cheap check; runs before the path BFS.
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
StampFootprint(loader, footprint, walkable: false, occupied: true);
|
if (builder.Jobs.Count >= builder.MaxQueueDepth)
|
||||||
|
{
|
||||||
|
Reject(req, PlacementRejectionReason.JobLimitReached);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Check 6: Path validity (queue-time)
|
||||||
|
// Temporarily stamp the footprint non-walkable, run BFS per spawner
|
||||||
|
// in the placing player's zone, then un-stamp if any spawner loses
|
||||||
|
// its exit route. Importantly we do NOT stamp other queued (but not
|
||||||
|
// yet constructing) jobs as non-walkable — queued ghosts represent
|
||||||
|
// intent only and don't block enemies. The check is "could THIS
|
||||||
|
// tower be built right now if it were instantly complete?" — a
|
||||||
|
// coarse test that catches obvious blockers at queue-time. The
|
||||||
|
// construction-start re-check (in Builder.DriveHead_Queued) catches
|
||||||
|
// cases where the maze changed since queue-time.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
StampWalkable(loader, footprint, walkable: false);
|
||||||
|
|
||||||
bool pathValid = CheckPathValidity(loader, placingSlot);
|
bool pathValid = CheckPathValidity(loader, placingSlot);
|
||||||
|
|
||||||
|
// Restore walkability — the queue stage leaves tiles walkable.
|
||||||
|
// Occupancy is stamped below as part of the commit.
|
||||||
|
StampWalkable(loader, footprint, walkable: true);
|
||||||
|
|
||||||
if (!pathValid)
|
if (!pathValid)
|
||||||
{
|
{
|
||||||
// Un-stamp — the placement is rejected, grid stays as it was.
|
|
||||||
StampFootprint(loader, footprint, walkable: true, occupied: false);
|
|
||||||
Reject(req, PlacementRejectionReason.BlocksPath);
|
Reject(req, PlacementRejectionReason.BlocksPath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// All checks passed — commit the placement.
|
// All checks passed — commit the queue entry.
|
||||||
// The footprint stamp is already applied (walkable=false, occupied=true).
|
// - Mark the footprint occupied (but keep it walkable; queued ghosts
|
||||||
// Deduct gold and spawn the tower NetworkObject.
|
// don't block enemies).
|
||||||
|
// - Deduct gold.
|
||||||
|
// - Append the BuildJob to the builder's queue. The Builder spawns
|
||||||
|
// the green-ghost build-site visual itself.
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
StampOccupied(loader, footprint, occupied: true);
|
||||||
goldManager.DeductGold(def.GoldCost);
|
goldManager.DeductGold(def.GoldCost);
|
||||||
|
|
||||||
SpawnTower(def, req.Anchor, placingSlot);
|
if (!builder.ServerEnqueueJob(req.Anchor, req.TowerTypeId, def.GoldCost,
|
||||||
|
out ulong jobId))
|
||||||
|
{
|
||||||
|
// Should not happen — we checked capacity above. Defensive: roll back.
|
||||||
|
StampOccupied(loader, footprint, occupied: false);
|
||||||
|
goldManager.AwardGold(def.GoldCost);
|
||||||
|
Reject(req, PlacementRejectionReason.JobLimitReached);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Debug.Log($"[TowerPlacementManager] Placed '{def.DisplayName}' for " +
|
Debug.Log($"[TowerPlacementManager] Queued '{def.DisplayName}' (job {jobId}) for " +
|
||||||
$"client {req.SenderClientId} ({placingSlot}) at anchor {req.Anchor}.");
|
$"client {req.SenderClientId} ({placingSlot}) at anchor {req.Anchor}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----- Server-side commit hooks called by Builder ------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server-only: called by <c>Builder</c> the moment a queued job's builder arrives
|
||||||
|
/// at the build site. Stamps the footprint non-walkable and re-runs the path BFS.
|
||||||
|
/// On success, the footprint stays non-walkable and the caller transitions the
|
||||||
|
/// job to Constructing. On failure, walkability is restored and false is returned;
|
||||||
|
/// caller must drop and refund the job.
|
||||||
|
/// </summary>
|
||||||
|
public bool ServerCommitConstructionStart(Vector2Int anchor, Vector2Int footprintSize,
|
||||||
|
PlayerSlot placingSlot)
|
||||||
|
{
|
||||||
|
if (!IsServer) return false;
|
||||||
|
|
||||||
|
var loader = LevelLoader.Instance;
|
||||||
|
if (loader == null || !loader.IsLoaded) return false;
|
||||||
|
|
||||||
|
var footprint = new List<Vector2Int>(footprintSize.x * footprintSize.y);
|
||||||
|
foreach (var tile in GridCoordinates.GetFootprintTiles(anchor, footprintSize))
|
||||||
|
footprint.Add(tile);
|
||||||
|
|
||||||
|
StampWalkable(loader, footprint, walkable: false);
|
||||||
|
|
||||||
|
bool ok = CheckPathValidity(loader, placingSlot);
|
||||||
|
|
||||||
|
if (!ok)
|
||||||
|
{
|
||||||
|
// Roll back — the maze would break. Caller refunds and drops the job.
|
||||||
|
StampWalkable(loader, footprint, walkable: true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footprint is now occupied (still) and non-walkable. Construction proceeds.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server-only: read-only path validity check for the given player's zone.
|
||||||
|
/// Used by <c>Builder</c> when resuming a paused job — the footprint is
|
||||||
|
/// already non-walkable (has been since original construction-start), so
|
||||||
|
/// stamping/un-stamping would be a no-op at best and a bug at worst (an
|
||||||
|
/// un-stamp would un-block an actively-blocking tile). This method just
|
||||||
|
/// runs the BFS against the current grid state and returns the result.
|
||||||
|
/// </summary>
|
||||||
|
public bool ServerVerifyPathStillValid(PlayerSlot placingSlot)
|
||||||
|
{
|
||||||
|
if (!IsServer) return false;
|
||||||
|
|
||||||
|
var loader = LevelLoader.Instance;
|
||||||
|
if (loader == null || !loader.IsLoaded) return false;
|
||||||
|
|
||||||
|
return CheckPathValidity(loader, placingSlot);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server-only: called by <c>Builder</c> when a constructing job completes.
|
||||||
|
/// Spawns the real <see cref="TowerInstance"/> NetworkObject at the anchor.
|
||||||
|
/// The footprint is already occupied and non-walkable from the construction-start
|
||||||
|
/// commit; <c>TowerInstance.OnNetworkSpawn</c>'s footprint stamp is idempotent
|
||||||
|
/// and harmless.
|
||||||
|
/// </summary>
|
||||||
|
public void ServerSpawnCompletedTower(TowerDefinition def, Vector2Int anchor,
|
||||||
|
PlayerSlot owner)
|
||||||
|
{
|
||||||
|
if (!IsServer) return;
|
||||||
|
SpawnTower(def, anchor, owner);
|
||||||
|
}
|
||||||
|
|
||||||
// ----- Path-validity BFS ------------------------------------------
|
// ----- Path-validity BFS ------------------------------------------
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -316,14 +424,9 @@ namespace TD.Gameplay
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the exit tile set: union of all leak exit tiles and all goal tiles.
|
// 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);
|
var exitTiles = BuildExitTileSet(levelData, slot);
|
||||||
if (exitTiles.Count == 0)
|
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. " +
|
Debug.LogWarning($"[TowerPlacementManager] Zone {slot} has no exit tiles. " +
|
||||||
$"This should have been caught at bake time (P5-8).");
|
$"This should have been caught at bake time (P5-8).");
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -406,17 +509,25 @@ namespace TD.Gameplay
|
||||||
// ----- Helpers ----------------------------------------------------
|
// ----- Helpers ----------------------------------------------------
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stamps or un-stamps all tiles in <paramref name="footprint"/> on both the
|
/// Stamps walkability on every tile in <paramref name="footprint"/>.
|
||||||
/// walkability and occupancy grids simultaneously. Always update both together.
|
/// Independent of occupancy because the queue-time and construction-time
|
||||||
|
/// transitions touch them on different schedules.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void StampFootprint(LevelLoader loader, List<Vector2Int> footprint,
|
private static void StampWalkable(LevelLoader loader, List<Vector2Int> footprint,
|
||||||
bool walkable, bool occupied)
|
bool walkable)
|
||||||
{
|
{
|
||||||
foreach (var tile in footprint)
|
foreach (var tile in footprint)
|
||||||
{
|
|
||||||
loader.SetWalkable(tile, walkable);
|
loader.SetWalkable(tile, walkable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stamps occupancy on every tile in <paramref name="footprint"/>.
|
||||||
|
/// </summary>
|
||||||
|
private static void StampOccupied(LevelLoader loader, List<Vector2Int> footprint,
|
||||||
|
bool occupied)
|
||||||
|
{
|
||||||
|
foreach (var tile in footprint)
|
||||||
loader.SetOccupied(tile, occupied);
|
loader.SetOccupied(tile, occupied);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -468,14 +579,21 @@ namespace TD.Gameplay
|
||||||
def = null;
|
def = null;
|
||||||
// typeId 0 is reserved; valid IDs start at 1.
|
// typeId 0 is reserved; valid IDs start at 1.
|
||||||
if (typeId <= 0) return false;
|
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 (Instance == null) return false;
|
||||||
if (typeId >= Instance.towerDefinitions.Length) return false;
|
if (typeId >= Instance.towerDefinitions.Length) return false;
|
||||||
def = Instance.towerDefinitions[typeId];
|
def = Instance.towerDefinitions[typeId];
|
||||||
return def != null;
|
return def != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Public lookup used by <see cref="Builder"/> to resolve a tower type ID
|
||||||
|
/// to its definition. Returns null if the ID is invalid.
|
||||||
|
/// </summary>
|
||||||
|
public static TowerDefinition GetDefinition(int typeId)
|
||||||
|
{
|
||||||
|
return TryGetDefinition(typeId, out var def) ? def : null;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maps a client ID to the PlayerSlot assigned to that client.
|
/// Maps a client ID to the PlayerSlot assigned to that client.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -486,8 +604,6 @@ namespace TD.Gameplay
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
private static PlayerSlot ClientIdToPlayerSlot(ulong clientId)
|
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);
|
byte slotByte = (byte)(clientId + 1);
|
||||||
if (slotByte < 1 || slotByte > 9) return PlayerSlot.None;
|
if (slotByte < 1 || slotByte > 9) return PlayerSlot.None;
|
||||||
return (PlayerSlot)slotByte;
|
return (PlayerSlot)slotByte;
|
||||||
|
|
@ -524,7 +640,8 @@ namespace TD.Gameplay
|
||||||
/// (they are Restricted or Outside the map).</summary>
|
/// (they are Restricted or Outside the map).</summary>
|
||||||
TileNotBuildable,
|
TileNotBuildable,
|
||||||
|
|
||||||
/// <summary>One or more footprint tiles are already occupied by an existing tower.</summary>
|
/// <summary>One or more footprint tiles are already occupied by an existing tower
|
||||||
|
/// or by a queued/constructing build job.</summary>
|
||||||
TileOccupied,
|
TileOccupied,
|
||||||
|
|
||||||
/// <summary>The placing player does not have enough gold.</summary>
|
/// <summary>The placing player does not have enough gold.</summary>
|
||||||
|
|
@ -537,6 +654,10 @@ namespace TD.Gameplay
|
||||||
/// spawner to its exit. The maze must remain passable.</summary>
|
/// spawner to its exit. The maze must remain passable.</summary>
|
||||||
BlocksPath,
|
BlocksPath,
|
||||||
|
|
||||||
|
/// <summary>The placing player's builder queue is at capacity. Cancel pending
|
||||||
|
/// jobs or wait for one to complete before queuing more.</summary>
|
||||||
|
JobLimitReached,
|
||||||
|
|
||||||
/// <summary>The requested tower type ID is not in the server's definition list.</summary>
|
/// <summary>The requested tower type ID is not in the server's definition list.</summary>
|
||||||
InvalidTowerType,
|
InvalidTowerType,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,25 +17,31 @@ namespace TD.Gameplay
|
||||||
/// <para><b>Rejection messages.</b> These are the strings shown on screen when the
|
/// <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
|
/// server rejects a placement. Kept here so designers can tune the wording without
|
||||||
/// touching code.</para>
|
/// touching code.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Note on build-site visuals.</b> The green queued-ghost material and the
|
||||||
|
/// constructing-stage material live on the <c>BuildSiteVisual</c> prefab, not here.
|
||||||
|
/// Those materials are prefab-local (the build-site visual prefab references them
|
||||||
|
/// directly), so promoting them to a shared settings asset would just add an
|
||||||
|
/// indirection without simplifying anything.</para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[CreateAssetMenu(fileName = "TowerPlacementSettings",
|
[CreateAssetMenu(fileName = "TowerPlacementSettings",
|
||||||
menuName = "TD/Tower Placement Settings",
|
menuName = "TD/Tower Placement Settings",
|
||||||
order = 3)]
|
order = 3)]
|
||||||
public class TowerPlacementSettings : ScriptableObject
|
public class TowerPlacementSettings : ScriptableObject
|
||||||
{
|
{
|
||||||
// ----- Ghost visuals -----------------------------------------------
|
// ----- Cursor ghost visuals ---------------------------------------
|
||||||
|
|
||||||
[Header("Ghost Materials")]
|
[Header("Cursor Ghost Materials")]
|
||||||
[Tooltip("Material applied to the placement ghost when placement is valid. " +
|
[Tooltip("Material applied to the cursor placement ghost when placement is valid. " +
|
||||||
"Should be a transparent/unlit white material so the tower mesh is " +
|
"Should be a transparent/unlit white material so the tower mesh is " +
|
||||||
"recognizable but clearly distinct from a placed tower.")]
|
"recognizable but clearly distinct from a placed tower.")]
|
||||||
public Material GhostValidMaterial;
|
public Material GhostValidMaterial;
|
||||||
|
|
||||||
[Tooltip("Material applied to the placement ghost when placement is invalid. " +
|
[Tooltip("Material applied to the cursor placement ghost when placement is invalid. " +
|
||||||
"Should be a transparent/unlit red material.")]
|
"Should be a transparent/unlit red material.")]
|
||||||
public Material GhostInvalidMaterial;
|
public Material GhostInvalidMaterial;
|
||||||
|
|
||||||
// ----- Rejection messages ------------------------------------------
|
// ----- Rejection messages -----------------------------------------
|
||||||
|
|
||||||
[Header("Rejection Messages")]
|
[Header("Rejection Messages")]
|
||||||
[Tooltip("Shown when the server rejects a placement because tiles belong to " +
|
[Tooltip("Shown when the server rejects a placement because tiles belong to " +
|
||||||
|
|
@ -47,8 +53,8 @@ namespace TD.Gameplay
|
||||||
public string MessageTileNotBuildable = "That location is not buildable.";
|
public string MessageTileNotBuildable = "That location is not buildable.";
|
||||||
|
|
||||||
[Tooltip("Shown when the server rejects because a tile is already occupied " +
|
[Tooltip("Shown when the server rejects because a tile is already occupied " +
|
||||||
"by an existing tower.")]
|
"by an existing tower or by a queued/constructing build job.")]
|
||||||
public string MessageTileOccupied = "A tower already occupies that location.";
|
public string MessageTileOccupied = "A tower is already there or queued there.";
|
||||||
|
|
||||||
[Tooltip("Shown when the server rejects because the player cannot afford " +
|
[Tooltip("Shown when the server rejects because the player cannot afford " +
|
||||||
"the tower.")]
|
"the tower.")]
|
||||||
|
|
@ -62,6 +68,10 @@ namespace TD.Gameplay
|
||||||
"all valid paths through the player's zone.")]
|
"all valid paths through the player's zone.")]
|
||||||
public string MessageBlocksPath = "That placement would block the path.";
|
public string MessageBlocksPath = "That placement would block the path.";
|
||||||
|
|
||||||
|
[Tooltip("Shown when the server rejects because the builder's queue is full. " +
|
||||||
|
"Player must cancel pending jobs or wait for one to complete.")]
|
||||||
|
public string MessageJobLimitReached = "Builder queue is full.";
|
||||||
|
|
||||||
[Tooltip("Shown for unexpected server-side errors (invalid tower type, etc.). " +
|
[Tooltip("Shown for unexpected server-side errors (invalid tower type, etc.). " +
|
||||||
"Should rarely appear in normal play.")]
|
"Should rarely appear in normal play.")]
|
||||||
public string MessageServerError = "Placement failed. Please try again.";
|
public string MessageServerError = "Placement failed. Please try again.";
|
||||||
|
|
@ -81,6 +91,7 @@ namespace TD.Gameplay
|
||||||
case PlacementRejectionReason.InsufficientGold: return MessageInsufficientGold;
|
case PlacementRejectionReason.InsufficientGold: return MessageInsufficientGold;
|
||||||
case PlacementRejectionReason.OutOfRange: return MessageOutOfRange;
|
case PlacementRejectionReason.OutOfRange: return MessageOutOfRange;
|
||||||
case PlacementRejectionReason.BlocksPath: return MessageBlocksPath;
|
case PlacementRejectionReason.BlocksPath: return MessageBlocksPath;
|
||||||
|
case PlacementRejectionReason.JobLimitReached: return MessageJobLimitReached;
|
||||||
case PlacementRejectionReason.InvalidTowerType:
|
case PlacementRejectionReason.InvalidTowerType:
|
||||||
case PlacementRejectionReason.ServerError:
|
case PlacementRejectionReason.ServerError:
|
||||||
default: return MessageServerError;
|
default: return MessageServerError;
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ TagManager:
|
||||||
- UI
|
- UI
|
||||||
- BuildablePlane
|
- BuildablePlane
|
||||||
- TerrainGeometry
|
- TerrainGeometry
|
||||||
-
|
- Selection
|
||||||
-
|
- BuildSite
|
||||||
-
|
-
|
||||||
-
|
-
|
||||||
-
|
-
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue