diff --git a/Assets/_Project/Art/Sprites.meta b/Assets/_Project/Art/Sprites.meta
new file mode 100644
index 0000000..d36ae05
--- /dev/null
+++ b/Assets/_Project/Art/Sprites.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 2d10906489190644baf28d93e5197c42
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Art/Sprites/BloodAngels.jpg b/Assets/_Project/Art/Sprites/BloodAngels.jpg
new file mode 100644
index 0000000..a85ea3d
--- /dev/null
+++ b/Assets/_Project/Art/Sprites/BloodAngels.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4fa63ce77f2808fb9a275dcfea9747c91e2c6e521589b1af1bf85d29ef7752ae
+size 7854
diff --git a/Assets/_Project/Art/Sprites/BloodAngels.jpg.meta b/Assets/_Project/Art/Sprites/BloodAngels.jpg.meta
new file mode 100644
index 0000000..1bb4c7f
--- /dev/null
+++ b/Assets/_Project/Art/Sprites/BloodAngels.jpg.meta
@@ -0,0 +1,117 @@
+fileFormatVersion: 2
+guid: dbc19961a66b0b6478efea006e0a0fee
+TextureImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 13
+ mipmaps:
+ mipMapMode: 0
+ enableMipMap: 0
+ sRGBTexture: 1
+ linearTexture: 0
+ fadeOut: 0
+ borderMipMap: 0
+ mipMapsPreserveCoverage: 0
+ alphaTestReferenceValue: 0.5
+ mipMapFadeDistanceStart: 1
+ mipMapFadeDistanceEnd: 3
+ bumpmap:
+ convertToNormalMap: 0
+ externalNormalMap: 0
+ heightScale: 0.25
+ normalMapFilter: 0
+ flipGreenChannel: 0
+ isReadable: 0
+ streamingMipmaps: 0
+ streamingMipmapsPriority: 0
+ vTOnly: 0
+ ignoreMipmapLimit: 0
+ grayScaleToAlpha: 0
+ generateCubemap: 6
+ cubemapConvolution: 0
+ seamlessCubemap: 0
+ textureFormat: 1
+ maxTextureSize: 2048
+ textureSettings:
+ serializedVersion: 2
+ filterMode: 1
+ aniso: 1
+ mipBias: 0
+ wrapU: 1
+ wrapV: 1
+ wrapW: 0
+ nPOTScale: 0
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 1
+ spriteExtrude: 1
+ spriteMeshType: 1
+ alignment: 0
+ spritePivot: {x: 0.5, y: 0.5}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 1
+ spriteTessellationDetail: -1
+ textureType: 8
+ textureShape: 1
+ singleChannelComponent: 0
+ flipbookRows: 1
+ flipbookColumns: 1
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ ignorePngGamma: 0
+ applyGammaDecoding: 0
+ swizzle: 50462976
+ cookieLightType: 0
+ platformSettings:
+ - serializedVersion: 4
+ buildTarget: DefaultTexturePlatform
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Standalone
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ spriteSheet:
+ serializedVersion: 2
+ sprites: []
+ outline: []
+ customData:
+ physicsShape: []
+ bones: []
+ spriteID: 5e97eb03825dee720800000000000000
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable: {}
+ mipmapLimitGroupName:
+ pSDRemoveMatte: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Art/Sprites/Ultramarines.jpg b/Assets/_Project/Art/Sprites/Ultramarines.jpg
new file mode 100644
index 0000000..c3abc51
--- /dev/null
+++ b/Assets/_Project/Art/Sprites/Ultramarines.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9033f5e4e490ee5a82e550aae4f690eefc49463a804ecc91e2874a28e8f988d1
+size 5726
diff --git a/Assets/_Project/Art/Sprites/Ultramarines.jpg.meta b/Assets/_Project/Art/Sprites/Ultramarines.jpg.meta
new file mode 100644
index 0000000..3a200b3
--- /dev/null
+++ b/Assets/_Project/Art/Sprites/Ultramarines.jpg.meta
@@ -0,0 +1,117 @@
+fileFormatVersion: 2
+guid: f19f71b7dd6671841abd34ce8b359351
+TextureImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 13
+ mipmaps:
+ mipMapMode: 0
+ enableMipMap: 0
+ sRGBTexture: 1
+ linearTexture: 0
+ fadeOut: 0
+ borderMipMap: 0
+ mipMapsPreserveCoverage: 0
+ alphaTestReferenceValue: 0.5
+ mipMapFadeDistanceStart: 1
+ mipMapFadeDistanceEnd: 3
+ bumpmap:
+ convertToNormalMap: 0
+ externalNormalMap: 0
+ heightScale: 0.25
+ normalMapFilter: 0
+ flipGreenChannel: 0
+ isReadable: 0
+ streamingMipmaps: 0
+ streamingMipmapsPriority: 0
+ vTOnly: 0
+ ignoreMipmapLimit: 0
+ grayScaleToAlpha: 0
+ generateCubemap: 6
+ cubemapConvolution: 0
+ seamlessCubemap: 0
+ textureFormat: 1
+ maxTextureSize: 2048
+ textureSettings:
+ serializedVersion: 2
+ filterMode: 1
+ aniso: 1
+ mipBias: 0
+ wrapU: 1
+ wrapV: 1
+ wrapW: 0
+ nPOTScale: 0
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 1
+ spriteExtrude: 1
+ spriteMeshType: 1
+ alignment: 0
+ spritePivot: {x: 0.5, y: 0.5}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 1
+ spriteTessellationDetail: -1
+ textureType: 8
+ textureShape: 1
+ singleChannelComponent: 0
+ flipbookRows: 1
+ flipbookColumns: 1
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ ignorePngGamma: 0
+ applyGammaDecoding: 0
+ swizzle: 50462976
+ cookieLightType: 0
+ platformSettings:
+ - serializedVersion: 4
+ buildTarget: DefaultTexturePlatform
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Standalone
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ spriteSheet:
+ serializedVersion: 2
+ sprites: []
+ outline: []
+ customData:
+ physicsShape: []
+ bones: []
+ spriteID: 5e97eb03825dee720800000000000000
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable: {}
+ mipmapLimitGroupName:
+ pSDRemoveMatte: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Data/Races.meta b/Assets/_Project/Data/Races.meta
new file mode 100644
index 0000000..2e39372
--- /dev/null
+++ b/Assets/_Project/Data/Races.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 0d631899488b6db4a8d9af9a97a9b2d5
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Data/Races/BloodAngels_Placeholder.asset b/Assets/_Project/Data/Races/BloodAngels_Placeholder.asset
new file mode 100644
index 0000000..7c72d09
--- /dev/null
+++ b/Assets/_Project/Data/Races/BloodAngels_Placeholder.asset
@@ -0,0 +1,23 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 98b7d5d116870f94da912007f6aa5cbb, type: 3}
+ m_Name: BloodAngels_Placeholder
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.RaceDefinition
+ Id: 2
+ DisplayName: Blood Angels
+ Icon: {fileID: 21300000, guid: dbc19961a66b0b6478efea006e0a0fee, type: 3}
+ BuilderName: Mother Teresa
+ BuilderDescription: Evil beyond measure, a truly fucked up woman
+ LoreText: Crucified and brought back by God because her job wasn't finished. She's
+ here to bring death to to Xenos scum.
+ BuilderPrefab: {fileID: 116861493430507844, guid: 3398cc5831880954487717577f61b6d7, type: 3}
+ Towers: []
diff --git a/Assets/_Project/Data/Races/BloodAngels_Placeholder.asset.meta b/Assets/_Project/Data/Races/BloodAngels_Placeholder.asset.meta
new file mode 100644
index 0000000..0c08310
--- /dev/null
+++ b/Assets/_Project/Data/Races/BloodAngels_Placeholder.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 4d91bbd27e96fb845af6bb1bf0a22fe4
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Data/Races/Ultramarines_Placeholder.asset b/Assets/_Project/Data/Races/Ultramarines_Placeholder.asset
new file mode 100644
index 0000000..e83b9ea
--- /dev/null
+++ b/Assets/_Project/Data/Races/Ultramarines_Placeholder.asset
@@ -0,0 +1,23 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 98b7d5d116870f94da912007f6aa5cbb, type: 3}
+ m_Name: Ultramarines_Placeholder
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.RaceDefinition
+ Id: 1
+ DisplayName: Ultramarines
+ Icon: {fileID: 21300000, guid: f19f71b7dd6671841abd34ce8b359351, type: 3}
+ BuilderName: Master Chief
+ BuilderDescription: My man makes some bangin towers
+ LoreText: King of the protoss, he's here to kick bubblegum and chew ass, and he's
+ all out of ass.
+ BuilderPrefab: {fileID: 116861493430507844, guid: 3398cc5831880954487717577f61b6d7, type: 3}
+ Towers: []
diff --git a/Assets/_Project/Data/Races/Ultramarines_Placeholder.asset.meta b/Assets/_Project/Data/Races/Ultramarines_Placeholder.asset.meta
new file mode 100644
index 0000000..8b9ea41
--- /dev/null
+++ b/Assets/_Project/Data/Races/Ultramarines_Placeholder.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: c8b11f545e3990049a5953a34459f58a
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Prefabs/Builders/Builder_Mixamo.prefab b/Assets/_Project/Prefabs/Builders/Builder_Mixamo.prefab
index 3371ce0..6a8b1dc 100644
--- a/Assets/_Project/Prefabs/Builders/Builder_Mixamo.prefab
+++ b/Assets/_Project/Prefabs/Builders/Builder_Mixamo.prefab
@@ -146,7 +146,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
- GlobalObjectIdHash: 4180539217
+ GlobalObjectIdHash: 3501512193
InScenePlacedSourceGlobalObjectIdHash: 3702618695
DeferredDespawnTick: 0
Ownership: 1
@@ -977,7 +977,6 @@ GameObject:
- component: {fileID: 4960685830559074027}
- component: {fileID: 4757296063414367819}
- component: {fileID: 336275605508886593}
- - component: {fileID: 4966682596699708025}
m_Layer: 0
m_Name: SelectionRing
m_TagString: Untagged
@@ -1057,18 +1056,6 @@ MeshRenderer:
m_SortingOrder: 0
m_MaskInteraction: 0
m_AdditionalVertexStreams: {fileID: 0}
---- !u!114 &4966682596699708025
-MonoBehaviour:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 2652716240617921727}
- 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 &2781170330021425455
GameObject:
m_ObjectHideFlags: 0
diff --git a/Assets/_Project/Scenes/Levels/Main.unity b/Assets/_Project/Scenes/Levels/Main.unity
index a5c86a7..e57a4d6 100644
--- a/Assets/_Project/Scenes/Levels/Main.unity
+++ b/Assets/_Project/Scenes/Levels/Main.unity
@@ -1502,12 +1502,12 @@ MonoBehaviour:
edgePanEnabled: 1
minDollyDistance: 5
maxDollyDistance: 50
- startDollyDistance: 35
+ startDollyDistance: 25
zoomSpeed: 3
cursorAnchoredZoom: 1
minPitchDegrees: 30
maxPitchDegrees: 75
- startPitchDegrees: 60
+ startPitchDegrees: 50
pitchSpeed: 4
--- !u!4 &1239994224
Transform:
@@ -1950,112 +1950,6 @@ MonoBehaviour:
serializedVersion: 2
m_Bits: 64
raycastMaxDistance: 500
---- !u!1 &1682341399
-GameObject:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- serializedVersion: 6
- m_Component:
- - component: {fileID: 1682341402}
- - component: {fileID: 1682341401}
- - component: {fileID: 1682341400}
- m_Layer: 0
- m_Name: NetworkManager
- m_TagString: Untagged
- m_Icon: {fileID: 0}
- m_NavMeshLayer: 0
- m_StaticEditorFlags: 0
- m_IsActive: 1
---- !u!114 &1682341400
-MonoBehaviour:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1682341399}
- m_Enabled: 1
- m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: 6960e84d07fb87f47956e7a81d71c4e6, type: 3}
- m_Name:
- m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.Transports.UTP.UnityTransport
- m_ProtocolType: 0
- m_UseWebSockets: 0
- m_UseEncryption: 0
- m_MaxPacketQueueSize: 128
- m_MaxPayloadSize: 6144
- m_HeartbeatTimeoutMS: 500
- m_ConnectTimeoutMS: 1000
- m_MaxConnectAttempts: 60
- m_DisconnectTimeoutMS: 30000
- ConnectionData:
- Address: 127.0.0.1
- Port: 7777
- WebSocketPath: /
- ServerListenAddress: 127.0.0.1
- ClientBindPort: 0
- DebugSimulator:
- PacketDelayMS: 0
- PacketJitterMS: 0
- PacketDropRate: 0
---- !u!114 &1682341401
-MonoBehaviour:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1682341399}
- m_Enabled: 1
- m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: 593a2fe42fa9d37498c96f9a383b6521, type: 3}
- m_Name:
- m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkManager
- NetworkManagerExpanded: 0
- NetworkConfig:
- ProtocolVersion: 0
- NetworkTransport: {fileID: 1682341400}
- PlayerPrefab: {fileID: 3493329038866903420, guid: 9a9c23b8584ab444aa5066a48579a9ec, type: 3}
- Prefabs:
- NetworkPrefabsLists:
- - {fileID: 11400000, guid: 481ab1d7456efd044bc3e349aacd92ae, type: 2}
- TickRate: 30
- ClientConnectionBufferTimeout: 10
- ConnectionApproval: 0
- ConnectionData:
- EnableTimeResync: 0
- TimeResyncInterval: 30
- EnsureNetworkVariableLengthSafety: 0
- EnableSceneManagement: 1
- ForceSamePrefabs: 1
- RecycleNetworkIds: 1
- NetworkIdRecycleDelay: 120
- RpcHashSize: 0
- LoadSceneTimeOut: 120
- SpawnTimeout: 10
- EnableNetworkLogs: 1
- NetworkTopology: 0
- UseCMBService: 0
- AutoSpawnPlayerPrefabClientSide: 1
- NetworkProfilingMetrics: 1
- OldPrefabList: []
- RunInBackground: 1
- LogLevel: 1
---- !u!4 &1682341402
-Transform:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1682341399}
- serializedVersion: 2
- m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
- m_LocalPosition: {x: 0, y: 0, z: 0}
- m_LocalScale: {x: 1, y: 1, z: 1}
- m_ConstrainProportionsScale: 0
- m_Children: []
- m_Father: {fileID: 0}
- m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1731269685
GameObject:
m_ObjectHideFlags: 0
@@ -2598,7 +2492,6 @@ SceneRoots:
m_Roots:
- {fileID: 410087041}
- {fileID: 832575519}
- - {fileID: 1682341402}
- {fileID: 441239881}
- {fileID: 167151709}
- {fileID: 1507514109}
diff --git a/Assets/_Project/Scenes/UI/Lobby.unity b/Assets/_Project/Scenes/UI/Lobby.unity
new file mode 100644
index 0000000..97e02e1
--- /dev/null
+++ b/Assets/_Project/Scenes/UI/Lobby.unity
@@ -0,0 +1,546 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!29 &1
+OcclusionCullingSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 2
+ m_OcclusionBakeSettings:
+ smallestOccluder: 5
+ smallestHole: 0.25
+ backfaceThreshold: 100
+ m_SceneGUID: 00000000000000000000000000000000
+ m_OcclusionCullingData: {fileID: 0}
+--- !u!104 &2
+RenderSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 10
+ m_Fog: 0
+ m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
+ m_FogMode: 3
+ m_FogDensity: 0.01
+ m_LinearFogStart: 0
+ m_LinearFogEnd: 300
+ m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
+ m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
+ m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
+ m_AmbientIntensity: 1
+ m_AmbientMode: 0
+ m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
+ m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
+ m_HaloStrength: 0.5
+ m_FlareStrength: 1
+ m_FlareFadeSpeed: 3
+ m_HaloTexture: {fileID: 0}
+ m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
+ m_DefaultReflectionMode: 0
+ m_DefaultReflectionResolution: 128
+ m_ReflectionBounces: 1
+ m_ReflectionIntensity: 1
+ m_CustomReflection: {fileID: 0}
+ m_Sun: {fileID: 0}
+ m_UseRadianceAmbientProbe: 0
+--- !u!157 &3
+LightmapSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 13
+ m_BakeOnSceneLoad: 0
+ m_GISettings:
+ serializedVersion: 2
+ m_BounceScale: 1
+ m_IndirectOutputScale: 1
+ m_AlbedoBoost: 1
+ m_EnvironmentLightingMode: 0
+ m_EnableBakedLightmaps: 1
+ m_EnableRealtimeLightmaps: 0
+ m_LightmapEditorSettings:
+ serializedVersion: 12
+ m_Resolution: 2
+ m_BakeResolution: 40
+ m_AtlasSize: 1024
+ m_AO: 0
+ m_AOMaxDistance: 1
+ m_CompAOExponent: 1
+ m_CompAOExponentDirect: 0
+ m_ExtractAmbientOcclusion: 0
+ m_Padding: 2
+ m_LightmapParameters: {fileID: 0}
+ m_LightmapsBakeMode: 1
+ m_TextureCompression: 1
+ m_ReflectionCompression: 2
+ m_MixedBakeMode: 2
+ m_BakeBackend: 1
+ m_PVRSampling: 1
+ m_PVRDirectSampleCount: 32
+ m_PVRSampleCount: 512
+ m_PVRBounces: 2
+ m_PVREnvironmentSampleCount: 256
+ m_PVREnvironmentReferencePointCount: 2048
+ m_PVRFilteringMode: 1
+ m_PVRDenoiserTypeDirect: 1
+ m_PVRDenoiserTypeIndirect: 1
+ m_PVRDenoiserTypeAO: 1
+ m_PVRFilterTypeDirect: 0
+ m_PVRFilterTypeIndirect: 0
+ m_PVRFilterTypeAO: 0
+ m_PVREnvironmentMIS: 1
+ m_PVRCulling: 1
+ m_PVRFilteringGaussRadiusDirect: 1
+ m_PVRFilteringGaussRadiusIndirect: 5
+ m_PVRFilteringGaussRadiusAO: 2
+ m_PVRFilteringAtrousPositionSigmaDirect: 0.5
+ m_PVRFilteringAtrousPositionSigmaIndirect: 2
+ m_PVRFilteringAtrousPositionSigmaAO: 1
+ m_ExportTrainingData: 0
+ m_TrainingDataDestination: TrainingData
+ m_LightProbeSampleCountMultiplier: 4
+ m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
+ m_LightingSettings: {fileID: 0}
+--- !u!196 &4
+NavMeshSettings:
+ serializedVersion: 2
+ m_ObjectHideFlags: 0
+ m_BuildSettings:
+ serializedVersion: 3
+ agentTypeID: 0
+ agentRadius: 0.5
+ agentHeight: 2
+ agentSlope: 45
+ agentClimb: 0.4
+ ledgeDropHeight: 0
+ maxJumpAcrossDistance: 0
+ minRegionArea: 2
+ manualCellSize: 0
+ cellSize: 0.16666667
+ manualTileSize: 0
+ tileSize: 256
+ buildHeightMesh: 0
+ maxJobWorkers: 0
+ preserveTilesOutsideBounds: 0
+ debug:
+ m_Flags: 0
+ m_NavMeshData: {fileID: 0}
+--- !u!1 &178257552
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 178257555}
+ - component: {fileID: 178257554}
+ - component: {fileID: 178257553}
+ - component: {fileID: 178257556}
+ m_Layer: 5
+ m_Name: LobbyUI
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &178257553
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 178257552}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 8c40427d598ba944c82f3790429c5532, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.UI.LobbyController
+ raceOverlay: {fileID: 178257556}
+--- !u!114 &178257554
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 178257552}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 19102, guid: 0000000000000000e000000000000000, type: 0}
+ m_Name:
+ m_EditorClassIdentifier: UnityEngine.dll::UnityEngine.UIElements.UIDocument
+ m_PanelSettings: {fileID: 11400000, guid: 6aa0af71585acea4db4995c3931dc946, type: 2}
+ m_ParentUI: {fileID: 0}
+ sourceAsset: {fileID: 0}
+ m_SortingOrder: 0
+ m_Position: 0
+ m_WorldSpaceSizeMode: 1
+ m_WorldSpaceWidth: 1920
+ m_WorldSpaceHeight: 1080
+ m_PivotReferenceSize: 0
+ m_Pivot: 0
+ m_WorldSpaceCollider: {fileID: 0}
+--- !u!4 &178257555
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 178257552}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &178257556
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 178257552}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 9ab3ad11b37d4c54d9f310f5356d31ac, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.UI.RaceSelectionOverlay
+--- !u!1 &203844586
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 203844589}
+ - component: {fileID: 203844588}
+ - component: {fileID: 203844587}
+ m_Layer: 0
+ m_Name: Directional Light
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &203844587
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 203844586}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_UsePipelineSettings: 1
+ m_AdditionalLightsShadowResolutionTier: 2
+ m_CustomShadowLayers: 0
+ m_LightCookieSize: {x: 1, y: 1}
+ m_LightCookieOffset: {x: 0, y: 0}
+ m_SoftShadowQuality: 0
+ m_RenderingLayersMask:
+ serializedVersion: 0
+ m_Bits: 1
+ m_ShadowRenderingLayersMask:
+ serializedVersion: 0
+ m_Bits: 1
+ m_Version: 4
+ m_LightLayerMask: 1
+ m_ShadowLayerMask: 1
+ m_RenderingLayers: 1
+ m_ShadowRenderingLayers: 1
+--- !u!108 &203844588
+Light:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 203844586}
+ m_Enabled: 1
+ serializedVersion: 13
+ m_Type: 1
+ m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1}
+ m_Intensity: 1
+ m_Range: 10
+ m_SpotAngle: 30
+ m_InnerSpotAngle: 21.80208
+ m_CookieSize2D: {x: 10, y: 10}
+ m_Shadows:
+ m_Type: 2
+ m_Resolution: -1
+ m_CustomResolution: -1
+ m_Strength: 1
+ m_Bias: 0.05
+ m_NormalBias: 0.4
+ m_NearPlane: 0.2
+ m_CullingMatrixOverride:
+ e00: 1
+ e01: 0
+ e02: 0
+ e03: 0
+ e10: 0
+ e11: 1
+ e12: 0
+ e13: 0
+ e20: 0
+ e21: 0
+ e22: 1
+ e23: 0
+ e30: 0
+ e31: 0
+ e32: 0
+ e33: 1
+ m_UseCullingMatrixOverride: 0
+ m_Cookie: {fileID: 0}
+ m_DrawHalo: 0
+ m_Flare: {fileID: 0}
+ m_RenderMode: 0
+ m_CullingMask:
+ serializedVersion: 2
+ m_Bits: 4294967295
+ m_RenderingLayerMask: 1
+ m_Lightmapping: 4
+ m_LightShadowCasterMode: 0
+ m_AreaSize: {x: 1, y: 1}
+ m_BounceIntensity: 1
+ m_ColorTemperature: 6570
+ m_UseColorTemperature: 0
+ m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
+ m_UseBoundingSphereOverride: 0
+ m_UseViewFrustumForShadowCasterCull: 1
+ m_ForceVisible: 0
+ m_ShapeRadius: 0
+ m_ShadowAngle: 0
+ m_LightUnit: 1
+ m_LuxAtDistance: 1
+ m_EnableSpotReflector: 1
+--- !u!4 &203844589
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 203844586}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0}
+--- !u!1 &961739749
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 961739753}
+ - component: {fileID: 961739752}
+ - component: {fileID: 961739751}
+ - component: {fileID: 961739750}
+ m_Layer: 0
+ m_Name: Main Camera
+ m_TagString: MainCamera
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &961739750
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 961739749}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_RenderShadows: 1
+ m_RequiresDepthTextureOption: 2
+ m_RequiresOpaqueTextureOption: 2
+ m_CameraType: 0
+ m_Cameras: []
+ m_RendererIndex: -1
+ m_VolumeLayerMask:
+ serializedVersion: 2
+ m_Bits: 1
+ m_VolumeTrigger: {fileID: 0}
+ m_VolumeFrameworkUpdateModeOption: 2
+ m_RenderPostProcessing: 0
+ m_Antialiasing: 0
+ m_AntialiasingQuality: 2
+ m_StopNaN: 0
+ m_Dithering: 0
+ m_ClearDepth: 1
+ m_AllowXRRendering: 1
+ m_AllowHDROutput: 1
+ m_UseScreenCoordOverride: 0
+ m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0}
+ m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0}
+ m_RequiresDepthTexture: 0
+ m_RequiresColorTexture: 0
+ m_TaaSettings:
+ m_Quality: 3
+ m_FrameInfluence: 0.1
+ m_JitterScale: 1
+ m_MipBias: 0
+ m_VarianceClampScale: 0.9
+ m_ContrastAdaptiveSharpening: 0
+ m_Version: 2
+--- !u!81 &961739751
+AudioListener:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 961739749}
+ m_Enabled: 1
+--- !u!20 &961739752
+Camera:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 961739749}
+ m_Enabled: 1
+ serializedVersion: 2
+ m_ClearFlags: 1
+ m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
+ m_projectionMatrixMode: 1
+ m_GateFitMode: 2
+ m_FOVAxisMode: 0
+ m_Iso: 200
+ m_ShutterSpeed: 0.005
+ m_Aperture: 16
+ m_FocusDistance: 10
+ m_FocalLength: 50
+ m_BladeCount: 5
+ m_Curvature: {x: 2, y: 11}
+ m_BarrelClipping: 0.25
+ m_Anamorphism: 0
+ m_SensorSize: {x: 36, y: 24}
+ m_LensShift: {x: 0, y: 0}
+ m_NormalizedViewPortRect:
+ serializedVersion: 2
+ x: 0
+ y: 0
+ width: 1
+ height: 1
+ near clip plane: 0.3
+ far clip plane: 1000
+ field of view: 60
+ orthographic: 0
+ orthographic size: 5
+ m_Depth: -1
+ m_CullingMask:
+ serializedVersion: 2
+ m_Bits: 4294967295
+ m_RenderingPath: -1
+ m_TargetTexture: {fileID: 0}
+ m_TargetDisplay: 0
+ m_TargetEye: 3
+ m_HDR: 1
+ m_AllowMSAA: 1
+ m_AllowDynamicResolution: 0
+ m_ForceIntoRT: 0
+ m_OcclusionCulling: 1
+ m_StereoConvergence: 10
+ m_StereoSeparation: 0.022
+--- !u!4 &961739753
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 961739749}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &2030215725
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 2030215727}
+ - component: {fileID: 2030215726}
+ - component: {fileID: 2030215728}
+ m_Layer: 0
+ m_Name: LobbyService
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &2030215726
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2030215725}
+ 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: 1514133910
+ InScenePlacedSourceGlobalObjectIdHash: 0
+ DeferredDespawnTick: 0
+ Ownership: 1
+ AlwaysReplicateAsRoot: 0
+ SynchronizeTransform: 1
+ ActiveSceneSynchronization: 0
+ SceneMigrationSynchronization: 1
+ SpawnWithObservers: 1
+ DontDestroyWithOwner: 0
+ AutoObjectParentSync: 1
+ SyncOwnerTransformWhenParented: 1
+ AllowOwnerToParent: 0
+--- !u!4 &2030215727
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2030215725}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &2030215728
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2030215725}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 46ab03868ea4bd541bd6be3446c2bd3d, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.LobbyService
+ ShowTopMostFoldoutHeaderGroup: 1
+--- !u!1660057539 &9223372036854775807
+SceneRoots:
+ m_ObjectHideFlags: 0
+ m_Roots:
+ - {fileID: 961739753}
+ - {fileID: 203844589}
+ - {fileID: 2030215727}
+ - {fileID: 178257555}
diff --git a/Assets/_Project/Scenes/UI/Lobby.unity.meta b/Assets/_Project/Scenes/UI/Lobby.unity.meta
new file mode 100644
index 0000000..d8e1f46
--- /dev/null
+++ b/Assets/_Project/Scenes/UI/Lobby.unity.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 3a3639dca674a8049a570cb848bb69d2
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scenes/UI/MainMenu.unity b/Assets/_Project/Scenes/UI/MainMenu.unity
new file mode 100644
index 0000000..5baacea
--- /dev/null
+++ b/Assets/_Project/Scenes/UI/MainMenu.unity
@@ -0,0 +1,662 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!29 &1
+OcclusionCullingSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 2
+ m_OcclusionBakeSettings:
+ smallestOccluder: 5
+ smallestHole: 0.25
+ backfaceThreshold: 100
+ m_SceneGUID: 00000000000000000000000000000000
+ m_OcclusionCullingData: {fileID: 0}
+--- !u!104 &2
+RenderSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 10
+ m_Fog: 0
+ m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
+ m_FogMode: 3
+ m_FogDensity: 0.01
+ m_LinearFogStart: 0
+ m_LinearFogEnd: 300
+ m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
+ m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
+ m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
+ m_AmbientIntensity: 1
+ m_AmbientMode: 0
+ m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
+ m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
+ m_HaloStrength: 0.5
+ m_FlareStrength: 1
+ m_FlareFadeSpeed: 3
+ m_HaloTexture: {fileID: 0}
+ m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
+ m_DefaultReflectionMode: 0
+ m_DefaultReflectionResolution: 128
+ m_ReflectionBounces: 1
+ m_ReflectionIntensity: 1
+ m_CustomReflection: {fileID: 0}
+ m_Sun: {fileID: 0}
+ m_UseRadianceAmbientProbe: 0
+--- !u!157 &3
+LightmapSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 13
+ m_BakeOnSceneLoad: 0
+ m_GISettings:
+ serializedVersion: 2
+ m_BounceScale: 1
+ m_IndirectOutputScale: 1
+ m_AlbedoBoost: 1
+ m_EnvironmentLightingMode: 0
+ m_EnableBakedLightmaps: 1
+ m_EnableRealtimeLightmaps: 0
+ m_LightmapEditorSettings:
+ serializedVersion: 12
+ m_Resolution: 2
+ m_BakeResolution: 40
+ m_AtlasSize: 1024
+ m_AO: 0
+ m_AOMaxDistance: 1
+ m_CompAOExponent: 1
+ m_CompAOExponentDirect: 0
+ m_ExtractAmbientOcclusion: 0
+ m_Padding: 2
+ m_LightmapParameters: {fileID: 0}
+ m_LightmapsBakeMode: 1
+ m_TextureCompression: 1
+ m_ReflectionCompression: 2
+ m_MixedBakeMode: 2
+ m_BakeBackend: 1
+ m_PVRSampling: 1
+ m_PVRDirectSampleCount: 32
+ m_PVRSampleCount: 512
+ m_PVRBounces: 2
+ m_PVREnvironmentSampleCount: 256
+ m_PVREnvironmentReferencePointCount: 2048
+ m_PVRFilteringMode: 1
+ m_PVRDenoiserTypeDirect: 1
+ m_PVRDenoiserTypeIndirect: 1
+ m_PVRDenoiserTypeAO: 1
+ m_PVRFilterTypeDirect: 0
+ m_PVRFilterTypeIndirect: 0
+ m_PVRFilterTypeAO: 0
+ m_PVREnvironmentMIS: 1
+ m_PVRCulling: 1
+ m_PVRFilteringGaussRadiusDirect: 1
+ m_PVRFilteringGaussRadiusIndirect: 5
+ m_PVRFilteringGaussRadiusAO: 2
+ m_PVRFilteringAtrousPositionSigmaDirect: 0.5
+ m_PVRFilteringAtrousPositionSigmaIndirect: 2
+ m_PVRFilteringAtrousPositionSigmaAO: 1
+ m_ExportTrainingData: 0
+ m_TrainingDataDestination: TrainingData
+ m_LightProbeSampleCountMultiplier: 4
+ m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
+ m_LightingSettings: {fileID: 0}
+--- !u!196 &4
+NavMeshSettings:
+ serializedVersion: 2
+ m_ObjectHideFlags: 0
+ m_BuildSettings:
+ serializedVersion: 3
+ agentTypeID: 0
+ agentRadius: 0.5
+ agentHeight: 2
+ agentSlope: 45
+ agentClimb: 0.4
+ ledgeDropHeight: 0
+ maxJumpAcrossDistance: 0
+ minRegionArea: 2
+ manualCellSize: 0
+ cellSize: 0.16666667
+ manualTileSize: 0
+ tileSize: 256
+ buildHeightMesh: 0
+ maxJobWorkers: 0
+ preserveTilesOutsideBounds: 0
+ debug:
+ m_Flags: 0
+ m_NavMeshData: {fileID: 0}
+--- !u!1 &203844586
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 203844589}
+ - component: {fileID: 203844588}
+ - component: {fileID: 203844587}
+ m_Layer: 0
+ m_Name: Directional Light
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &203844587
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 203844586}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_UsePipelineSettings: 1
+ m_AdditionalLightsShadowResolutionTier: 2
+ m_CustomShadowLayers: 0
+ m_LightCookieSize: {x: 1, y: 1}
+ m_LightCookieOffset: {x: 0, y: 0}
+ m_SoftShadowQuality: 0
+ m_RenderingLayersMask:
+ serializedVersion: 0
+ m_Bits: 1
+ m_ShadowRenderingLayersMask:
+ serializedVersion: 0
+ m_Bits: 1
+ m_Version: 4
+ m_LightLayerMask: 1
+ m_ShadowLayerMask: 1
+ m_RenderingLayers: 1
+ m_ShadowRenderingLayers: 1
+--- !u!108 &203844588
+Light:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 203844586}
+ m_Enabled: 1
+ serializedVersion: 13
+ m_Type: 1
+ m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1}
+ m_Intensity: 1
+ m_Range: 10
+ m_SpotAngle: 30
+ m_InnerSpotAngle: 21.80208
+ m_CookieSize2D: {x: 10, y: 10}
+ m_Shadows:
+ m_Type: 2
+ m_Resolution: -1
+ m_CustomResolution: -1
+ m_Strength: 1
+ m_Bias: 0.05
+ m_NormalBias: 0.4
+ m_NearPlane: 0.2
+ m_CullingMatrixOverride:
+ e00: 1
+ e01: 0
+ e02: 0
+ e03: 0
+ e10: 0
+ e11: 1
+ e12: 0
+ e13: 0
+ e20: 0
+ e21: 0
+ e22: 1
+ e23: 0
+ e30: 0
+ e31: 0
+ e32: 0
+ e33: 1
+ m_UseCullingMatrixOverride: 0
+ m_Cookie: {fileID: 0}
+ m_DrawHalo: 0
+ m_Flare: {fileID: 0}
+ m_RenderMode: 0
+ m_CullingMask:
+ serializedVersion: 2
+ m_Bits: 4294967295
+ m_RenderingLayerMask: 1
+ m_Lightmapping: 4
+ m_LightShadowCasterMode: 0
+ m_AreaSize: {x: 1, y: 1}
+ m_BounceIntensity: 1
+ m_ColorTemperature: 6570
+ m_UseColorTemperature: 0
+ m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
+ m_UseBoundingSphereOverride: 0
+ m_UseViewFrustumForShadowCasterCull: 1
+ m_ForceVisible: 0
+ m_ShapeRadius: 0
+ m_ShadowAngle: 0
+ m_LightUnit: 1
+ m_LuxAtDistance: 1
+ m_EnableSpotReflector: 1
+--- !u!4 &203844589
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 203844586}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0}
+--- !u!1 &514623720
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 514623722}
+ - component: {fileID: 514623721}
+ m_Layer: 0
+ m_Name: RaceRegistry
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &514623721
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 514623720}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 50ccfdaa301b6a7439b4edfc750550aa, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.RaceRegistry
+ definitions:
+ - {fileID: 11400000, guid: c8b11f545e3990049a5953a34459f58a, type: 2}
+ - {fileID: 11400000, guid: 4d91bbd27e96fb845af6bb1bf0a22fe4, type: 2}
+--- !u!4 &514623722
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 514623720}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &626141751
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 626141754}
+ - component: {fileID: 626141753}
+ - component: {fileID: 626141752}
+ m_Layer: 5
+ m_Name: MainMenuUI
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &626141752
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 626141751}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 01199add5a12f4a4bb9a94d1e44fbb4d, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.UI.MainMenuController
+ defaultPort: 7777
+ defaultJoinAddress: 127.0.0.1
+--- !u!114 &626141753
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 626141751}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 19102, guid: 0000000000000000e000000000000000, type: 0}
+ m_Name:
+ m_EditorClassIdentifier: UnityEngine.dll::UnityEngine.UIElements.UIDocument
+ m_PanelSettings: {fileID: 11400000, guid: 6aa0af71585acea4db4995c3931dc946, type: 2}
+ m_ParentUI: {fileID: 0}
+ sourceAsset: {fileID: 0}
+ m_SortingOrder: 0
+ m_Position: 0
+ m_WorldSpaceSizeMode: 1
+ m_WorldSpaceWidth: 1920
+ m_WorldSpaceHeight: 1080
+ m_PivotReferenceSize: 0
+ m_Pivot: 0
+ m_WorldSpaceCollider: {fileID: 0}
+--- !u!4 &626141754
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 626141751}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &769692006
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 769692009}
+ - component: {fileID: 769692008}
+ - component: {fileID: 769692007}
+ m_Layer: 0
+ m_Name: NetworkManager
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &769692007
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 769692006}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 6960e84d07fb87f47956e7a81d71c4e6, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.Transports.UTP.UnityTransport
+ m_ProtocolType: 0
+ m_UseWebSockets: 0
+ m_UseEncryption: 0
+ m_MaxPacketQueueSize: 128
+ m_MaxPayloadSize: 6144
+ m_HeartbeatTimeoutMS: 500
+ m_ConnectTimeoutMS: 1000
+ m_MaxConnectAttempts: 60
+ m_DisconnectTimeoutMS: 30000
+ ConnectionData:
+ Address: 127.0.0.1
+ Port: 7777
+ WebSocketPath: /
+ ServerListenAddress: 127.0.0.1
+ ClientBindPort: 0
+ DebugSimulator:
+ PacketDelayMS: 0
+ PacketJitterMS: 0
+ PacketDropRate: 0
+--- !u!114 &769692008
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 769692006}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 593a2fe42fa9d37498c96f9a383b6521, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkManager
+ NetworkManagerExpanded: 0
+ NetworkConfig:
+ ProtocolVersion: 0
+ NetworkTransport: {fileID: 769692007}
+ PlayerPrefab: {fileID: 3493329038866903420, guid: 9a9c23b8584ab444aa5066a48579a9ec, type: 3}
+ Prefabs:
+ NetworkPrefabsLists:
+ - {fileID: 11400000, guid: 481ab1d7456efd044bc3e349aacd92ae, type: 2}
+ TickRate: 30
+ ClientConnectionBufferTimeout: 10
+ ConnectionApproval: 0
+ ConnectionData:
+ EnableTimeResync: 0
+ TimeResyncInterval: 30
+ EnsureNetworkVariableLengthSafety: 0
+ EnableSceneManagement: 1
+ ForceSamePrefabs: 1
+ RecycleNetworkIds: 1
+ NetworkIdRecycleDelay: 120
+ RpcHashSize: 0
+ LoadSceneTimeOut: 120
+ SpawnTimeout: 10
+ EnableNetworkLogs: 1
+ NetworkTopology: 0
+ UseCMBService: 0
+ AutoSpawnPlayerPrefabClientSide: 1
+ NetworkProfilingMetrics: 1
+ OldPrefabList: []
+ RunInBackground: 1
+ LogLevel: 1
+--- !u!4 &769692009
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 769692006}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &961739749
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 961739753}
+ - component: {fileID: 961739752}
+ - component: {fileID: 961739751}
+ - component: {fileID: 961739750}
+ m_Layer: 0
+ m_Name: Main Camera
+ m_TagString: MainCamera
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &961739750
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 961739749}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_RenderShadows: 1
+ m_RequiresDepthTextureOption: 2
+ m_RequiresOpaqueTextureOption: 2
+ m_CameraType: 0
+ m_Cameras: []
+ m_RendererIndex: -1
+ m_VolumeLayerMask:
+ serializedVersion: 2
+ m_Bits: 1
+ m_VolumeTrigger: {fileID: 0}
+ m_VolumeFrameworkUpdateModeOption: 2
+ m_RenderPostProcessing: 0
+ m_Antialiasing: 0
+ m_AntialiasingQuality: 2
+ m_StopNaN: 0
+ m_Dithering: 0
+ m_ClearDepth: 1
+ m_AllowXRRendering: 1
+ m_AllowHDROutput: 1
+ m_UseScreenCoordOverride: 0
+ m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0}
+ m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0}
+ m_RequiresDepthTexture: 0
+ m_RequiresColorTexture: 0
+ m_TaaSettings:
+ m_Quality: 3
+ m_FrameInfluence: 0.1
+ m_JitterScale: 1
+ m_MipBias: 0
+ m_VarianceClampScale: 0.9
+ m_ContrastAdaptiveSharpening: 0
+ m_Version: 2
+--- !u!81 &961739751
+AudioListener:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 961739749}
+ m_Enabled: 1
+--- !u!20 &961739752
+Camera:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 961739749}
+ m_Enabled: 1
+ serializedVersion: 2
+ m_ClearFlags: 1
+ m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
+ m_projectionMatrixMode: 1
+ m_GateFitMode: 2
+ m_FOVAxisMode: 0
+ m_Iso: 200
+ m_ShutterSpeed: 0.005
+ m_Aperture: 16
+ m_FocusDistance: 10
+ m_FocalLength: 50
+ m_BladeCount: 5
+ m_Curvature: {x: 2, y: 11}
+ m_BarrelClipping: 0.25
+ m_Anamorphism: 0
+ m_SensorSize: {x: 36, y: 24}
+ m_LensShift: {x: 0, y: 0}
+ m_NormalizedViewPortRect:
+ serializedVersion: 2
+ x: 0
+ y: 0
+ width: 1
+ height: 1
+ near clip plane: 0.3
+ far clip plane: 1000
+ field of view: 60
+ orthographic: 0
+ orthographic size: 5
+ m_Depth: -1
+ m_CullingMask:
+ serializedVersion: 2
+ m_Bits: 4294967295
+ m_RenderingPath: -1
+ m_TargetTexture: {fileID: 0}
+ m_TargetDisplay: 0
+ m_TargetEye: 3
+ m_HDR: 1
+ m_AllowMSAA: 1
+ m_AllowDynamicResolution: 0
+ m_ForceIntoRT: 0
+ m_OcclusionCulling: 1
+ m_StereoConvergence: 10
+ m_StereoSeparation: 0.022
+--- !u!4 &961739753
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 961739749}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &1648104262
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1648104264}
+ - component: {fileID: 1648104263}
+ m_Layer: 0
+ m_Name: SessionFlow
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &1648104263
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1648104262}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 474003b8e462dcc479e313f3d4f1cf12, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Net.SessionFlow
+--- !u!4 &1648104264
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1648104262}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1660057539 &9223372036854775807
+SceneRoots:
+ m_ObjectHideFlags: 0
+ m_Roots:
+ - {fileID: 961739753}
+ - {fileID: 203844589}
+ - {fileID: 769692009}
+ - {fileID: 1648104264}
+ - {fileID: 626141754}
+ - {fileID: 514623722}
diff --git a/Assets/_Project/Scenes/UI/MainMenu.unity.meta b/Assets/_Project/Scenes/UI/MainMenu.unity.meta
new file mode 100644
index 0000000..4a94fa6
--- /dev/null
+++ b/Assets/_Project/Scenes/UI/MainMenu.unity.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 1f4cb4a6391f5e94285960890282b70e
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Core/Enums.cs b/Assets/_Project/Scripts/Core/Enums.cs
index 80f9df8..78bb9aa 100644
--- a/Assets/_Project/Scripts/Core/Enums.cs
+++ b/Assets/_Project/Scripts/Core/Enums.cs
@@ -73,14 +73,24 @@ namespace TD.Core
}
///
- /// Identifies the race a player has chosen in the race-pick phase.
- /// Backed by byte. Specific race values are defined in Phase 1.8.
+ /// Stable identifier per race. Values 1-16 reserve slots for the planned
+ /// 16-race grid in the lobby; only races with a corresponding
+ /// RaceDefinition asset registered with RaceRegistry are
+ /// playable. Unregistered slots render as "Coming Soon" in the selection UI.
+ ///
+ /// Names are intentionally generic so display names / lore can be authored
+ /// on the asset without renaming the enum — renaming would change byte
+ /// values and break save data once persistence lands.
///
public enum RaceId : byte
{
/// No race selected yet (lobby / pre-pick).
None = 0,
- // Race entries added in Phase 1.8.
+
+ Race1 = 1, Race2 = 2, Race3 = 3, Race4 = 4,
+ Race5 = 5, Race6 = 6, Race7 = 7, Race8 = 8,
+ Race9 = 9, Race10 = 10, Race11 = 11, Race12 = 12,
+ Race13 = 13, Race14 = 14, Race15 = 15, Race16 = 16,
}
public enum PlayerSlot : byte
diff --git a/Assets/_Project/Scripts/Gameplay/PlayerBuilderSpawner.cs b/Assets/_Project/Scripts/Gameplay/PlayerBuilderSpawner.cs
index f204146..bc88852 100644
--- a/Assets/_Project/Scripts/Gameplay/PlayerBuilderSpawner.cs
+++ b/Assets/_Project/Scripts/Gameplay/PlayerBuilderSpawner.cs
@@ -1,15 +1,18 @@
// Assets/_Project/Scripts/Gameplay/PlayerBuilderSpawner.cs
+using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
+using UnityEngine.SceneManagement;
using TD.Core;
using TD.Levels;
+using TD.Net;
namespace TD.Gameplay
{
///
- /// Lives on the Player Prefab. On the server, when the player NetworkObject spawns,
- /// instantiates and spawns a separate NetworkObject owned by that
- /// player. The builder is positioned at the centroid of the player's zone before spawn.
+ /// Lives on the Player Prefab. On the server, spawns and re-spawns a separate
+ /// NetworkObject owned by this player each time the Match
+ /// scene loads. The builder is positioned at the centroid of the player's zone.
///
///
/// Why a separate NetworkObject? Multi-builder races (Path E) become "spawn
@@ -20,31 +23,79 @@ namespace TD.Gameplay
/// no-ops; they just receive the resulting Builder NetworkObject like any other
/// replicated spawn.
///
- /// Lifetime. The spawned builder is destroyed when the player NetworkObject
- /// despawns (e.g., disconnect). NGO does this automatically because we set
- /// destroyWithScene and store no other references — the builder's despawn cleans
- /// up the static registry in .
+ /// Lifetime & scene flow. The player NetworkObject persists across
+ /// scenes (MainMenu → Lobby → Match → Lobby …) because it's the
+ /// NetworkManager.PlayerPrefab. The Builder is spawned with
+ /// destroyWithScene: true so it's torn down on every scene unload — we
+ /// only want a builder in the Match scene.
+ /// re-spawns when entering Match; handles
+ /// disconnect cleanup.
+ ///
+ /// Race-driven builder prefab. If a exists
+ /// in the Match scene AND the player's selected has
+ /// a BuilderPrefab assigned, that prefab is used. Otherwise this falls back
+ /// to the inspector-assigned (the universal default).
+ /// Phase 1.8 races just need to fill in their BuilderPrefab field; no code
+ /// change required.
///
public class PlayerBuilderSpawner : NetworkBehaviour
{
- [Tooltip("Builder prefab to instantiate. Must be registered with the NetworkManager " +
- "as a network prefab.")]
+ [Tooltip("Default Builder prefab. Used when the player's race has no BuilderPrefab " +
+ "assigned (or when no RaceRegistry is present in the scene). Must be " +
+ "registered with the NetworkManager as a network prefab.")]
[SerializeField] private GameObject builderPrefab;
// Cached reference so we can despawn the builder if needed (e.g., player disconnects).
private NetworkObject spawnedBuilder;
+ // Track our scene-load subscription state so we can clean up correctly.
+ private bool sceneSubscribed;
+
public override void OnNetworkSpawn()
{
if (!IsServer) return;
if (builderPrefab == null)
{
- Debug.LogError("[PlayerBuilderSpawner] No Builder prefab assigned. " +
+ Debug.LogError("[PlayerBuilderSpawner] No default Builder prefab assigned. " +
"Cannot spawn builder for client " + OwnerClientId + ".");
return;
}
+ // Subscribe to NGO's scene-load completion so we re-spawn the builder
+ // every time the Match scene comes up (initial match start, Retry, etc.).
+ if (NetworkManager != null && NetworkManager.SceneManager != null)
+ {
+ NetworkManager.SceneManager.OnLoadEventCompleted += HandleSceneLoadCompleted;
+ sceneSubscribed = true;
+ }
+
+ // Edge case: the player connected while a match is already in progress.
+ // The Match scene is already loaded, so OnLoadEventCompleted won't fire
+ // again until the next transition. Spawn now.
+ if (SceneManager.GetActiveScene().name == SceneNames.Match)
+ TrySpawnBuilder();
+ }
+
+ // NGO fires this on the server once a scene load is acknowledged complete
+ // by every connected client (or timed out). We only act when the Match
+ // scene loads; Lobby / MainMenu loads are no-ops here.
+ private void HandleSceneLoadCompleted(string sceneName,
+ LoadSceneMode loadSceneMode,
+ List clientsCompleted,
+ List clientsTimedOut)
+ {
+ if (!IsServer) return;
+ if (sceneName != SceneNames.Match) return;
+ TrySpawnBuilder();
+ }
+
+ // Spawns the builder if it doesn't already exist. Defers to a SlotReady
+ // event if PlayerMatchState hasn't finished assigning the slot yet.
+ private void TrySpawnBuilder()
+ {
+ if (spawnedBuilder != null && spawnedBuilder.IsSpawned) return;
+
var pms = GetComponent();
if (pms == null)
{
@@ -53,8 +104,6 @@ namespace TD.Gameplay
return;
}
- // PlayerMatchState.OnNetworkSpawn may have already fired (component order: it first)
- // or may fire after us (component order: we first). Handle both cases.
if (pms.Slot != PlayerSlot.None)
SpawnBuilderForOwner(pms.Slot);
else
@@ -65,11 +114,21 @@ namespace TD.Gameplay
{
var pms = GetComponent();
if (pms != null) pms.SlotReady -= OnOwnerSlotReady;
- SpawnBuilderForOwner(slot);
+
+ // Only spawn if we're in the Match scene. SlotReady can fire in MainMenu
+ // (during initial connection) — we don't want a builder there.
+ if (SceneManager.GetActiveScene().name == SceneNames.Match)
+ SpawnBuilderForOwner(slot);
}
public override void OnNetworkDespawn()
{
+ if (sceneSubscribed && NetworkManager != null && NetworkManager.SceneManager != null)
+ {
+ NetworkManager.SceneManager.OnLoadEventCompleted -= HandleSceneLoadCompleted;
+ sceneSubscribed = false;
+ }
+
// When the player despawns (disconnect), also despawn their builder if it still exists.
if (IsServer && spawnedBuilder != null && spawnedBuilder.IsSpawned)
{
@@ -84,7 +143,19 @@ namespace TD.Gameplay
// Falls back to origin if loader/zone data isn't available.
Vector3 spawnPos = ComputeZoneCentroid(slot);
- var go = Instantiate(builderPrefab, spawnPos, Quaternion.identity);
+ // Pick the prefab: race-specific takes priority, default falls back.
+ // Falling back is what lets all races share the default builder
+ // during Phase 1.7 — each RaceDefinition just needs the same default
+ // assigned, and the spawner picks it up automatically.
+ GameObject prefab = ResolveBuilderPrefab();
+ if (prefab == null)
+ {
+ Debug.LogError("[PlayerBuilderSpawner] No builder prefab available. " +
+ "Set the default in the inspector or assign one to the player's race.");
+ return;
+ }
+
+ var go = Instantiate(prefab, spawnPos, Quaternion.identity);
var netObj = go.GetComponent();
if (netObj == null)
{
@@ -114,6 +185,22 @@ namespace TD.Gameplay
// ----- Helpers ----------------------------------------------------
+ // Picks the builder prefab to spawn for this player. Race-specific takes
+ // priority when (a) RaceRegistry is in the scene, (b) the player picked
+ // a race, and (c) that race's RaceDefinition has a BuilderPrefab assigned.
+ // Otherwise falls back to the inspector-assigned default.
+ private GameObject ResolveBuilderPrefab()
+ {
+ var pms = GetComponent();
+ if (pms != null && pms.RaceSelection != RaceId.None && RaceRegistry.Instance != null)
+ {
+ var raceDef = RaceRegistry.Instance.Get(pms.RaceSelection);
+ if (raceDef != null && raceDef.BuilderPrefab != null)
+ return raceDef.BuilderPrefab;
+ }
+ return builderPrefab;
+ }
+
private static Vector3 ComputeZoneCentroid(PlayerSlot slot)
{
var loader = LevelLoader.Instance;
diff --git a/Assets/_Project/Scripts/Gameplay/RaceDefinition.cs b/Assets/_Project/Scripts/Gameplay/RaceDefinition.cs
new file mode 100644
index 0000000..83aec06
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/RaceDefinition.cs
@@ -0,0 +1,75 @@
+// Assets/_Project/Scripts/Gameplay/RaceDefinition.cs
+using UnityEngine;
+using TD.Core;
+using TD.Towers;
+
+namespace TD.Gameplay
+{
+ ///
+ /// One asset per playable race. Holds the race's identity (the
+ /// binding for networked selection), the visual + lore
+ /// content shown in the lobby's race-selection UI, and stub fields for the
+ /// Phase 1.8 gameplay payload (race-specific builder + tower roster).
+ ///
+ ///
+ /// Creating a new race. Right-click in the project window →
+ /// Create → TD → Race Definition. Fill in the inspector:
+ ///
+ /// - Id — pick an unused value (Race1..Race16).
+ /// - Display Name — what shows in the grid + detail header.
+ /// - Icon — square sprite, ~256x256, drawn in the grid + detail.
+ /// - Builder Name / Description — text in the detail panel.
+ /// - Lore Text — longer description in the detail panel.
+ /// - Builder Prefab / Towers — stubs, wired in Phase 1.8.
+ ///
+ /// Then drag the asset into the RaceRegistry's Definitions
+ /// array on the scene's RaceRegistry GameObject.
+ ///
+ /// Why is serialized rather than inferred from the
+ /// asset name. Race selection is networked via a
+ /// -backed enum on
+ /// PlayerMatchState. The enum byte value is the wire identity; the
+ /// asset is just the local lookup. Keeping the binding explicit on the
+ /// asset prevents accidental drift if assets get renamed.
+ ///
+ [CreateAssetMenu(fileName = "RaceDefinition", menuName = "TD/Race Definition", order = 5)]
+ public class RaceDefinition : ScriptableObject
+ {
+ [Header("Identity")]
+ [Tooltip("Enum value used by PlayerMatchState.RaceSelection on the network. " +
+ "Pick an unused RaceId (Race1..Race16). Each asset must use a unique value.")]
+ public RaceId Id = RaceId.None;
+
+ [Tooltip("Race name shown in the lobby grid and detail panel header.")]
+ public string DisplayName;
+
+ [Tooltip("Square icon shown in the grid cell and as the larger image in the detail panel. " +
+ "~256x256 PNG works well; the UI scales to fit.")]
+ public Sprite Icon;
+
+ [Header("Builder")]
+ [Tooltip("Builder name shown in the detail panel (the builder is the in-match avatar " +
+ "that gates tower placement by proximity).")]
+ public string BuilderName;
+
+ [Tooltip("Short description of the builder shown beneath the name in the detail panel.")]
+ [TextArea(2, 4)]
+ public string BuilderDescription;
+
+ [Header("Lore")]
+ [Tooltip("Race lore / background shown in the detail panel. Lorem ipsum is fine " +
+ "for placeholder races; replace when actual writing is ready.")]
+ [TextArea(5, 15)]
+ public string LoreText;
+
+ [Header("Gameplay payload (Phase 1.8 — not wired yet)")]
+ [Tooltip("STUB (Phase 1.8): race-specific builder prefab. Currently every race spawns " +
+ "the default builder. When Phase 1.8 lands, PlayerBuilderSpawner will pick " +
+ "the prefab based on the player's RaceSelection.")]
+ public GameObject BuilderPrefab;
+
+ [Tooltip("STUB (Phase 1.8): tower roster available to this race. TowerRegistry will " +
+ "filter to this list when the active player belongs to this race.")]
+ public TowerDefinition[] Towers;
+ }
+}
diff --git a/Assets/_Project/Scripts/Gameplay/RaceDefinition.cs.meta b/Assets/_Project/Scripts/Gameplay/RaceDefinition.cs.meta
new file mode 100644
index 0000000..50dd0fd
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/RaceDefinition.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 98b7d5d116870f94da912007f6aa5cbb
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/RaceRegistry.cs b/Assets/_Project/Scripts/Gameplay/RaceRegistry.cs
new file mode 100644
index 0000000..562c5a7
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/RaceRegistry.cs
@@ -0,0 +1,152 @@
+// Assets/_Project/Scripts/Gameplay/RaceRegistry.cs
+using System.Collections.Generic;
+using UnityEngine;
+using TD.Core;
+
+namespace TD.Gameplay
+{
+ ///
+ /// Persistent (DontDestroyOnLoad) singleton that holds every
+ /// available in the current build and lets
+ /// any code look one up by .
+ ///
+ ///
+ /// Inspector setup. Place ONE RaceRegistry GameObject in
+ /// the MainMenu scene only. Drag every RaceDefinition asset
+ /// into the Definitions array. The registry marks itself
+ /// DontDestroyOnLoad on Awake, so it survives the scene transitions
+ /// MainMenu → Lobby → Match → back to Lobby and is available to all of
+ /// them through .
+ ///
+ /// Why not also in Lobby/Match scenes? Maintaining the
+ /// Definitions array in multiple places is a designer trap — update
+ /// one, forget the other, runtime mismatch. Single source of truth in
+ /// MainMenu eliminates that class of bug. Duplicate instances in other
+ /// scenes are detected in and self-destruct, so an
+ /// accidental copy doesn't break anything but does log a warning.
+ ///
+ /// Editor-only standalone testing. If you open the Lobby or
+ /// Match scene directly from the editor (without going through MainMenu
+ /// first), no RaceRegistry will exist and race-dependent code falls back
+ /// gracefully ( returns null; UI shows "Coming Soon" or
+ /// the default builder is used). For standalone-scene testing, you can
+ /// temporarily add a registry to whatever scene you're testing — but don't
+ /// commit it as part of normal play flow.
+ ///
+ /// Slot model. The lobby grid shows 16 slots (one per
+ /// value 1-16) regardless of how many are filled.
+ /// returns null for unregistered slots, which the UI
+ /// renders as a "Coming Soon" placeholder.
+ ///
+ /// Plain MonoBehaviour. Not a NetworkBehaviour — the registry
+ /// is identical on every peer (same ScriptableObject assets), so nothing
+ /// to sync. Network state tracks only the chosen on
+ /// PlayerMatchState; the rest is local lookup.
+ ///
+ public class RaceRegistry : MonoBehaviour
+ {
+ // ----- Singleton -------------------------------------------------
+
+ public static RaceRegistry Instance { get; private set; }
+
+ // ----- Inspector --------------------------------------------------
+
+ [Tooltip("All RaceDefinition assets available in this build. Drag each asset " +
+ "into the array. Duplicate Ids are rejected with a warning; null entries " +
+ "are skipped.")]
+ [SerializeField] private RaceDefinition[] definitions;
+
+ // ----- Internal lookup -------------------------------------------
+
+ private readonly Dictionary byId
+ = new Dictionary();
+
+ // ----- Lifecycle --------------------------------------------------
+
+ private void Awake()
+ {
+ // Persistent-singleton pattern: the FIRST instance to wake up wins
+ // and survives scene loads. Subsequent instances (e.g. a stale
+ // copy left over in the Lobby or Match scene) are self-destroyed,
+ // not just ignored — we want the scene to "self-heal" if someone
+ // accidentally drops a second copy in.
+ if (Instance != null && Instance != this)
+ {
+ Debug.LogWarning(
+ $"[RaceRegistry] Persistent instance already exists. " +
+ $"Destroying duplicate in scene '{gameObject.scene.name}'. " +
+ $"Keep RaceRegistry in the MainMenu scene only.");
+ Destroy(gameObject);
+ return;
+ }
+ Instance = this;
+ DontDestroyOnLoad(gameObject);
+ BuildLookup();
+ }
+
+ private void OnDestroy()
+ {
+ if (Instance == this) Instance = null;
+ }
+
+ // ----- Public API -------------------------------------------------
+
+ ///
+ /// Returns the for the given id, or null
+ /// if no asset is registered for that id (e.g. "Coming Soon" slots).
+ ///
+ public RaceDefinition Get(RaceId id)
+ {
+ byId.TryGetValue(id, out var def);
+ return def;
+ }
+
+ ///
+ /// Iterates the canonical 16 lobby slots (Race1..Race16). For each
+ /// slot returns either the registered or
+ /// null. UI consumers use this to render a stable 16-cell grid where
+ /// unfilled slots show a placeholder.
+ ///
+ public IEnumerable<(RaceId id, RaceDefinition def)> AllSlots()
+ {
+ for (int i = (int)RaceId.Race1; i <= (int)RaceId.Race16; i++)
+ {
+ var id = (RaceId)i;
+ yield return (id, Get(id));
+ }
+ }
+
+ // ----- Private ----------------------------------------------------
+
+ private void BuildLookup()
+ {
+ byId.Clear();
+ if (definitions == null || definitions.Length == 0)
+ {
+ Debug.LogWarning("[RaceRegistry] No RaceDefinition assets assigned. " +
+ "Drag assets into the Definitions array.");
+ return;
+ }
+
+ foreach (var def in definitions)
+ {
+ if (def == null) continue;
+ if (def.Id == RaceId.None)
+ {
+ Debug.LogWarning($"[RaceRegistry] '{def.name}' has Id=None — set it to " +
+ "an unused Race1..Race16 value.");
+ continue;
+ }
+ if (byId.ContainsKey(def.Id))
+ {
+ Debug.LogWarning($"[RaceRegistry] Duplicate Id '{def.Id}' detected on " +
+ $"'{def.name}'. Earlier registration kept; rename one.");
+ continue;
+ }
+ byId[def.Id] = def;
+ }
+
+ Debug.Log($"[RaceRegistry] Registered {byId.Count} race(s).");
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Gameplay/RaceRegistry.cs.meta b/Assets/_Project/Scripts/Gameplay/RaceRegistry.cs.meta
new file mode 100644
index 0000000..351abdb
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/RaceRegistry.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 50ccfdaa301b6a7439b4edfc750550aa
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/UI/LobbyController.cs b/Assets/_Project/Scripts/UI/LobbyController.cs
index e6906df..de2930b 100644
--- a/Assets/_Project/Scripts/UI/LobbyController.cs
+++ b/Assets/_Project/Scripts/UI/LobbyController.cs
@@ -1,5 +1,6 @@
// Assets/_Project/Scripts/UI/LobbyController.cs
using System.Linq;
+using System.Text;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.UIElements;
@@ -45,6 +46,22 @@ namespace TD.UI
private Button leaveButton;
private Label statusLabel;
+ // ----- Race selection overlay ------------------------------------
+
+ [Tooltip("Sibling RaceSelectionOverlay component that owns the race-pick UI. " +
+ "Auto-located on the same GameObject if not assigned.")]
+ [SerializeField] private RaceSelectionOverlay raceOverlay;
+
+ // Snapshot of player-list state from the last rebuild. RefreshPlayerList
+ // skips rebuilding when this matches the current frame's signature —
+ // critical because rebuilding every frame destroys the per-row buttons
+ // mid-click, and UI Toolkit's Clickable manipulator needs the same
+ // element instance to receive PointerDown AND PointerUp for the action
+ // to fire. Pre-fix, clicks on Select Race / Ready / Unready were silently
+ // lost because the button was destroyed between press and release.
+ private string lastPlayerListSignature = string.Empty;
+ private readonly StringBuilder signatureBuffer = new StringBuilder();
+
// ----- Lifecycle --------------------------------------------------
private void Start()
@@ -59,6 +76,13 @@ namespace TD.UI
}
BuildUI(root);
+
+ // Initialize the race overlay with our root so it can install its
+ // UI elements on top of the lobby. Hidden by default.
+ if (raceOverlay == null) raceOverlay = GetComponent();
+ if (raceOverlay != null) raceOverlay.Initialize(root);
+ else Debug.LogWarning("[LobbyController] No RaceSelectionOverlay component found — " +
+ "race-picker button will be disabled. Add one to this GameObject.");
}
private void Update()
@@ -131,16 +155,23 @@ namespace TD.UI
private void RefreshPlayerList()
{
+ // Sort by slot for stable ordering. AllPlayers is keyed by clientId
+ // which may not be slot-ordered.
+ var players = PlayerMatchState.AllPlayers.OrderBy(p => (int)p.Slot).ToList();
+
+ // Skip rebuild when the player-relevant state hasn't changed.
+ // Without this guard the per-row buttons are destroyed every frame,
+ // which loses clicks (see lastPlayerListSignature comment above).
+ string signature = ComputePlayerListSignature(players);
+ if (signature == lastPlayerListSignature) return;
+ lastPlayerListSignature = signature;
+
playerListContainer.Clear();
ulong localId = NetworkManager.Singleton != null
? NetworkManager.Singleton.LocalClientId
: ulong.MaxValue;
- // Sort by slot for stable ordering. AllPlayers is keyed by clientId
- // which may not be slot-ordered.
- var players = PlayerMatchState.AllPlayers.OrderBy(p => (int)p.Slot).ToList();
-
foreach (var pms in players)
{
bool isLocal = pms.OwnerClientId == localId;
@@ -151,6 +182,26 @@ namespace TD.UI
playerListContainer.Add(new Label("(no players connected)") { style = { color = Color.gray } });
}
+ // Compact signature of every field the player rows depend on. When this
+ // changes we rebuild; when it's identical we leave the existing rows
+ // (and their button event handlers) intact for click handling.
+ private string ComputePlayerListSignature(System.Collections.Generic.List players)
+ {
+ signatureBuffer.Clear();
+ foreach (var pms in players)
+ {
+ signatureBuffer.Append(pms.OwnerClientId);
+ signatureBuffer.Append(':');
+ signatureBuffer.Append((int)pms.Slot);
+ signatureBuffer.Append(':');
+ signatureBuffer.Append((int)pms.RaceSelection);
+ signatureBuffer.Append(':');
+ signatureBuffer.Append(pms.IsReady ? '1' : '0');
+ signatureBuffer.Append(';');
+ }
+ return signatureBuffer.ToString();
+ }
+
private VisualElement BuildPlayerRow(PlayerMatchState pms, bool isLocal)
{
var row = new VisualElement();
@@ -200,23 +251,17 @@ namespace TD.UI
// Local-only controls.
if (isLocal)
{
- // PLACEHOLDER race picker — the RaceId enum only has None right
- // now (Phase 1.8 will fill it). For now the single button submits
- // RaceId.None, which keeps the ready-up gate effectively a no-op
- // (the server requires RaceSelection != None to allow ready). To
- // exercise the flow end-to-end before races exist, comment out
- // the gate in PlayerMatchState.SubmitReadyRpc.
- //
- // TODO (Phase 1.8): replace with a dropdown of RaceDefinition
- // assets discovered at runtime, each option calling
- // pms.SubmitRaceRpc(definition.Id).
- var pickRaceBtn = new Button(() => pms.SubmitRaceRpc(RaceId.None))
+ // Race selection — opens the overlay that lets the player browse
+ // the 4x4 race grid and pick one. The overlay handles submission
+ // (PlayerMatchState.SubmitRaceRpc) directly, so we just open it.
+ var pickRaceBtn = new Button(OpenRaceOverlay)
{
- text = "Pick Race (stub)"
+ text = "Select Race"
};
pickRaceBtn.style.minWidth = 130;
pickRaceBtn.style.height = 28;
pickRaceBtn.style.marginLeft = 12;
+ pickRaceBtn.SetEnabled(raceOverlay != null);
row.Add(pickRaceBtn);
var readyBtn = new Button(() => pms.SubmitReadyRpc(!pms.IsReady))
@@ -247,6 +292,16 @@ namespace TD.UI
// ----- Button handlers --------------------------------------------
+ private void OpenRaceOverlay()
+ {
+ if (raceOverlay == null)
+ {
+ Debug.LogWarning("[LobbyController] Race overlay not assigned.");
+ return;
+ }
+ raceOverlay.Show();
+ }
+
private void OnStartMatchClicked()
{
var svc = LobbyService.Instance;
diff --git a/Assets/_Project/Scripts/UI/MainMenuController.cs b/Assets/_Project/Scripts/UI/MainMenuController.cs
index b72ab09..c971ef2 100644
--- a/Assets/_Project/Scripts/UI/MainMenuController.cs
+++ b/Assets/_Project/Scripts/UI/MainMenuController.cs
@@ -1,7 +1,10 @@
// Assets/_Project/Scripts/UI/MainMenuController.cs
+using System.Collections;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.UIElements;
+using TD.Core;
+using TD.Gameplay;
using TD.Net;
namespace TD.UI
@@ -44,6 +47,7 @@ namespace TD.UI
private Button hostButton;
private Button joinButton;
private Button quitButton;
+ private Button quickStartButton;
private VisualElement joinPanel;
private TextField joinAddressField;
private TextField joinPortField;
@@ -96,12 +100,14 @@ namespace TD.UI
buttonColumn.style.alignItems = Align.Center;
root.Add(buttonColumn);
- hostButton = MakeMenuButton("Host Game", OnHostClicked);
- joinButton = MakeMenuButton("Join Game", OnJoinClicked);
- quitButton = MakeMenuButton("Quit", OnQuitClicked);
+ hostButton = MakeMenuButton("Host Game", OnHostClicked);
+ joinButton = MakeMenuButton("Join Game", OnJoinClicked);
+ quitButton = MakeMenuButton("Quit", OnQuitClicked);
+ quickStartButton = MakeMenuButton("Quick Start", OnQuickStartClicked);
buttonColumn.Add(hostButton);
buttonColumn.Add(joinButton);
buttonColumn.Add(quitButton);
+ buttonColumn.Add(quickStartButton);
// Join sub-panel — hidden until Join is clicked. Holds the IP+port
// fields and the Connect / Cancel buttons.
@@ -120,14 +126,14 @@ namespace TD.UI
joinAddressField = new TextField("Host address");
joinAddressField.value = defaultJoinAddress;
joinAddressField.style.width = 280;
- joinAddressField.style.color = Color.white;
+ StyleJoinFieldText(joinAddressField);
joinPanel.Add(joinAddressField);
joinPortField = new TextField("Port");
joinPortField.value = defaultPort.ToString();
joinPortField.style.width = 280;
joinPortField.style.marginTop = 8;
- joinPortField.style.color = Color.white;
+ StyleJoinFieldText(joinPortField);
joinPanel.Add(joinPortField);
var joinButtons = new VisualElement();
@@ -154,6 +160,19 @@ namespace TD.UI
root.Add(statusLabel);
}
+ // TextField's visible text color lives on the inner "unity-text-input"
+ // element, not on the TextField root. Setting it on the root alone
+ // leaves the inner element's inherited white color in place — which is
+ // invisible against the default white input background. Same gotcha as
+ // the chat input's dark-styling path in HUDController.
+ private static void StyleJoinFieldText(TextField field)
+ {
+ field.style.color = Color.black;
+ var inner = field.Q("unity-text-input");
+ if (inner != null)
+ inner.style.color = Color.black;
+ }
+
private static Button MakeMenuButton(string text, System.Action onClick)
{
var btn = new Button(() => onClick?.Invoke()) { text = text };
@@ -229,5 +248,53 @@ namespace TD.UI
Application.Quit();
#endif
}
+
+ // Dev / testing shortcut: skips the lobby entirely. Hosts a single-player
+ // session, auto-selects Race1 for the local player, and loads the Match
+ // scene directly. Useful for iterating on gameplay without clicking
+ // through Host → Lobby → Pick Race → Ready → Start every time.
+ //
+ // To remove for a shipping build: delete the button-creation lines in
+ // BuildUI plus this method and its coroutine. No other consumers.
+ private void OnQuickStartClicked()
+ {
+ statusLabel.text = "Quick starting…";
+ StartCoroutine(QuickStartCoroutine());
+ }
+
+ private IEnumerator QuickStartCoroutine()
+ {
+ if (!NetworkBootstrap.StartHost(defaultPort))
+ {
+ statusLabel.text = "Failed to start host. Check the console.";
+ yield break;
+ }
+
+ // Wait for the local player's PlayerMatchState to spawn. Empirically
+ // this happens synchronously inside StartHost, but waiting one frame
+ // is cheap insurance against future NGO changes to spawn timing.
+ // Cap the wait at 60 frames so a real failure doesn't silently hang.
+ int safetyFrames = 0;
+ while (PlayerMatchState.Local == null && safetyFrames++ < 60)
+ yield return null;
+
+ var pms = PlayerMatchState.Local;
+ if (pms == null)
+ {
+ Debug.LogError("[MainMenuController] Quick Start: PlayerMatchState.Local " +
+ "didn't appear within 60 frames after StartHost. Aborting.");
+ statusLabel.text = "Quick Start failed: player did not spawn.";
+ yield break;
+ }
+
+ // Server-only setters — host is server + client, so these are valid
+ // directly. Skipping the RPC round-trip avoids any frame-of-latency
+ // before LoadSceneAsHost reads RaceSelection on the way into Match.
+ pms.SetRaceSelection(RaceId.Race1);
+ pms.SetReady(true);
+
+ // Skip Lobby — drop straight into the Match scene.
+ NetworkBootstrap.LoadSceneAsHost(SceneNames.Match);
+ }
}
}
diff --git a/Assets/_Project/Scripts/UI/RaceSelectionOverlay.cs b/Assets/_Project/Scripts/UI/RaceSelectionOverlay.cs
new file mode 100644
index 0000000..7cc8b6e
--- /dev/null
+++ b/Assets/_Project/Scripts/UI/RaceSelectionOverlay.cs
@@ -0,0 +1,510 @@
+// Assets/_Project/Scripts/UI/RaceSelectionOverlay.cs
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Unity.Netcode;
+using UnityEngine;
+using UnityEngine.InputSystem;
+using UnityEngine.UIElements;
+using TD.Core;
+using TD.Gameplay;
+
+namespace TD.UI
+{
+ ///
+ /// Lobby-scene overlay that lets the local player pick their race.
+ /// Shows a 4x4 grid of race icons + a detail panel that pops up to the
+ /// right when an icon is clicked. Reads other players' picks from
+ /// each frame to grey out
+ /// races that have already been taken.
+ ///
+ ///
+ /// Wiring. Attach this component to the same GameObject as
+ /// LobbyController. The controller calls
+ /// once with its root , and
+ /// when the "Select Race" button is clicked.
+ ///
+ /// Why same UIDocument. The overlay's elements are inserted as
+ /// absolute-positioned children of the lobby root — that way one UIDocument
+ /// + one PanelSettings serves both the lobby and the overlay, and z-order is
+ /// natural (overlay last → on top).
+ ///
+ /// Visual states per grid cell.
+ ///
+ /// - Free — race not picked by anyone. Icon in color, clickable.
+ /// - Taken by another player — greyed out, clickable for viewing
+ /// the detail panel but confirm button is disabled. Player slot
+ /// number overlaid in color so others know who claimed it.
+ /// - Taken by local player — bordered highlight, clickable.
+ /// Confirm button enabled (no-op if already selected, but lets the
+ /// player re-confirm). Local slot number overlaid.
+ /// - Unregistered slot — placeholder "Coming Soon", not
+ /// clickable. No RaceDefinition asset exists for this RaceId yet.
+ ///
+ ///
+ public class RaceSelectionOverlay : MonoBehaviour
+ {
+ // ----- UI state ---------------------------------------------------
+
+ private VisualElement overlayRoot;
+ private VisualElement gridContainer;
+ private VisualElement detailPanel;
+ private Label detailHeader;
+ private VisualElement detailIcon;
+ private Label detailBuilderName;
+ private Label detailBuilderDesc;
+ private Label detailLore;
+ private Button detailConfirmButton;
+ private Label detailUnavailableNote;
+
+ // Currently-viewed race in the detail panel. RaceId.None means no
+ // detail is open and the panel shows a hint instead.
+ private RaceId viewedRace = RaceId.None;
+
+ // Re-built each Update from PlayerMatchState data. Maps each taken
+ // RaceId to the slot of the player that picked it.
+ private readonly Dictionary picksByRace = new Dictionary();
+
+ // Snapshot of pick-state from the last grid rebuild. RefreshGrid skips
+ // rebuilding when this matches the current frame — same click-eating
+ // problem as the lobby's player list. Cleared on Show() so the grid
+ // always rebuilds when the overlay reopens.
+ private string lastGridSignature = string.Empty;
+ private readonly StringBuilder signatureBuffer = new StringBuilder();
+
+ // ----- Public API -------------------------------------------------
+
+ public bool IsVisible =>
+ overlayRoot != null && overlayRoot.style.display.value == DisplayStyle.Flex;
+
+ ///
+ /// Called by LobbyController once to attach the overlay's UI to the
+ /// lobby's UIDocument root. Builds the elements (hidden) and is then
+ /// reused across Show / Hide cycles.
+ ///
+ public void Initialize(VisualElement lobbyRoot)
+ {
+ if (lobbyRoot == null)
+ {
+ Debug.LogError("[RaceSelectionOverlay] Initialize received null lobby root.");
+ return;
+ }
+ BuildUI(lobbyRoot);
+ Hide();
+ }
+
+ ///
+ /// Reveals the overlay, refreshes the grid against current picks, and
+ /// auto-opens the detail panel for the local player's currently-picked
+ /// race (if any) so they can change it directly.
+ ///
+ public void Show()
+ {
+ if (overlayRoot == null) return;
+ overlayRoot.style.display = DisplayStyle.Flex;
+
+ // If the local player already has a race, seed the detail panel with
+ // it. Otherwise leave the panel showing the placeholder hint.
+ var local = PlayerMatchState.Local;
+ viewedRace = (local != null) ? local.RaceSelection : RaceId.None;
+
+ // Force a fresh grid build on each open so visuals reflect any
+ // picks that happened while the overlay was closed.
+ lastGridSignature = string.Empty;
+ RefreshGrid();
+ RefreshDetail();
+ }
+
+ public void Hide()
+ {
+ if (overlayRoot == null) return;
+ overlayRoot.style.display = DisplayStyle.None;
+ viewedRace = RaceId.None;
+ }
+
+ // ----- Lifecycle --------------------------------------------------
+
+ private void Update()
+ {
+ if (!IsVisible) return;
+
+ // Esc closes the overlay. Read directly via Input System — we want
+ // this even when nothing is focused.
+ var kb = Keyboard.current;
+ if (kb != null && kb.escapeKey.wasPressedThisFrame)
+ {
+ Hide();
+ return;
+ }
+
+ // Refresh against current picks every frame — cheap (max 16 cells,
+ // max 9 players) and means other-player changes show up immediately.
+ RefreshGrid();
+ RefreshDetail();
+ }
+
+ // ----- UI construction --------------------------------------------
+
+ private void BuildUI(VisualElement lobbyRoot)
+ {
+ // Root overlay — fills the screen with a dark backdrop.
+ overlayRoot = new VisualElement();
+ overlayRoot.pickingMode = PickingMode.Position;
+ overlayRoot.style.position = Position.Absolute;
+ overlayRoot.style.left = 0;
+ overlayRoot.style.right = 0;
+ overlayRoot.style.top = 0;
+ overlayRoot.style.bottom = 0;
+ overlayRoot.style.backgroundColor = new Color(0f, 0f, 0f, 0.85f);
+ overlayRoot.style.alignItems = Align.Center;
+ overlayRoot.style.justifyContent = Justify.Center;
+ lobbyRoot.Add(overlayRoot);
+
+ // Title + close button in a top bar.
+ var topBar = new VisualElement();
+ topBar.style.position = Position.Absolute;
+ topBar.style.top = 20;
+ topBar.style.left = 40;
+ topBar.style.right = 40;
+ topBar.style.flexDirection = FlexDirection.Row;
+ topBar.style.justifyContent = Justify.SpaceBetween;
+ topBar.style.alignItems = Align.Center;
+ overlayRoot.Add(topBar);
+
+ var title = new Label("Select Race");
+ title.style.fontSize = 32;
+ title.style.color = Color.white;
+ title.style.unityFontStyleAndWeight = FontStyle.Bold;
+ topBar.Add(title);
+
+ var closeBtn = new Button(Hide) { text = "X" };
+ closeBtn.style.width = 44;
+ closeBtn.style.height = 44;
+ closeBtn.style.fontSize = 20;
+ topBar.Add(closeBtn);
+
+ // Main content row: grid on the left, detail panel on the right.
+ var content = new VisualElement();
+ content.style.flexDirection = FlexDirection.Row;
+ content.style.alignItems = Align.Stretch;
+ content.style.marginTop = 40;
+ overlayRoot.Add(content);
+
+ // Grid container — 4x4 of cells. Uses wrap to flow rows.
+ gridContainer = new VisualElement();
+ gridContainer.style.flexDirection = FlexDirection.Row;
+ gridContainer.style.flexWrap = Wrap.Wrap;
+ gridContainer.style.width = 560; // 4 cells * (120 + 8 margin) = 512 + slack
+ gridContainer.style.marginRight = 32;
+ content.Add(gridContainer);
+
+ // Detail panel — built once, populated in RefreshDetail.
+ detailPanel = new VisualElement();
+ detailPanel.style.width = 380;
+ detailPanel.style.minHeight = 520;
+ detailPanel.style.paddingTop = 20;
+ detailPanel.style.paddingBottom = 20;
+ detailPanel.style.paddingLeft = 20;
+ detailPanel.style.paddingRight = 20;
+ detailPanel.style.backgroundColor = new Color(0.10f, 0.10f, 0.14f, 0.95f);
+ detailPanel.style.borderTopWidth = detailPanel.style.borderBottomWidth =
+ detailPanel.style.borderLeftWidth = detailPanel.style.borderRightWidth = 2;
+ var detailBorder = new Color(0.4f, 0.4f, 0.5f);
+ detailPanel.style.borderTopColor = detailPanel.style.borderBottomColor =
+ detailPanel.style.borderLeftColor = detailPanel.style.borderRightColor = detailBorder;
+ detailPanel.style.flexDirection = FlexDirection.Column;
+ content.Add(detailPanel);
+
+ detailHeader = new Label("Pick a race");
+ detailHeader.style.fontSize = 22;
+ detailHeader.style.color = Color.white;
+ detailHeader.style.unityFontStyleAndWeight = FontStyle.Bold;
+ detailHeader.style.marginBottom = 12;
+ detailPanel.Add(detailHeader);
+
+ detailIcon = new VisualElement();
+ detailIcon.style.width = 200;
+ detailIcon.style.height = 200;
+ detailIcon.style.alignSelf = Align.Center;
+ detailIcon.style.backgroundColor = new Color(0.18f, 0.18f, 0.22f);
+ detailIcon.style.marginBottom = 12;
+ detailPanel.Add(detailIcon);
+
+ detailBuilderName = new Label();
+ detailBuilderName.style.fontSize = 16;
+ detailBuilderName.style.color = new Color(0.95f, 0.85f, 0.5f);
+ detailBuilderName.style.unityFontStyleAndWeight = FontStyle.Bold;
+ detailPanel.Add(detailBuilderName);
+
+ detailBuilderDesc = new Label();
+ detailBuilderDesc.style.fontSize = 13;
+ detailBuilderDesc.style.color = new Color(0.85f, 0.85f, 0.85f);
+ detailBuilderDesc.style.marginBottom = 10;
+ detailBuilderDesc.style.whiteSpace = WhiteSpace.Normal;
+ detailPanel.Add(detailBuilderDesc);
+
+ detailLore = new Label();
+ detailLore.style.fontSize = 12;
+ detailLore.style.color = new Color(0.75f, 0.75f, 0.75f);
+ detailLore.style.marginBottom = 16;
+ detailLore.style.whiteSpace = WhiteSpace.Normal;
+ detailLore.style.flexGrow = 1;
+ detailPanel.Add(detailLore);
+
+ // Notice shown when the viewed race is taken by another player.
+ detailUnavailableNote = new Label();
+ detailUnavailableNote.style.color = new Color(1f, 0.5f, 0.3f);
+ detailUnavailableNote.style.fontSize = 12;
+ detailUnavailableNote.style.marginBottom = 8;
+ detailUnavailableNote.style.whiteSpace = WhiteSpace.Normal;
+ detailPanel.Add(detailUnavailableNote);
+
+ detailConfirmButton = new Button(OnConfirmClicked) { text = "Confirm Selection" };
+ detailConfirmButton.style.height = 40;
+ detailConfirmButton.style.fontSize = 16;
+ detailPanel.Add(detailConfirmButton);
+ }
+
+ // ----- Per-frame refresh ------------------------------------------
+
+ // Walks every PlayerMatchState and records which RaceId each non-None
+ // pick belongs to. Used by both the grid (grey out taken) and the
+ // detail panel (gate the confirm button).
+ private void RebuildPickIndex()
+ {
+ picksByRace.Clear();
+ foreach (var pms in PlayerMatchState.AllPlayers)
+ {
+ if (pms.RaceSelection == RaceId.None) continue;
+ picksByRace[pms.RaceSelection] = pms.Slot;
+ }
+ }
+
+ private void RefreshGrid()
+ {
+ if (gridContainer == null) return;
+ if (RaceRegistry.Instance == null) return;
+
+ RebuildPickIndex();
+
+ // Skip rebuild when nothing changed — keeps the cell event handlers
+ // alive across frames so clicks aren't eaten mid-press.
+ PlayerSlot localSlot = PlayerMatchState.Local != null
+ ? PlayerMatchState.Local.Slot
+ : PlayerSlot.None;
+ string signature = ComputeGridSignature(localSlot);
+ if (signature == lastGridSignature) return;
+ lastGridSignature = signature;
+
+ gridContainer.Clear();
+ foreach (var (id, def) in RaceRegistry.Instance.AllSlots())
+ {
+ gridContainer.Add(BuildCell(id, def, localSlot));
+ }
+ }
+
+ // Compact signature of every input the grid cells depend on: the
+ // current pick map (RaceId → PlayerSlot) plus the local player's slot
+ // (which affects "is this mine" highlighting on each cell).
+ private string ComputeGridSignature(PlayerSlot localSlot)
+ {
+ signatureBuffer.Clear();
+ signatureBuffer.Append((int)localSlot);
+ signatureBuffer.Append('|');
+ foreach (var kvp in picksByRace.OrderBy(p => (int)p.Key))
+ {
+ signatureBuffer.Append((int)kvp.Key);
+ signatureBuffer.Append(':');
+ signatureBuffer.Append((int)kvp.Value);
+ signatureBuffer.Append(';');
+ }
+ return signatureBuffer.ToString();
+ }
+
+ private VisualElement BuildCell(RaceId id, RaceDefinition def, PlayerSlot localSlot)
+ {
+ // Outer cell with fixed size for consistent grid layout.
+ var cell = new VisualElement();
+ cell.style.width = 120;
+ cell.style.height = 120;
+ cell.style.marginTop = cell.style.marginBottom =
+ cell.style.marginLeft = cell.style.marginRight = 4;
+ cell.style.borderTopWidth = cell.style.borderBottomWidth =
+ cell.style.borderLeftWidth = cell.style.borderRightWidth = 2;
+
+ bool isLocked = (def == null);
+ bool isPicked = picksByRace.TryGetValue(id, out var picker);
+ bool isMine = isPicked && picker == localSlot;
+ bool isTakenByOther = isPicked && !isMine;
+
+ // Border color signals state at a glance.
+ Color borderColor = isMine
+ ? new Color(0.3f, 0.85f, 0.3f) // green for "mine"
+ : isTakenByOther
+ ? new Color(0.6f, 0.2f, 0.2f) // red-ish for taken
+ : isLocked
+ ? new Color(0.3f, 0.3f, 0.3f) // dim for placeholder
+ : new Color(0.5f, 0.5f, 0.6f); // neutral for free
+ cell.style.borderTopColor = cell.style.borderBottomColor =
+ cell.style.borderLeftColor = cell.style.borderRightColor = borderColor;
+ cell.style.backgroundColor = new Color(0.10f, 0.10f, 0.14f);
+
+ // Icon area (or placeholder text for locked slots).
+ var iconHolder = new VisualElement();
+ iconHolder.pickingMode = PickingMode.Ignore;
+ iconHolder.style.flexGrow = 1;
+ iconHolder.style.alignItems = Align.Center;
+ iconHolder.style.justifyContent = Justify.Center;
+ iconHolder.style.unityBackgroundImageTintColor = isTakenByOther
+ ? new Color(0.4f, 0.4f, 0.4f) // greyed out
+ : Color.white;
+ if (def != null && def.Icon != null)
+ iconHolder.style.backgroundImage = new StyleBackground(def.Icon);
+ else if (isLocked)
+ {
+ var placeholder = new Label("?");
+ placeholder.style.fontSize = 36;
+ placeholder.style.color = new Color(0.4f, 0.4f, 0.4f);
+ iconHolder.Add(placeholder);
+ }
+ cell.Add(iconHolder);
+
+ // Race name strip at the bottom — visible even when icon is missing.
+ var nameLabel = new Label(def != null ? def.DisplayName : "Coming Soon");
+ nameLabel.pickingMode = PickingMode.Ignore;
+ nameLabel.style.unityTextAlign = TextAnchor.MiddleCenter;
+ nameLabel.style.fontSize = 11;
+ nameLabel.style.color = isLocked ? new Color(0.45f, 0.45f, 0.45f) : Color.white;
+ nameLabel.style.paddingTop = 2;
+ nameLabel.style.paddingBottom = 2;
+ nameLabel.style.backgroundColor = new Color(0f, 0f, 0f, 0.55f);
+ cell.Add(nameLabel);
+
+ // Picker badge — top-right corner, only when taken. Shows the slot
+ // number of whoever picked it.
+ if (isPicked)
+ {
+ var badge = new Label($"P{(int)picker}");
+ badge.pickingMode = PickingMode.Ignore;
+ badge.style.position = Position.Absolute;
+ badge.style.top = 4;
+ badge.style.right = 4;
+ badge.style.fontSize = 16;
+ badge.style.unityFontStyleAndWeight = FontStyle.Bold;
+ badge.style.color = isMine
+ ? new Color(0.3f, 0.85f, 0.3f)
+ : new Color(1f, 0.7f, 0.2f);
+ badge.style.paddingLeft = 4;
+ badge.style.paddingRight = 4;
+ badge.style.backgroundColor = new Color(0f, 0f, 0f, 0.7f);
+ cell.Add(badge);
+ }
+
+ // Click handler — only enabled for non-locked cells. Locked cells
+ // still get the visual treatment but no click response.
+ if (!isLocked)
+ {
+ cell.RegisterCallback(_ => OnCellClicked(id));
+ }
+
+ return cell;
+ }
+
+ // ----- Detail panel -----------------------------------------------
+
+ private void OnCellClicked(RaceId id)
+ {
+ viewedRace = id;
+ RefreshDetail();
+ }
+
+ private void RefreshDetail()
+ {
+ if (detailPanel == null) return;
+ var registry = RaceRegistry.Instance;
+ if (registry == null) return;
+
+ var def = registry.Get(viewedRace);
+
+ if (def == null)
+ {
+ // Empty / locked / no selection — show placeholder hint.
+ detailHeader.text = "Pick a race";
+ detailBuilderName.text = string.Empty;
+ detailBuilderDesc.text = string.Empty;
+ detailLore.text = "Click a race icon to see details.";
+ detailIcon.style.backgroundImage = null;
+ detailUnavailableNote.text = string.Empty;
+ detailConfirmButton.SetEnabled(false);
+ detailConfirmButton.text = "Confirm Selection";
+ return;
+ }
+
+ detailHeader.text = def.DisplayName;
+ detailBuilderName.text = string.IsNullOrEmpty(def.BuilderName)
+ ? "Builder: (unnamed)"
+ : $"Builder: {def.BuilderName}";
+ detailBuilderDesc.text = def.BuilderDescription ?? string.Empty;
+ detailLore.text = def.LoreText ?? string.Empty;
+ detailIcon.style.backgroundImage = def.Icon != null
+ ? new StyleBackground(def.Icon)
+ : null;
+
+ // Is this race available to the local player?
+ bool isPicked = picksByRace.TryGetValue(viewedRace, out var picker);
+ PlayerSlot localSlot = PlayerMatchState.Local != null
+ ? PlayerMatchState.Local.Slot
+ : PlayerSlot.None;
+ bool takenByOther = isPicked && picker != localSlot;
+ bool takenByMe = isPicked && picker == localSlot;
+
+ if (takenByOther)
+ {
+ detailUnavailableNote.text = $"Already selected by P{(int)picker}.";
+ detailConfirmButton.SetEnabled(false);
+ detailConfirmButton.text = "Unavailable";
+ }
+ else if (takenByMe)
+ {
+ detailUnavailableNote.text = "Your current selection.";
+ detailConfirmButton.SetEnabled(true);
+ detailConfirmButton.text = "Confirm Selection";
+ }
+ else
+ {
+ detailUnavailableNote.text = string.Empty;
+ detailConfirmButton.SetEnabled(true);
+ detailConfirmButton.text = "Confirm Selection";
+ }
+ }
+
+ private void OnConfirmClicked()
+ {
+ var local = PlayerMatchState.Local;
+ if (local == null)
+ {
+ Debug.LogWarning("[RaceSelectionOverlay] No local PlayerMatchState — cannot submit race.");
+ return;
+ }
+ if (viewedRace == RaceId.None) return;
+
+ // Re-check exclusivity right before submitting — between detail open
+ // and confirm click, another player may have grabbed it.
+ if (picksByRace.TryGetValue(viewedRace, out var picker) && picker != local.Slot)
+ {
+ Debug.Log($"[RaceSelectionOverlay] Race already taken by P{(int)picker}. " +
+ "Refreshing detail.");
+ RefreshDetail();
+ return;
+ }
+
+ local.SubmitRaceRpc(viewedRace);
+
+ // Per the spec: confirming clears the detail panel but keeps the
+ // overlay open. The grid will update to show the new picked state
+ // on the next Update tick.
+ viewedRace = RaceId.None;
+ RefreshDetail();
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/UI/RaceSelectionOverlay.cs.meta b/Assets/_Project/Scripts/UI/RaceSelectionOverlay.cs.meta
new file mode 100644
index 0000000..a9ba306
--- /dev/null
+++ b/Assets/_Project/Scripts/UI/RaceSelectionOverlay.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 9ab3ad11b37d4c54d9f310f5356d31ac
\ No newline at end of file
diff --git a/ProjectSettings/EditorBuildSettings.asset b/ProjectSettings/EditorBuildSettings.asset
index b9c569c..923345d 100644
--- a/ProjectSettings/EditorBuildSettings.asset
+++ b/ProjectSettings/EditorBuildSettings.asset
@@ -5,6 +5,12 @@ EditorBuildSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Scenes:
+ - enabled: 1
+ path: Assets/_Project/Scenes/UI/MainMenu.unity
+ guid: 1f4cb4a6391f5e94285960890282b70e
+ - enabled: 1
+ path: Assets/_Project/Scenes/UI/Lobby.unity
+ guid: 3a3639dca674a8049a570cb848bb69d2
- enabled: 1
path: Assets/_Project/Scenes/Levels/Main.unity
guid: 99c9720ab356a0642a771bea13969a05
diff --git a/ProjectSettings/EntitiesClientSettings.asset b/ProjectSettings/EntitiesClientSettings.asset
index baf6668..44f58f9 100644
--- a/ProjectSettings/EntitiesClientSettings.asset
+++ b/ProjectSettings/EntitiesClientSettings.asset
@@ -2,7 +2,7 @@
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &1
MonoBehaviour:
- m_ObjectHideFlags: 53
+ m_ObjectHideFlags: 61
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
diff --git a/Project_Roadmap.md b/Project_Roadmap.md
index 52afb7f..cb329f2 100644
--- a/Project_Roadmap.md
+++ b/Project_Roadmap.md
@@ -75,20 +75,24 @@ The most architecturally significant pending system. This blocks the Phase 2 tow
- 4-connected, no diagonals (matches existing maze validation)
- Re-pathing on tower placement/removal events. Fire on: `TowerInstance` spawn/despawn, construction-start, construction-finish, shelved-tower despawn.
-### 1.7 Lobby & Connection (CURRENT)
+### 1.7 Lobby & Connection (COMPLETE — 2026-05-17)
-Replaces the current bare "Start Host" button with a proper main menu → lobby → match flow.
+Replaced the bare "Start Host" button with a proper main menu → lobby → match flow.
Lobby is its own scene; players gather, pick races, ready up, and the host starts the match.
-**v1 scope (Direct IP, Option A host-leaves-closes-lobby):**
+**v1 shipped (Direct IP, Option A host-leaves-closes-lobby):**
-- `MainMenu` scene — Host / Join (with IP+port field) / Quit
-- `Lobby` scene — player list, per-player race picker, ready toggle, host-only Start button, Leave button
+- `MainMenu` scene — Host / Join (with IP+port field) / Quit / **Quick Start** (dev shortcut)
+- `Lobby` scene — player list, per-player **Select Race** button opening the race-selection overlay, ready toggle, host-only Start button, Leave button
- `LobbyService` `NetworkBehaviour` scene singleton — Start Match RPC, Return to Lobby RPC
- `NetworkBootstrap` static — wraps `UnityTransport` host/join/disconnect. Designed for the Steam swap-out below: same call sites, different transport.
-- `PlayerMatchState` extended with `IsReady` NetworkVariable and `SetReadyRpc` / `SetRaceRpc` for client → server submissions
+- `SessionFlow` DontDestroyOnLoad singleton — routes disconnected peers back to MainMenu on host loss
+- `PlayerMatchState` extended with `IsReady` NetworkVariable and `SubmitRaceRpc` / `SubmitReadyRpc` for client → server submissions
+- `PlayerBuilderSpawner` refactored to spawn on `NetworkSceneManager.OnLoadEventCompleted` for the Match scene, not on initial player spawn. Pulls race-specific `BuilderPrefab` from `RaceRegistry` when available, falls back to the inspector default. Re-spawns cleanly across Match → Lobby → Match cycles.
- Match-end overlay extended: **Retry** (back to lobby) AND **Return to Main Menu** (this player only)
-- Host leaves → server raises a "lobby closed" RPC; all clients shutdown + return to main menu. Same applies if the host clicks "Return to Main Menu" after a match — session ends for everyone, each player returns independently to their own main menu.
+- Host leaves → all clients return to main menu via `SessionFlow.OnClientDisconnectCallback` (Option A). Same applies if the host clicks "Return to Main Menu" after a match.
+- **Race data + selection UI** delivered as part of 1.7 (originally scoped for 1.8): `RaceId` enum expanded to 16 slots, `RaceDefinition` ScriptableObject (Id, DisplayName, Icon, BuilderName, BuilderDescription, LoreText, BuilderPrefab, Towers stub), `RaceRegistry` (DontDestroyOnLoad singleton in MainMenu — single source of truth across all scenes), `RaceSelectionOverlay` (4×4 grid + detail panel, taken-race greying with picker badge, Esc/X close, server-side exclusivity enforced via `SubmitRaceRpc`).
+- **Quick Start dev button** in MainMenu — bypasses lobby: hosts → waits for `PlayerMatchState.Local` → sets Race1 + ready → loads Match scene directly. Coroutine in `MainMenuController.QuickStartCoroutine`.
### 1.7-Future Steam Lobby Migration (Option C — DEFERRED)
@@ -119,16 +123,29 @@ Decision deferred per existing context document; Builder code is already terrain
Options previously discussed: Unity Terrain, mesh-based (Blender), ProBuilder, per-tile heights via volumes.
-### 1.8 Race system (Path E)
+### 1.8 Race system (Path E) — PARTIALLY COMPLETE
-The largest content-shaped piece of Phase 1. Required before Phase 2 because the prototype tower (Space Marine) is conceptually a unit of the Adeptus Astartes race, and the data model needs to reflect that.
+Data model and lobby selection UI shipped as part of 1.7 (2026-05-17). Remaining work is gameplay-side integration of the race payload during a match.
-- `RaceDefinition` `ScriptableObject` — race content bundle (tower roster, race-specific units, builder variants, starting bonuses)
-- Race-pick UI (in-match overlay) — grid of race cards, timer-based auto-lock. First concrete `MatchState` consumer.
-- Multi-builder race support — revisit `PlayerBuilderSpawner` to spawn N builders
-- `TowerRegistry` filtering by active match's race rosters
-- Auto-discovery of `RaceDefinition` assets (mirror the `Resources.LoadAll` pattern from `TowerRegistry`)
+**Already done (delivered with 1.7):**
+
+- `RaceDefinition` ScriptableObject with Identity / Builder / Lore / Gameplay-payload sections
+- `RaceRegistry` DontDestroyOnLoad singleton (placed in MainMenu, persists into Lobby + Match)
+- `RaceId` enum (None + Race1..Race16) — 16-slot grid reserved
+- Lobby race-selection overlay (4×4 grid + detail panel + exclusivity enforcement)
+- `PlayerMatchState.RaceSelection` NetworkVariable + `SubmitRaceRpc`
+- `PlayerBuilderSpawner` reads race-specific `BuilderPrefab` when available
+- Two placeholder RaceDefinition assets configured with the default builder (proves the data path works without race-specific content)
+
+**Still pending:**
+
+- Replace lobby race-pick with in-match race-pick overlay (timer-based auto-lock, `MatchState.RacePickTimer`) — OR retain lobby pick and remove the in-match flow concept. **Open decision** (see 1.10 reconciliation question).
+- Multi-builder race support — revisit `PlayerBuilderSpawner` to spawn N builders for races that need them
+- `TowerRegistry` filtering by active match's race rosters (currently shows all registered towers regardless of race)
+- Auto-discovery of `RaceDefinition` assets (today's flow: drag each asset into `RaceRegistry.Definitions` manually)
- Replace `TowerPlacementManager.towerDefinitions[]` inspector array with race-driven discovery
+- Race-specific builder prefabs (currently every race uses the same default builder)
+- Real race content (display names, lore, icons, distinct builder visuals) — placeholder races exist but are interchangeable
### 1.9 Camera polish