using Unity.Netcode; using UnityEngine; namespace TD.Gameplay { /// /// GoldManager — canonical server-authoritative template for this project. /// /// Every gameplay system (towers, enemies, waves, damage) should follow /// the same three-beat pattern demonstrated here: /// 1. State lives in NetworkVariables, which only the server can write. /// 2. Clients REQUEST changes via [Rpc(SendTo.Server, ...)] methods. /// They never change state directly. /// 3. The server VALIDATES the request before applying it. /// Never trust the client. /// /// Cosmetic-only reactions (sounds, VFX, UI popups) can use /// [Rpc(SendTo.ClientsAndHost)] or [Rpc(SendTo.NotServer)] to broadcast. /// public class GoldManager : NetworkBehaviour { // --- Tunables (editable in Inspector) ----------------------------- [Tooltip("How much gold every player starts with when the game begins.")] [SerializeField] private int startingGold = 100; // --- Networked state ---------------------------------------------- // A NetworkVariable automatically syncs from server to clients. // readPerm = Everyone: all clients can read the current value. // writePerm = Server: only the server can change it. private readonly NetworkVariable currentGold = new NetworkVariable( value: 0, readPerm: NetworkVariableReadPermission.Everyone, writePerm: NetworkVariableWritePermission.Server ); // Public read-only accessor for other scripts (UI, tower placement). public int CurrentGold => currentGold.Value; // --- Lifecycle ---------------------------------------------------- /// /// OnNetworkSpawn runs on every peer (server + all clients) when this /// NetworkBehaviour becomes active on the network. Replaces Start() /// for networked setup. /// public override void OnNetworkSpawn() { Debug.Log($"[GoldManager] OnNetworkSpawn ran. IsServer={IsServer}, IsClient={IsClient}, IsHost={IsHost}"); currentGold.OnValueChanged += HandleGoldChanged; Debug.Log($"[GoldManager] Subscribed to OnValueChanged. Current value before init: {currentGold.Value}"); if (IsServer) { currentGold.Value = startingGold; Debug.Log($"[GoldManager] Server initialized gold. Current value after set: {currentGold.Value}"); } } public override void OnNetworkDespawn() { // Always unsubscribe to avoid callback leaks. currentGold.OnValueChanged -= HandleGoldChanged; } private void HandleGoldChanged(int previous, int current) { // Fires on every peer whenever the value syncs. Use Log here so // you can see syncing in the Console during development. Debug.Log($"[GoldManager] Gold changed: {previous} -> {current}"); } // --- Public API (called by client-side code) ---------------------- /// /// Client-side entry point for spending gold. Called by gameplay code /// like TowerPlacement when the local player clicks "build tower." /// /// The actual spending happens on the server via the Rpc. /// public void RequestSpendGold(int amount) { SpendGoldRpc(amount); } /// /// Server-side entry point for awarding gold (wave clear, enemy kill). /// Not Rpc-wrapped — this is called directly by server game logic in /// response to server-authoritative events. /// public void AwardGold(int amount) { if (!IsServer) { Debug.LogError("[GoldManager] AwardGold called on a client! " + "Only server code should call this directly."); return; } if (amount <= 0) return; currentGold.Value += amount; } // --- Server-side RPC ---------------------------------------------- // [Rpc(SendTo.Server, ...)] means: a client calls this locally, but // NGO routes the call and executes the method on the server. // // RequireOwnership = false lets any client call it (correct for a // shared GoldManager). For per-player NetworkObjects you'd usually // leave the default ownership requirement in place. // // Naming convention: methods with [Rpc] attributes must end with "Rpc". // The source generator relies on this suffix. [Rpc(SendTo.Server, RequireOwnership = false)] private void SpendGoldRpc(int amount, RpcParams rpcParams = default) { // This method body runs on the server only. // Validate everything — do not trust the client. // Validation 1: reject non-positive amounts. A negative amount // would let a malicious client GAIN gold if we just subtracted. if (amount <= 0) { Debug.LogWarning($"[GoldManager] Rejected spend of {amount} " + $"from client {rpcParams.Receive.SenderClientId}: " + $"amount must be positive."); return; } // Validation 2: can't spend more than current balance. if (currentGold.Value < amount) { Debug.LogWarning($"[GoldManager] Rejected spend of {amount} " + $"from client {rpcParams.Receive.SenderClientId}: " + $"insufficient funds (have {currentGold.Value})."); return; } // Server applies the change. NetworkVariable syncs to clients // automatically at the next network tick. currentGold.Value -= amount; } } }