// 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;
}
}