Adding new Races, Main Menu -> Lobby flow, and what's needed to set up multiplayer testing.

This commit is contained in:
Matt F 2026-05-17 23:31:02 -07:00
parent 60fa58b07f
commit fdada6f132
29 changed files with 2581 additions and 176 deletions

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2d10906489190644baf28d93e5197c42
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4fa63ce77f2808fb9a275dcfea9747c91e2c6e521589b1af1bf85d29ef7752ae
size 7854

View file

@ -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:

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9033f5e4e490ee5a82e550aae4f690eefc49463a804ecc91e2874a28e8f988d1
size 5726

View file

@ -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:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0d631899488b6db4a8d9af9a97a9b2d5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -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: []

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4d91bbd27e96fb845af6bb1bf0a22fe4
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View file

@ -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: []

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c8b11f545e3990049a5953a34459f58a
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View file

@ -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

View file

@ -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}

View file

@ -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}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 3a3639dca674a8049a570cb848bb69d2
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -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}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 1f4cb4a6391f5e94285960890282b70e
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -73,14 +73,24 @@ namespace TD.Core
}
/// <summary>
/// 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
/// <c>RaceDefinition</c> asset registered with <c>RaceRegistry</c> 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.
/// </summary>
public enum RaceId : byte
{
/// <summary>No race selected yet (lobby / pre-pick).</summary>
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

View file

@ -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
{
/// <summary>
/// Lives on the Player Prefab. On the server, when the player NetworkObject spawns,
/// instantiates and spawns a separate <see cref="Builder"/> NetworkObject owned by that
/// player. The builder is positioned at the centroid of the player's zone before spawn.
/// Lives on the Player Prefab. On the server, spawns and re-spawns a separate
/// <see cref="Builder"/> NetworkObject owned by this player each time the Match
/// scene loads. The builder is positioned at the centroid of the player's zone.
/// </summary>
/// <remarks>
/// <para><b>Why a separate NetworkObject?</b> 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.</para>
///
/// <para><b>Lifetime.</b> The spawned builder is destroyed when the player NetworkObject
/// despawns (e.g., disconnect). NGO does this automatically because we set
/// <c>destroyWithScene</c> and store no other references — the builder's despawn cleans
/// up the static registry in <see cref="Builder.OnNetworkDespawn"/>.</para>
/// <para><b>Lifetime &amp; scene flow.</b> The player NetworkObject persists across
/// scenes (MainMenu → Lobby → Match → Lobby …) because it's the
/// <c>NetworkManager.PlayerPrefab</c>. The Builder is spawned with
/// <c>destroyWithScene: true</c> so it's torn down on every scene unload — we
/// only want a builder in the Match scene. <see cref="HandleSceneLoadCompleted"/>
/// re-spawns when entering Match; <see cref="OnNetworkDespawn"/> handles
/// disconnect cleanup.</para>
///
/// <para><b>Race-driven builder prefab.</b> If a <see cref="RaceRegistry"/> exists
/// in the Match scene AND the player's selected <see cref="RaceDefinition"/> has
/// a <c>BuilderPrefab</c> assigned, that prefab is used. Otherwise this falls back
/// to the inspector-assigned <see cref="builderPrefab"/> (the universal default).
/// Phase 1.8 races just need to fill in their <c>BuilderPrefab</c> field; no code
/// change required.</para>
/// </remarks>
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<ulong> clientsCompleted,
List<ulong> 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<PlayerMatchState>();
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<PlayerMatchState>();
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<NetworkObject>();
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<PlayerMatchState>();
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;

View file

@ -0,0 +1,75 @@
// Assets/_Project/Scripts/Gameplay/RaceDefinition.cs
using UnityEngine;
using TD.Core;
using TD.Towers;
namespace TD.Gameplay
{
/// <summary>
/// One asset per playable race. Holds the race's identity (the
/// <see cref="RaceId"/> 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).
/// </summary>
/// <remarks>
/// <para><b>Creating a new race.</b> Right-click in the project window →
/// <c>Create → TD → Race Definition</c>. Fill in the inspector:
/// <list type="bullet">
/// <item><b>Id</b> — pick an unused <see cref="RaceId"/> value (Race1..Race16).</item>
/// <item><b>Display Name</b> — what shows in the grid + detail header.</item>
/// <item><b>Icon</b> — square sprite, ~256x256, drawn in the grid + detail.</item>
/// <item><b>Builder Name / Description</b> — text in the detail panel.</item>
/// <item><b>Lore Text</b> — longer description in the detail panel.</item>
/// <item><b>Builder Prefab / Towers</b> — <i>stubs</i>, wired in Phase 1.8.</item>
/// </list>
/// Then drag the asset into the <c>RaceRegistry</c>'s <c>Definitions</c>
/// array on the scene's <c>RaceRegistry</c> GameObject.</para>
///
/// <para><b>Why <see cref="Id"/> is serialized rather than inferred from the
/// asset name.</b> Race selection is networked via a
/// <see cref="UnityEngine.SerializeField"/>-backed enum on
/// <c>PlayerMatchState</c>. 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.</para>
/// </remarks>
[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;
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 98b7d5d116870f94da912007f6aa5cbb

View file

@ -0,0 +1,152 @@
// Assets/_Project/Scripts/Gameplay/RaceRegistry.cs
using System.Collections.Generic;
using UnityEngine;
using TD.Core;
namespace TD.Gameplay
{
/// <summary>
/// Persistent (DontDestroyOnLoad) singleton that holds every
/// <see cref="RaceDefinition"/> available in the current build and lets
/// any code look one up by <see cref="RaceId"/>.
/// </summary>
/// <remarks>
/// <para><b>Inspector setup.</b> Place ONE <c>RaceRegistry</c> GameObject in
/// the <b>MainMenu scene</b> only. Drag every <c>RaceDefinition</c> asset
/// into the <c>Definitions</c> array. The registry marks itself
/// <c>DontDestroyOnLoad</c> on Awake, so it survives the scene transitions
/// MainMenu → Lobby → Match → back to Lobby and is available to all of
/// them through <see cref="Instance"/>.</para>
///
/// <para><b>Why not also in Lobby/Match scenes?</b> Maintaining the
/// <c>Definitions</c> 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 <see cref="Awake"/> and self-destruct, so an
/// accidental copy doesn't break anything but does log a warning.</para>
///
/// <para><b>Editor-only standalone testing.</b> 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 (<see cref="Get"/> 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.</para>
///
/// <para><b>Slot model.</b> The lobby grid shows 16 slots (one per
/// <see cref="RaceId"/> value 1-16) regardless of how many are filled.
/// <see cref="Get"/> returns null for unregistered slots, which the UI
/// renders as a "Coming Soon" placeholder.</para>
///
/// <para><b>Plain MonoBehaviour.</b> Not a NetworkBehaviour — the registry
/// is identical on every peer (same ScriptableObject assets), so nothing
/// to sync. Network state tracks only the chosen <see cref="RaceId"/> on
/// <c>PlayerMatchState</c>; the rest is local lookup.</para>
/// </remarks>
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<RaceId, RaceDefinition> byId
= new Dictionary<RaceId, RaceDefinition>();
// ----- 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 -------------------------------------------------
/// <summary>
/// Returns the <see cref="RaceDefinition"/> for the given id, or null
/// if no asset is registered for that id (e.g. "Coming Soon" slots).
/// </summary>
public RaceDefinition Get(RaceId id)
{
byId.TryGetValue(id, out var def);
return def;
}
/// <summary>
/// Iterates the canonical 16 lobby slots (Race1..Race16). For each
/// slot returns either the registered <see cref="RaceDefinition"/> or
/// null. UI consumers use this to render a stable 16-cell grid where
/// unfilled slots show a placeholder.
/// </summary>
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).");
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 50ccfdaa301b6a7439b4edfc750550aa

View file

@ -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<RaceSelectionOverlay>();
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<PlayerMatchState> 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;

View file

@ -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);
}
}
}

