// Assets/_Project/Scripts/Gameplay/BuilderUpgradeManager.cs using System.Collections.Generic; using Unity.Netcode; using UnityEngine; using TD.Core; using TD.Gameplay.BuilderEffects; namespace TD.Gameplay { /// /// Per-player set of granted builder effects. Lives on the Player prefab alongside /// , , and /// . /// /// /// Bitmask, not a list. A builder effect carries no per-grant state (no level, /// no enable/disable toggle) — it's either granted or it isn't — so the granted set is one /// bitmask keyed by rather /// than a NetworkList. "Has effect" and "grant effect" are both O(1) bit ops. /// /// Granted via draft. /// calls when a player picks it. Effects are permanent for /// the match — there is no revoke. /// /// Query, don't snapshot. Consumers (e.g. awarding /// kill gold) query this manager live at the point of use rather than towers caching their /// own copy — every tower a player owns benefits immediately, including ones built before /// the effect was granted. /// /// Generic accessor, not one method per effect. /// is the only way to read an effect's tunable data. /// This keeps the API surface fixed as the effect roster grows — a new effect kind adds a /// call site where it's consumed (e.g. a new line in WaveManager), not a new method /// here. /// public class BuilderUpgradeManager : NetworkBehaviour { // ----- Static registry (mirrors PlayerGoldManager / PlayerTowerDeck) ----- private static readonly Dictionary s_byClientId = new Dictionary(); /// Returns the manager owned by the given client, or null. public static BuilderUpgradeManager GetForClient(ulong clientId) { s_byClientId.TryGetValue(clientId, out var mgr); return mgr; } /// Convenience: the local client's own manager. public static BuilderUpgradeManager Local { get { var nm = NetworkManager.Singleton; if (nm == null || !nm.IsClient) return null; return GetForClient(nm.LocalClientId); } } // ----- Networked state -------------------------------------------- // One bit per BuilderEffectKind. readPerm Everyone (consistent with the other // per-player managers); writePerm Server. private readonly NetworkVariable grantedMask = new NetworkVariable( value: 0, readPerm: NetworkVariableReadPermission.Everyone, writePerm: NetworkVariableWritePermission.Server ); /// Fired on every peer when a new effect is granted. public event System.Action OnUpgradesChanged; // ----- NGO lifecycle ------------------------------------------------ public override void OnNetworkSpawn() { s_byClientId[OwnerClientId] = this; grantedMask.OnValueChanged += HandleMaskChanged; } public override void OnNetworkDespawn() { grantedMask.OnValueChanged -= HandleMaskChanged; if (s_byClientId.TryGetValue(OwnerClientId, out var registered) && registered == this) s_byClientId.Remove(OwnerClientId); } private void HandleMaskChanged(uint previous, uint current) => OnUpgradesChanged?.Invoke(); // ----- Read API ----------------------------------------------------- /// True if this player has been granted the given effect. public bool PlayerHasEffect(BuilderEffectKind kind) => (grantedMask.Value & BitFor(kind)) != 0; /// /// Returns this player's for /// as , or null if the player doesn't have it (or the pool has no /// asset for it). Callers extract whatever field they need — see the "generic accessor" /// note above for why this isn't a per-effect method. /// public T GetEffectDefinition(BuilderEffectKind kind) where T : BuilderEffectDefinition => PlayerHasEffect(kind) ? BuilderEffectPool.Instance?.Get(kind) as T : null; // ----- Server mutation ----------------------------------------------- /// /// Server-only: grants the given effect (a draft pick). No-op if already granted. /// Returns true if it was actually granted. /// public bool ServerGrantEffect(BuilderEffectKind kind) { if (!IsServer) return false; uint bit = BitFor(kind); if ((grantedMask.Value & bit) != 0) return false; grantedMask.Value |= bit; return true; } private static uint BitFor(BuilderEffectKind kind) => 1u << (int)kind; } }