Adding 9 Player level
This commit is contained in:
parent
fdada6f132
commit
a7be12fa9b
30 changed files with 45984 additions and 300 deletions
|
|
@ -6,6 +6,7 @@ using UnityEngine;
|
|||
using UnityEngine.UIElements;
|
||||
using TD.Core;
|
||||
using TD.Gameplay;
|
||||
using TD.Levels;
|
||||
using TD.Net;
|
||||
|
||||
namespace TD.UI
|
||||
|
|
@ -41,11 +42,18 @@ namespace TD.UI
|
|||
{
|
||||
// ----- Cached UI elements -----------------------------------------
|
||||
|
||||
private VisualElement mapListContainer;
|
||||
private VisualElement playerListContainer;
|
||||
private Button startMatchButton;
|
||||
private Button leaveButton;
|
||||
private Label statusLabel;
|
||||
|
||||
// Signature snapshot for the map list, mirroring the player-list pattern.
|
||||
// Components: registry count, selected index, current player count, host flag.
|
||||
// Without this, the cards rebuild every frame and the Clickable manipulator
|
||||
// loses presses (same root cause as the player-list signature).
|
||||
private string lastMapListSignature = string.Empty;
|
||||
|
||||
// ----- Race selection overlay ------------------------------------
|
||||
|
||||
[Tooltip("Sibling RaceSelectionOverlay component that owns the race-pick UI. " +
|
||||
|
|
@ -88,6 +96,7 @@ namespace TD.UI
|
|||
private void Update()
|
||||
{
|
||||
if (playerListContainer == null) return;
|
||||
RefreshMapList();
|
||||
RefreshPlayerList();
|
||||
RefreshStartButton();
|
||||
}
|
||||
|
|
@ -112,6 +121,28 @@ namespace TD.UI
|
|||
title.style.marginBottom = 24;
|
||||
root.Add(title);
|
||||
|
||||
// Map selection panel — host clicks to change, everyone sees the current pick.
|
||||
// Horizontal row of cards rebuilt by RefreshMapList when its signature changes.
|
||||
var mapPanelLabel = new Label("Map");
|
||||
mapPanelLabel.style.fontSize = 18;
|
||||
mapPanelLabel.style.color = new Color(0.85f, 0.85f, 0.85f);
|
||||
mapPanelLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
mapPanelLabel.style.marginBottom = 6;
|
||||
root.Add(mapPanelLabel);
|
||||
|
||||
mapListContainer = new VisualElement();
|
||||
mapListContainer.style.flexDirection = FlexDirection.Row;
|
||||
mapListContainer.style.justifyContent = Justify.Center;
|
||||
mapListContainer.style.flexWrap = Wrap.Wrap;
|
||||
mapListContainer.style.minWidth = 520;
|
||||
mapListContainer.style.paddingTop = 8;
|
||||
mapListContainer.style.paddingBottom = 8;
|
||||
mapListContainer.style.paddingLeft = 8;
|
||||
mapListContainer.style.paddingRight = 8;
|
||||
mapListContainer.style.backgroundColor = new Color(0f, 0f, 0f, 0.4f);
|
||||
mapListContainer.style.marginBottom = 24;
|
||||
root.Add(mapListContainer);
|
||||
|
||||
// Player list container (rebuilt every Update from AllPlayers).
|
||||
playerListContainer = new VisualElement();
|
||||
playerListContainer.style.flexDirection = FlexDirection.Column;
|
||||
|
|
@ -153,6 +184,157 @@ namespace TD.UI
|
|||
|
||||
// ----- Per-frame refresh ------------------------------------------
|
||||
|
||||
private void RefreshMapList()
|
||||
{
|
||||
// Inputs that determine the rendered state of the map row. Anything not in this
|
||||
// signature won't trigger a rebuild — match what BuildMapCard reads.
|
||||
var registry = MapRegistry.Instance;
|
||||
var svc = LobbyService.Instance;
|
||||
int registryCount = registry != null ? registry.Count : 0;
|
||||
int selectedIndex = svc != null ? svc.SelectedMapIndex : -1;
|
||||
int playerCount = LobbyService.CountConnectedPlayers();
|
||||
bool isHost = NetworkManager.Singleton != null && NetworkManager.Singleton.IsHost;
|
||||
|
||||
string signature = $"{registryCount}:{selectedIndex}:{playerCount}:{(isHost ? 'H' : 'C')}";
|
||||
if (signature == lastMapListSignature) return;
|
||||
lastMapListSignature = signature;
|
||||
|
||||
mapListContainer.Clear();
|
||||
|
||||
if (registry == null || registryCount == 0)
|
||||
{
|
||||
var emptyLabel = new Label("(no maps available — MapRegistry missing)");
|
||||
emptyLabel.style.color = new Color(0.7f, 0.5f, 0.5f);
|
||||
emptyLabel.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||||
mapListContainer.Add(emptyLabel);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < registryCount; i++)
|
||||
{
|
||||
var map = registry.Get(i);
|
||||
if (map == null) continue;
|
||||
|
||||
bool isSelected = i == selectedIndex;
|
||||
bool fitsLobby = MapRegistry.IsSelectableFor(map, playerCount);
|
||||
bool clickable = isHost && fitsLobby;
|
||||
|
||||
mapListContainer.Add(BuildMapCard(map, i, isSelected, fitsLobby, clickable, playerCount));
|
||||
}
|
||||
}
|
||||
|
||||
private VisualElement BuildMapCard(LevelData map, int index, bool isSelected,
|
||||
bool fitsLobby, bool clickable, int playerCount)
|
||||
{
|
||||
var card = new VisualElement();
|
||||
card.style.width = 180;
|
||||
card.style.height = 220;
|
||||
card.style.marginRight = 8;
|
||||
card.style.marginLeft = 8;
|
||||
card.style.paddingTop = 8;
|
||||
card.style.paddingBottom = 8;
|
||||
card.style.paddingLeft = 8;
|
||||
card.style.paddingRight = 8;
|
||||
card.style.alignItems = Align.Center;
|
||||
card.style.justifyContent = Justify.FlexStart;
|
||||
|
||||
// Background reflects selectability: highlighted when selected, dim when oversized.
|
||||
Color background = isSelected
|
||||
? new Color(0.20f, 0.32f, 0.55f)
|
||||
: new Color(0.12f, 0.12f, 0.16f);
|
||||
if (!fitsLobby) background.a *= 0.7f;
|
||||
card.style.backgroundColor = background;
|
||||
|
||||
// Border to make the selection visually unambiguous.
|
||||
float borderWidth = isSelected ? 3f : 1f;
|
||||
Color borderColor = isSelected
|
||||
? new Color(0.45f, 0.70f, 1f)
|
||||
: new Color(0.25f, 0.25f, 0.30f);
|
||||
card.style.borderTopWidth = borderWidth;
|
||||
card.style.borderBottomWidth = borderWidth;
|
||||
card.style.borderLeftWidth = borderWidth;
|
||||
card.style.borderRightWidth = borderWidth;
|
||||
card.style.borderTopColor = borderColor;
|
||||
card.style.borderBottomColor = borderColor;
|
||||
card.style.borderLeftColor = borderColor;
|
||||
card.style.borderRightColor = borderColor;
|
||||
|
||||
// Thumbnail (top of card). Falls back to a neutral placeholder if MapThumbnail isn't set.
|
||||
var thumb = new VisualElement();
|
||||
thumb.style.width = 140;
|
||||
thumb.style.height = 105;
|
||||
thumb.style.marginBottom = 6;
|
||||
thumb.style.backgroundColor = new Color(0.05f, 0.05f, 0.08f);
|
||||
if (map.MapThumbnail != null)
|
||||
{
|
||||
thumb.style.backgroundImage = new StyleBackground(map.MapThumbnail);
|
||||
}
|
||||
// Dim the thumbnail when the map doesn't fit the lobby — reinforces the disabled look.
|
||||
if (!fitsLobby)
|
||||
{
|
||||
thumb.style.opacity = 0.45f;
|
||||
}
|
||||
card.Add(thumb);
|
||||
|
||||
// Map name.
|
||||
var nameLabel = new Label(map.MapName);
|
||||
nameLabel.style.color = fitsLobby ? Color.white : new Color(0.7f, 0.6f, 0.6f);
|
||||
nameLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
nameLabel.style.fontSize = 14;
|
||||
nameLabel.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||||
card.Add(nameLabel);
|
||||
|
||||
// Player count label — tells the user the capacity, and why the card is greyed out
|
||||
// when oversized. Live player count is shown alongside the cap so it's obvious why.
|
||||
string countText = fitsLobby
|
||||
? $"Up to {map.PlayerCount} players"
|
||||
: $"Up to {map.PlayerCount} — needs ≤ {map.PlayerCount} (lobby has {playerCount})";
|
||||
var countLabel = new Label(countText);
|
||||
countLabel.style.color = fitsLobby
|
||||
? new Color(0.75f, 0.75f, 0.75f)
|
||||
: new Color(0.85f, 0.55f, 0.45f);
|
||||
countLabel.style.fontSize = 11;
|
||||
countLabel.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||||
countLabel.style.marginTop = 2;
|
||||
countLabel.style.whiteSpace = WhiteSpace.Normal;
|
||||
card.Add(countLabel);
|
||||
|
||||
// Make the whole card clickable for host (when the map fits the lobby). Non-hosts
|
||||
// see the cards but can't change selection. Capture `index` in a local so the
|
||||
// closure doesn't bind to the loop variable.
|
||||
if (clickable)
|
||||
{
|
||||
int capturedIndex = index;
|
||||
card.RegisterCallback<ClickEvent>(_ => OnMapCardClicked(capturedIndex));
|
||||
// Standard hover affordance to advertise interactivity.
|
||||
card.RegisterCallback<MouseEnterEvent>(_ =>
|
||||
{
|
||||
if (!isSelected)
|
||||
card.style.backgroundColor = new Color(0.17f, 0.20f, 0.28f);
|
||||
});
|
||||
card.RegisterCallback<MouseLeaveEvent>(_ =>
|
||||
{
|
||||
if (!isSelected)
|
||||
card.style.backgroundColor = new Color(0.12f, 0.12f, 0.16f);
|
||||
});
|
||||
}
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
private void OnMapCardClicked(int index)
|
||||
{
|
||||
var svc = LobbyService.Instance;
|
||||
if (svc == null)
|
||||
{
|
||||
Debug.LogError("[LobbyController] LobbyService.Instance is null — cannot change map.");
|
||||
return;
|
||||
}
|
||||
// Don't bother round-tripping if we're already on this map.
|
||||
if (svc.SelectedMapIndex == index) return;
|
||||
svc.RequestSelectMapRpc(index);
|
||||
}
|
||||
|
||||
private void RefreshPlayerList()
|
||||
{
|
||||
// Sort by slot for stable ordering. AllPlayers is keyed by clientId
|
||||
|
|
@ -285,7 +467,32 @@ namespace TD.UI
|
|||
startMatchButton.style.display = isHost ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
if (!isHost) return;
|
||||
|
||||
bool canStart = LobbyService.AreAllPlayersReady(out string reason);
|
||||
// Two gates: readiness (existing) and map selectability (new). Show whichever
|
||||
// failure is preventing the start so the host knows what to fix. Readiness
|
||||
// is checked first because it's the more common blocker.
|
||||
bool readyOk = LobbyService.AreAllPlayersReady(out string readyReason);
|
||||
string reason = readyOk ? string.Empty : readyReason;
|
||||
|
||||
bool mapOk = true;
|
||||
if (readyOk)
|
||||
{
|
||||
var svc = LobbyService.Instance;
|
||||
var selected = svc != null ? svc.SelectedMap : null;
|
||||
int playerCount = LobbyService.CountConnectedPlayers();
|
||||
if (selected == null)
|
||||
{
|
||||
mapOk = false;
|
||||
reason = "No map selected.";
|
||||
}
|
||||
else if (!MapRegistry.IsSelectableFor(selected, playerCount))
|
||||
{
|
||||
mapOk = false;
|
||||
reason = $"'{selected.MapName}' supports up to {selected.PlayerCount} " +
|
||||
$"players; lobby has {playerCount}.";
|
||||
}
|
||||
}
|
||||
|
||||
bool canStart = readyOk && mapOk;
|
||||
startMatchButton.SetEnabled(canStart);
|
||||
statusLabel.text = canStart ? string.Empty : reason;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -191,13 +191,13 @@ namespace TD.UI.Minimap
|
|||
terrainLayer.style.backgroundImage =
|
||||
new StyleBackground(Background.FromTexture2D(bakedTerrain));
|
||||
|
||||
// World extents of the baked rectangle. Tile (n) covers world n - 0.5 to n + 0.5.
|
||||
// World extents of the baked rectangle. Tile (n) spans world [n, n+1] (edge-aligned),
|
||||
// so the rectangle covers [GridOriginTile, GridOriginTile + GridSize] on each axis.
|
||||
var data = loader.LevelData;
|
||||
float halfTile = GridCoordinates.TILE_SIZE * 0.5f;
|
||||
float minX = data.GridOriginTile.x * GridCoordinates.TILE_SIZE - halfTile;
|
||||
float maxX = (data.GridOriginTile.x + data.GridSize.x) * GridCoordinates.TILE_SIZE - halfTile;
|
||||
float minZ = data.GridOriginTile.y * GridCoordinates.TILE_SIZE - halfTile;
|
||||
float maxZ = (data.GridOriginTile.y + data.GridSize.y) * GridCoordinates.TILE_SIZE - halfTile;
|
||||
float minX = data.GridOriginTile.x * GridCoordinates.TILE_SIZE;
|
||||
float maxX = (data.GridOriginTile.x + data.GridSize.x) * GridCoordinates.TILE_SIZE;
|
||||
float minZ = data.GridOriginTile.y * GridCoordinates.TILE_SIZE;
|
||||
float maxZ = (data.GridOriginTile.y + data.GridSize.y) * GridCoordinates.TILE_SIZE;
|
||||
worldMin = new Vector2(minX, minZ);
|
||||
worldMax = new Vector2(maxX, maxZ);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue