purchase buffs menu works

This commit is contained in:
Ian Woods 2026-06-03 00:05:52 -07:00
parent 3737ad517c
commit 79cd331141
6 changed files with 222 additions and 7 deletions

View file

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

View file

@ -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
/// </remarks>
public class PlayerBuffManager : NetworkBehaviour
{
// ----- Static registry (mirrors PlayerGoldManager pattern) -----------
private static readonly Dictionary<ulong, PlayerBuffManager> s_byClientId
= new Dictionary<ulong, PlayerBuffManager>();
/// <summary>Returns the PlayerBuffManager owned by the given client, or null.</summary>
public static PlayerBuffManager GetForClient(ulong clientId)
{
s_byClientId.TryGetValue(clientId, out var mgr);
return mgr;
}
/// <summary>Convenience: the local client's own buff manager.</summary>
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<ActiveBuff>();
}
// ----- 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 -------------------------------------------------
/// <summary>

View file

@ -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);
}
/// <summary>
/// Toggles the buff menu overlay. Called by <see cref="TD.Gameplay.BuilderInputController"/>
/// when the player presses B.
/// </summary>
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