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 / Towersstubs, 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