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

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