diff --git a/Assets/_Project/Prefabs/Player/Player.prefab b/Assets/_Project/Prefabs/Player/Player.prefab index 4649bc7..4d7c382 100644 --- a/Assets/_Project/Prefabs/Player/Player.prefab +++ b/Assets/_Project/Prefabs/Player/Player.prefab @@ -48,7 +48,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} m_Name: m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject - GlobalObjectIdHash: 121878297 + GlobalObjectIdHash: 1552073510 InScenePlacedSourceGlobalObjectIdHash: 0 DeferredDespawnTick: 0 Ownership: 1 @@ -116,5 +116,4 @@ MonoBehaviour: m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.PlayerBuffManager ShowTopMostFoldoutHeaderGroup: 1 categories: - - {fileID: 0} - - {fileID: 0} + - {fileID: 11400000, guid: d8ed3b9535538f7fc82be878cc307d26, type: 2} diff --git a/Assets/_Project/Scripts/Gameplay/BuilderInputController.cs b/Assets/_Project/Scripts/Gameplay/BuilderInputController.cs index 5de1f4a..ce1e258 100644 --- a/Assets/_Project/Scripts/Gameplay/BuilderInputController.cs +++ b/Assets/_Project/Scripts/Gameplay/BuilderInputController.cs @@ -149,6 +149,14 @@ namespace TD.Gameplay // Right-click. Suppressed entirely during a modal mode (placement/paint // controllers handle right-click as cancel there) and when over HUD. if (isModal) return; + + // B: toggle buff menu. + if (keyboard != null && keyboard.bKey.wasPressedThisFrame + && !HUDController.IsTextInputActive) + { + HUDController.Instance?.ToggleBuffMenu(); + } + if (pointerOverHud) return; if (!mouse.rightButton.wasPressedThisFrame) return; diff --git a/Assets/_Project/Scripts/Gameplay/PlayerBuffManager.cs b/Assets/_Project/Scripts/Gameplay/PlayerBuffManager.cs index 8112423..506791e 100644 --- a/Assets/_Project/Scripts/Gameplay/PlayerBuffManager.cs +++ b/Assets/_Project/Scripts/Gameplay/PlayerBuffManager.cs @@ -1,4 +1,5 @@ // Assets/_Project/Scripts/Gameplay/PlayerBuffManager.cs +using System.Collections.Generic; using Unity.Collections; using Unity.Netcode; using UnityEngine; @@ -27,6 +28,31 @@ namespace TD.Gameplay /// public class PlayerBuffManager : NetworkBehaviour { + // ----- Static registry (mirrors PlayerGoldManager pattern) ----------- + + private static readonly Dictionary s_byClientId + = new Dictionary(); + + /// Returns the PlayerBuffManager owned by the given client, or null. + public static PlayerBuffManager GetForClient(ulong clientId) + { + s_byClientId.TryGetValue(clientId, out var mgr); + return mgr; + } + + /// Convenience: the local client's own buff manager. + public static PlayerBuffManager Local + { + get + { + var nm = NetworkManager.Singleton; + if (nm == null) return null; + return GetForClient(nm.LocalClientId); + } + } + + // ----- Inspector ------------------------------------------------------ + [Tooltip("Purchasable buff categories shown in the buff menu. Index in this " + "array is the categoryIndex passed to RequestPurchaseBuffRpc.")] [SerializeField] private BuffCategory[] categories; @@ -38,6 +64,19 @@ namespace TD.Gameplay buffs = new NetworkList(); } + // ----- NGO lifecycle ---------------------------------------------- + + public override void OnNetworkSpawn() + { + s_byClientId[OwnerClientId] = this; + } + + public override void OnNetworkDespawn() + { + if (s_byClientId.TryGetValue(OwnerClientId, out var registered) && registered == this) + s_byClientId.Remove(OwnerClientId); + } + // ----- Public API ------------------------------------------------- /// diff --git a/Assets/_Project/Scripts/UI/HUDController.cs b/Assets/_Project/Scripts/UI/HUDController.cs index 6c3963f..e3e169f 100644 --- a/Assets/_Project/Scripts/UI/HUDController.cs +++ b/Assets/_Project/Scripts/UI/HUDController.cs @@ -21,6 +21,8 @@ namespace TD.UI [RequireComponent(typeof(UIDocument))] public class HUDController : MonoBehaviour { + public static HUDController Instance { get; private set; } + // ----- Inspector -------------------------------------------------- [Header("Scene References")] @@ -93,6 +95,10 @@ namespace TD.UI // Match-end overlay — built once on Start and toggled on Phase changes. private VisualElement matchEndOverlay; + + // Buff menu overlay — toggled by the B key via ToggleBuffMenu(). + private VisualElement buffMenuOverlay; + private VisualElement buffMenuContent; private Label matchEndTitle; // Chat panel (bottom-left, above portrait) — programmatic. The container @@ -309,6 +315,9 @@ namespace TD.UI // MatchState.OnPhaseChanged fires Victory or Defeat. BuildMatchEndOverlay(root); + // Build the buff menu overlay. Hidden until the player presses B. + BuildBuffMenuOverlay(root); + // Chat feed + input. Anchored bottom-left, just above the portrait/bottom-ui bar. // Player typing toggled with Enter; system messages (e.g. life lost) post via // ChatService.PostLocalSystem on every peer. @@ -323,6 +332,11 @@ namespace TD.UI uiInitialized = true; } + private void Awake() + { + Instance = this; + } + private void OnEnable() { TowerPlacementController.OnRejectionMessageReady += ShowRejectionMessage; @@ -437,6 +451,8 @@ namespace TD.UI private void OnDestroy() { + if (Instance == this) Instance = null; + minimapView?.Dispose(); minimapView = null; @@ -1208,6 +1224,149 @@ namespace TD.UI // ----- Chat feed + input ----------------------------------------- + // ----- Buff menu overlay ------------------------------------------ + + private void BuildBuffMenuOverlay(VisualElement root) + { + buffMenuOverlay = new VisualElement(); + buffMenuOverlay.style.position = Position.Absolute; + buffMenuOverlay.style.left = 0; + buffMenuOverlay.style.right = 0; + buffMenuOverlay.style.top = 0; + buffMenuOverlay.style.bottom = 0; + buffMenuOverlay.style.alignItems = Align.Center; + buffMenuOverlay.style.justifyContent = Justify.Center; + buffMenuOverlay.style.backgroundColor = new Color(0f, 0f, 0f, 0.5f); + buffMenuOverlay.style.display = DisplayStyle.None; + buffMenuOverlay.pickingMode = PickingMode.Position; + + var panel = new VisualElement(); + panel.style.minWidth = 340; + panel.style.paddingTop = 20; + panel.style.paddingBottom = 20; + panel.style.paddingLeft = 28; + panel.style.paddingRight = 28; + panel.style.backgroundColor = new Color(0.08f, 0.08f, 0.10f, 0.95f); + panel.style.borderTopWidth = panel.style.borderBottomWidth = + panel.style.borderLeftWidth = panel.style.borderRightWidth = 2; + var border = new Color(0.4f, 0.4f, 0.45f); + panel.style.borderTopColor = panel.style.borderBottomColor = + panel.style.borderLeftColor = panel.style.borderRightColor = border; + + var title = new Label("Buffs"); + title.style.fontSize = 24; + title.style.color = Color.white; + title.style.unityFontStyleAndWeight = FontStyle.Bold; + title.style.marginBottom = 12; + panel.Add(title); + + // Scrollable content area: current buffs + purchase buttons. + // Rebuilt each time the menu is shown via RefreshBuffMenuContent(). + buffMenuContent = new VisualElement(); + panel.Add(buffMenuContent); + + var closeBtn = new Button(() => SetBuffMenuVisible(false)) { text = "Close [B]" }; + closeBtn.style.marginTop = 16; + closeBtn.style.height = 32; + panel.Add(closeBtn); + + buffMenuOverlay.Add(panel); + root.Add(buffMenuOverlay); + } + + /// + /// Toggles the buff menu overlay. Called by + /// when the player presses B. + /// + public void ToggleBuffMenu() + { + bool nowVisible = buffMenuOverlay?.style.display == DisplayStyle.None; + SetBuffMenuVisible(nowVisible); + } + + private void SetBuffMenuVisible(bool visible) + { + if (buffMenuOverlay == null) return; + buffMenuOverlay.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None; + if (visible) RefreshBuffMenuContent(); + } + + private void RefreshBuffMenuContent() + { + if (buffMenuContent == null) return; + buffMenuContent.Clear(); + + var buffManager = TD.Gameplay.PlayerBuffManager.Local; + + // Current buffs section. + var buffsHeader = new Label("Active Buffs"); + buffsHeader.style.color = new Color(0.7f, 0.9f, 0.7f); + buffsHeader.style.fontSize = 14; + buffsHeader.style.marginBottom = 4; + buffMenuContent.Add(buffsHeader); + + if (buffManager == null || buffManager.Buffs.Count == 0) + { + var none = new Label("None"); + none.style.color = new Color(0.55f, 0.55f, 0.55f); + none.style.marginBottom = 8; + buffMenuContent.Add(none); + } + else + { + for (int i = 0; i < buffManager.Buffs.Count; i++) + { + var buff = buffManager.Buffs[i]; + string statName = buff.Stat == TD.Core.BuffStat.Damage ? "Damage" : "Attack Speed"; + string activeTag = buff.IsActive ? "" : " [DISABLED]"; + var row = new Label($"• {buff.DisplayName} ({statName} ×{buff.Multiplier:F2}){activeTag}"); + row.style.color = buff.IsActive ? Color.white : new Color(0.5f, 0.5f, 0.5f); + row.style.fontSize = 13; + buffMenuContent.Add(row); + } + buffMenuContent.style.marginBottom = 12; + } + + // Purchase buttons section. + var buyHeader = new Label("Purchase"); + buyHeader.style.color = new Color(0.9f, 0.8f, 0.5f); + buyHeader.style.fontSize = 14; + buyHeader.style.marginTop = 8; + buyHeader.style.marginBottom = 4; + buffMenuContent.Add(buyHeader); + + int categoryCount = buffManager?.CategoryCount ?? 0; + if (categoryCount == 0) + { + var none = new Label("No categories available."); + none.style.color = new Color(0.55f, 0.55f, 0.55f); + buffMenuContent.Add(none); + return; + } + + int localGold = TD.Gameplay.PlayerGoldManager.Local?.CurrentGold ?? 0; + for (int i = 0; i < categoryCount; i++) + { + var category = buffManager.GetCategory(i); + if (category == null) continue; + + int idx = i; // capture for lambda + var btn = new Button(() => + { + TD.Gameplay.PlayerBuffManager.Local?.RequestPurchaseBuffRpc(idx); + RefreshBuffMenuContent(); + }) + { + text = $"{category.DisplayName} — {category.Cost}g" + }; + btn.style.height = 34; + btn.style.fontSize = 13; + btn.style.marginBottom = 4; + btn.SetEnabled(localGold >= category.Cost); + buffMenuContent.Add(btn); + } + } + // Bottom-left chat panel. Anchored 12px from the left edge, with the // bottom edge sitting above the 220px bottom-ui. Layout uses a flex // column: scrollable feed on top, input below. The feed clips at diff --git a/Packages/manifest.json b/Packages/manifest.json index 5b70fbf..541a381 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -12,11 +12,12 @@ "com.unity.netcode.gameobjects": "2.11.0", "com.unity.probuilder": "6.0.9", "com.unity.render-pipelines.universal": "17.4.0", - "com.unity.sdk.linux-x86_64": "1.0.2", + "com.unity.sdk.linux-x86_64": "1.1.0", "com.unity.services.multiplayer": "2.1.3", "com.unity.terrain-tools": "5.3.2", "com.unity.test-framework": "1.6.0", "com.unity.timeline": "1.8.12", + "com.unity.toolchain.linux-x86_64-linux": "1.1.0", "com.unity.toolchain.win-x86_64-linux": "1.0.2", "com.unity.ugui": "2.0.0", "com.unity.visualscripting": "1.9.11", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index 499be20..ca389ce 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -221,11 +221,11 @@ "url": "https://packages.unity.com" }, "com.unity.sdk.linux-x86_64": { - "version": "1.0.2", + "version": "1.1.0", "depth": 0, "source": "registry", "dependencies": { - "com.unity.sysroot.base": "1.0.2" + "com.unity.sysroot.base": "1.1.0" }, "url": "https://packages.unity.com" }, @@ -355,7 +355,7 @@ "url": "https://packages.unity.com" }, "com.unity.sysroot.base": { - "version": "1.0.2", + "version": "1.1.0", "depth": 1, "source": "registry", "dependencies": {}, @@ -403,6 +403,15 @@ }, "url": "https://packages.unity.com" }, + "com.unity.toolchain.linux-x86_64-linux": { + "version": "1.1.0", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.sysroot.base": "1.1.0" + }, + "url": "https://packages.unity.com" + }, "com.unity.toolchain.win-x86_64-linux": { "version": "1.0.2", "depth": 0,