// Assets/_Project/Scripts/Gameplay/PlayerBuffManager.cs using System.Collections.Generic; using Unity.Collections; using Unity.Netcode; using UnityEngine; using TD.Core; namespace TD.Gameplay { /// /// Holds all buffs currently owned by one player. Lives on the Player prefab /// alongside and . /// /// /// Purchase flow. The owning client calls /// with a category index. The server /// validates gold, deducts the cost, picks a random /// from the category's pool, and appends an /// to the replicated list. /// /// Multiplier query. is called by /// on the server each time a tower fires. /// It multiplies all active buffs for the requested stat together. /// /// Enable / disable. Enemies can call /// and by index to temporarily suppress buffs /// without removing them from the list. /// 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; private NetworkList buffs; private void Awake() { 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 ------------------------------------------------- /// /// Returns the combined multiplier for across all /// active buffs owned by this player. Returns 1.0 if no matching buffs /// are active. Safe to call from any peer. /// public float GetMultiplier(BuffStat stat) { float result = 1f; for (int i = 0; i < buffs.Count; i++) { var buff = buffs[i]; if (buff.Stat == stat && buff.IsActive) result *= buff.Multiplier; } return result; } /// /// Returns the number of buff categories available, for building the UI. /// public int CategoryCount => categories?.Length ?? 0; /// Returns the category at the given index, or null. public BuffCategory GetCategory(int index) => (categories != null && index >= 0 && index < categories.Length) ? categories[index] : null; /// Read-only access to the buff list for UI rendering. public NetworkList Buffs => buffs; // ----- Server helpers --------------------------------------------- /// /// Server-only: disables the buff at . /// The buff remains in the list and can be re-enabled. /// public void ServerDisableBuff(int index) { if (!IsServer) return; if (index < 0 || index >= buffs.Count) return; var b = buffs[index]; if (!b.IsActive) return; b.IsActive = false; buffs[index] = b; } /// Server-only: re-enables the buff at . public void ServerEnableBuff(int index) { if (!IsServer) return; if (index < 0 || index >= buffs.Count) return; var b = buffs[index]; if (b.IsActive) return; b.IsActive = true; buffs[index] = b; } // ----- Purchase RPC ----------------------------------------------- /// /// Owning-client entry point. Sends a request to the server to purchase /// a random buff from the category at . /// The server validates gold and adds the buff if the purchase succeeds. /// [Rpc(SendTo.Server, RequireOwnership = true)] public void RequestPurchaseBuffRpc(int categoryIndex) { if (categories == null || categoryIndex < 0 || categoryIndex >= categories.Length) { Debug.LogWarning("[PlayerBuffManager] RequestPurchaseBuff: invalid category index."); return; } var category = categories[categoryIndex]; if (category == null || category.Pool == null || category.Pool.Length == 0) { Debug.LogWarning("[PlayerBuffManager] RequestPurchaseBuff: category has no buffs."); return; } var goldManager = PlayerGoldManager.GetForClient(OwnerClientId); if (goldManager == null) { Debug.LogError("[PlayerBuffManager] RequestPurchaseBuff: no PlayerGoldManager found."); return; } if (goldManager.CurrentGold < category.Cost) { Debug.Log($"[PlayerBuffManager] Client {OwnerClientId} cannot afford " + $"'{category.DisplayName}' (cost {category.Cost}, " + $"gold {goldManager.CurrentGold})."); return; } goldManager.DeductGold(category.Cost); var def = category.Pool[Random.Range(0, category.Pool.Length)]; buffs.Add(new ActiveBuff { Stat = def.Stat, Multiplier = def.Multiplier, IsActive = true, DisplayName = new FixedString64Bytes(def.DisplayName ?? string.Empty), }); Debug.Log($"[PlayerBuffManager] Client {OwnerClientId} purchased buff " + $"'{def.DisplayName}' ({def.Stat} ×{def.Multiplier})."); } } }