View file

@ -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
{
/// <summary>
/// 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
/// <see cref="PlayerMatchState.AllPlayers"/> each frame to grey out
/// races that have already been taken.
/// </summary>
/// <remarks>
/// <para><b>Wiring.</b> Attach this component to the same GameObject as
/// <c>LobbyController</c>. The controller calls <see cref="Initialize"/>
/// once with its root <see cref="VisualElement"/>, and <see cref="Show"/>
/// when the "Select Race" button is clicked.</para>
///
/// <para><b>Why same UIDocument.</b> 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).</para>
///
/// <para><b>Visual states per grid cell.</b>
/// <list type="bullet">
/// <item><b>Free</b> — race not picked by anyone. Icon in color, clickable.</item>
/// <item><b>Taken by another player</b> — 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.</item>
/// <item><b>Taken by local player</b> — bordered highlight, clickable.
/// Confirm button enabled (no-op if already selected, but lets the
/// player re-confirm). Local slot number overlaid.</item>
/// <item><b>Unregistered slot</b> — placeholder "Coming Soon", not
/// clickable. No RaceDefinition asset exists for this RaceId yet.</item>
/// </list></para>
/// </remarks>
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<RaceId, PlayerSlot> picksByRace = new Dictionary<RaceId, PlayerSlot>();
// 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;
/// <summary>
/// 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.
/// </summary>
public void Initialize(VisualElement lobbyRoot)
{
if (lobbyRoot == null)
{
Debug.LogError("[RaceSelectionOverlay] Initialize received null lobby root.");
return;
}
BuildUI(lobbyRoot);
Hide();
}
/// <summary>
/// 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.
/// </summary>
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<ClickEvent>(_ => 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();
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9ab3ad11b37d4c54d9f310f5356d31ac

View file

@ -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

View file

@ -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}

View file

@ -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