From 586d364772bf0b1428bb16304121d84b2de639d3 Mon Sep 17 00:00:00 2001 From: Marek Sorokin Date: Sun, 5 Oct 2025 19:01:48 +0200 Subject: [PATCH] lobby tweaking --- Networking/Steam/SteamLobbyService.cs | 1 + UI/Scripts/UGUIMultiplayerLobbyController.cs | 204 ++++++++++++++++++- 2 files changed, 200 insertions(+), 5 deletions(-) diff --git a/Networking/Steam/SteamLobbyService.cs b/Networking/Steam/SteamLobbyService.cs index 11fe169..e680096 100644 --- a/Networking/Steam/SteamLobbyService.cs +++ b/Networking/Steam/SteamLobbyService.cs @@ -461,6 +461,7 @@ namespace MegaKoop.Steam public void OpenFriendsOverlay() { Debug.Log("[SteamLobbyService] OpenFriendsOverlay stub"); } public IReadOnlyList<(string steamId, string name, bool online)> GetFriends() => new List<(string, string, bool)>(); public void InviteFriendBySteamId(string steamId) { Debug.Log("[SteamLobbyService] InviteFriendBySteamId stub"); } + public bool IsKickedLocal() => false; public IReadOnlyList<(string steamId, string name, bool isReady, bool isHost)> GetMembers() => new List<(string,string,bool,bool)>(); public void JoinLobbyByCode(string code) { _isInLobby = true; IsHost = false; LobbyCode = code; OnLobbyEntered?.Invoke(); } } diff --git a/UI/Scripts/UGUIMultiplayerLobbyController.cs b/UI/Scripts/UGUIMultiplayerLobbyController.cs index 897f7e3..d4ff621 100644 --- a/UI/Scripts/UGUIMultiplayerLobbyController.cs +++ b/UI/Scripts/UGUIMultiplayerLobbyController.cs @@ -64,9 +64,17 @@ namespace MegaKoop.UI private Button btnLeaveLobby; private Button btnBackFromLobby; + [Header("Player Ready Styling")] + [SerializeField] private Color readyBorderColor = new Color(0.2f, 0.82f, 0.35f, 1f); + [SerializeField] private Color notReadyBorderColor = new Color(0.85f, 0.25f, 0.25f, 1f); + [SerializeField] private float avatarBorderThickness = 12f; + // Selection private string selectedPlayerSteamId = string.Empty; + // Cached readiness state per member + private readonly Dictionary memberReadyCache = new Dictionary(); + // Steam service private SteamLobbyService steam; @@ -364,6 +372,7 @@ namespace MegaKoop.UI private void OnLobbyCreated() { selectedPlayerSteamId = string.Empty; + memberReadyCache.Clear(); UpdateUIFromSteam(); // Auto-open invite overlay for the host if (steam != null && steam.IsInLobby && steam.IsHost) @@ -376,6 +385,7 @@ namespace MegaKoop.UI private void OnLobbyEntered() { selectedPlayerSteamId = string.Empty; + memberReadyCache.Clear(); UpdateUIFromSteam(); // Auto-open invite overlay if we are the host entering our lobby if (steam != null && steam.IsInLobby && steam.IsHost) @@ -388,6 +398,7 @@ namespace MegaKoop.UI private void OnLobbyLeft() { selectedPlayerSteamId = string.Empty; + memberReadyCache.Clear(); UpdateUIFromSteam(); } @@ -427,9 +438,24 @@ namespace MegaKoop.UI if (av) { var img = av.GetComponent(); - if (img && steam.TryGetAvatarSprite(steamId, out var spr2, true)) + if (img) { - img.sprite = spr2; img.color = Color.white; img.preserveAspect = true; + if (steam.TryGetAvatarSprite(steamId, out var spr2, true)) + { + img.sprite = spr2; + img.color = Color.white; + img.preserveAspect = true; + } + else + { + img.sprite = null; + img.color = new Color(0.3f, 0.6f, 0.3f, 1f); + } + + if (TryGetMemberReadyState(steamId, out var isReady)) + { + ApplyReadyBorder(img, isReady); + } } } } @@ -572,12 +598,19 @@ namespace MegaKoop.UI { if (IsHost || !IsInLobby || steam == null) return; var members = GetSteamMembers(); + CacheMemberReadyStates(members); var localId = steam.LocalSteamIdString; bool current = false; var me = members.FirstOrDefault(m => m.steamId == localId); // When tuple is default, steamId will be null; guard if (me.steamId == localId) current = me.isReady; - steam.SetReady(!current); + bool newReadyState = !current; + steam.SetReady(newReadyState); + if (!string.IsNullOrEmpty(localId)) + { + memberReadyCache[localId] = newReadyState; + UpdateAvatarUIForSteamId(localId); + } UpdateUIFromSteam(); } @@ -664,7 +697,15 @@ namespace MegaKoop.UI private void UpdateVisibility() { - if (btnStartGame) btnStartGame.gameObject.SetActive(IsInLobby && IsHost); + if (btnStartGame) + { + bool canHostStart = IsInLobby && IsHost; + btnStartGame.gameObject.SetActive(canHostStart); + if (btnStartGame.gameObject.activeSelf) + { + btnStartGame.interactable = AreAllPlayersReady(); + } + } if (btnLeaveLobby) btnLeaveLobby.gameObject.SetActive(IsInLobby); if (btnToggleReady) btnToggleReady.gameObject.SetActive(IsInLobby && !IsHost); if (btnInviteFriends) btnInviteFriends.gameObject.SetActive(IsInLobby && IsHost); @@ -681,6 +722,137 @@ namespace MegaKoop.UI return steam != null ? steam.GetMembers().ToList() : new List<(string, string, bool, bool)>(); } + private void CacheMemberReadyStates(IEnumerable<(string steamId, string name, bool isReady, bool isHost)> members) + { + memberReadyCache.Clear(); + if (members == null) return; + foreach (var member in members) + { + if (string.IsNullOrEmpty(member.steamId)) continue; + memberReadyCache[member.steamId] = member.isReady; + } + } + + private bool TryGetMemberReadyState(string steamId, out bool isReady) + { + isReady = false; + if (string.IsNullOrEmpty(steamId)) return false; + + if (memberReadyCache.TryGetValue(steamId, out var cachedReady)) + { + isReady = cachedReady; + return true; + } + + var members = GetSteamMembers(); + CacheMemberReadyStates(members); + foreach (var member in members) + { + if (member.steamId == steamId) + { + isReady = member.isReady; + return true; + } + } + return false; + } + + private bool AreAllPlayersReady() + { + if (!IsInLobby) return false; + if (memberReadyCache.Count == 0) + { + CacheMemberReadyStates(GetSteamMembers()); + } + if (memberReadyCache.Count == 0) return false; + foreach (var kvp in memberReadyCache) + { + if (!kvp.Value) return false; + } + return true; + } + + private void ApplyReadyBorder(Image avatarImage, bool isReady) + { + if (avatarImage == null) return; + + var legacyOutline = avatarImage.GetComponent(); + if (legacyOutline != null) legacyOutline.enabled = false; + + var borderImage = EnsureAvatarBorderImage(avatarImage); + if (borderImage == null) return; + + if (avatarBorderThickness <= 0f) + { + borderImage.enabled = false; + return; + } + + borderImage.enabled = true; + borderImage.color = isReady ? readyBorderColor : notReadyBorderColor; + + var avatarRect = avatarImage.rectTransform; + var borderRect = borderImage.rectTransform; + borderRect.anchorMin = avatarRect.anchorMin; + borderRect.anchorMax = avatarRect.anchorMax; + borderRect.pivot = avatarRect.pivot; + borderRect.anchoredPosition = avatarRect.anchoredPosition; + borderRect.localPosition = avatarRect.localPosition; + borderRect.localRotation = avatarRect.localRotation; + borderRect.localScale = avatarRect.localScale; + var padding = avatarBorderThickness * 2f; + borderRect.sizeDelta = avatarRect.sizeDelta + new Vector2(padding, padding); + } + + private Image EnsureAvatarBorderImage(Image avatarImage) + { + if (avatarImage == null) return null; + var parent = avatarImage.transform.parent; + if (parent == null) return null; + + const string borderName = "Image_AvatarBorder"; + Transform borderTransform = null; + for (int i = 0; i < parent.childCount; i++) + { + var child = parent.GetChild(i); + if (child != null && child.name == borderName) + { + borderTransform = child; + break; + } + } + + if (borderTransform == null) + { + var borderGO = new GameObject(borderName, typeof(RectTransform), typeof(Image)); + borderTransform = borderGO.transform; + borderTransform.SetParent(parent, false); + } + + var borderImage = borderTransform.GetComponent(); + if (borderImage == null) + { + borderImage = borderTransform.gameObject.AddComponent(); + } + + borderImage.raycastTarget = false; + borderImage.preserveAspect = false; + borderImage.type = Image.Type.Simple; + + var layoutElement = borderTransform.GetComponent(); + if (layoutElement == null) + { + layoutElement = borderTransform.gameObject.AddComponent(); + } + layoutElement.ignoreLayout = true; + + var avatarIndex = avatarImage.transform.GetSiblingIndex(); + borderTransform.SetSiblingIndex(avatarIndex); + avatarImage.transform.SetSiblingIndex(borderTransform.GetSiblingIndex() + 1); + + return borderImage; + } + private void RebuildPlayers() { if (contentPlayers == null) return; @@ -694,6 +866,7 @@ namespace MegaKoop.UI foreach (var go in toDestroy) DestroyImmediate(go); var members = GetSteamMembers(); + CacheMemberReadyStates(members); if (textPlayerCount) textPlayerCount.text = $"{members.Count}/{(ddMaxPlayers ? int.Parse(ddMaxPlayers.options[ddMaxPlayers.value].text) : 4)}"; if (members.Count == 0) @@ -735,6 +908,14 @@ namespace MegaKoop.UI var goLE = go.GetComponent(); if (!goLE) goLE = go.AddComponent(); goLE.minHeight = 100; goLE.flexibleWidth = 1f; + var horizontalLayout = go.GetComponent(); + if (horizontalLayout) + { + var paddingValue = Mathf.CeilToInt(avatarBorderThickness); + if (horizontalLayout.padding.left < paddingValue) horizontalLayout.padding.left = paddingValue; + if (horizontalLayout.padding.right < paddingValue) horizontalLayout.padding.right = paddingValue; + } + // Children // Find avatar inside this item (not globally) Image avatarGO = null; @@ -786,7 +967,20 @@ namespace MegaKoop.UI avRT.anchoredPosition = Vector2.zero; avRT.sizeDelta = new Vector2(96, 96); var avLE = avatarGO.GetComponent() ?? avatarGO.gameObject.AddComponent(); - avLE.minWidth = 96; avLE.minHeight = 96; + var avatarSize = Mathf.Max(avRT.sizeDelta.x, avRT.sizeDelta.y); + var paddedSize = avatarSize + avatarBorderThickness * 2f; + avLE.minWidth = paddedSize; + avLE.minHeight = paddedSize; + avLE.preferredWidth = paddedSize; + avLE.preferredHeight = paddedSize; + + if (goLE != null) + { + goLE.minWidth = Mathf.Max(goLE.minWidth, paddedSize); + goLE.preferredWidth = Mathf.Max(goLE.preferredWidth, paddedSize); + } + + ApplyReadyBorder(avatarGO, ready); } // Selection handler