// Assets/_Project/Scripts/Gameplay/Draft/PlayerDraft.cs using System.Collections.Generic; using Unity.Netcode; using UnityEngine; namespace TD.Gameplay.Draft { /// /// Per-player draft state — the set of s currently offered to /// one player. Lives on the Player prefab alongside , /// , etc. /// /// /// Offered set. holds the DraftOptionIds /// the player is choosing between (typically 3). Empty means no active draft. The /// owning client renders cards from these ids; the server reads them to validate a pick. /// /// Authority. The server offers (), resolves /// ( / ), and applies the /// chosen option. The owning client only sends intent via /// and . /// /// Generation lives in ; this component is just /// the replicated state + the pick/buy entry points. /// public class PlayerDraft : NetworkBehaviour { // ----- Static registry (mirrors PlayerGoldManager / PlayerTowerDeck) ----- private static readonly Dictionary s_byClientId = new Dictionary(); public static PlayerDraft GetForClient(ulong clientId) { s_byClientId.TryGetValue(clientId, out var draft); return draft; } public static PlayerDraft Local { get { var nm = NetworkManager.Singleton; if (nm == null || !nm.IsClient) return null; return GetForClient(nm.LocalClientId); } } // ----- Networked state -------------------------------------------- // The DraftOptionIds currently offered to this player. Empty = no active draft. private NetworkList currentOptionIds; /// Fired on every peer when the offered set changes (offered, picked, /// cleared). The HUD subscribes to show/hide/rebuild the draft overlay. public event System.Action OnDraftChanged; private void Awake() { currentOptionIds = new NetworkList(); } public override void OnNetworkSpawn() { s_byClientId[OwnerClientId] = this; currentOptionIds.OnListChanged += HandleListChanged; } public override void OnNetworkDespawn() { currentOptionIds.OnListChanged -= HandleListChanged; if (s_byClientId.TryGetValue(OwnerClientId, out var registered) && registered == this) s_byClientId.Remove(OwnerClientId); } private void HandleListChanged(NetworkListEvent _) => OnDraftChanged?.Invoke(); // ----- Read API --------------------------------------------------- /// True if the player currently has options to choose from. public bool HasActiveDraft => currentOptionIds.Count > 0; public int OptionCount => currentOptionIds.Count; public int GetOptionId(int index) => currentOptionIds[index]; private bool IsOffered(int optionId) { for (int i = 0; i < currentOptionIds.Count; i++) if (currentOptionIds[i] == optionId) return true; return false; } // ----- Server: offer / resolve ------------------------------------ /// Server-only: replace the offered set with . public void ServerOffer(IReadOnlyList ids) { if (!IsServer) return; currentOptionIds.Clear(); if (ids == null) return; for (int i = 0; i < ids.Count; i++) currentOptionIds.Add(ids[i]); } /// /// Server-only: apply one of the currently-offered options by id, then clear the /// draft. No-op (returns false) if the id wasn't offered or the option fails to apply. /// private bool ServerResolve(int optionId) { if (!IsServer) return false; if (!IsOffered(optionId)) return false; var option = DraftPool.Instance != null ? DraftPool.Instance.Get(optionId) : null; bool applied = option != null && option.ServerApply(OwnerClientId); if (!applied) Debug.LogWarning($"[PlayerDraft] Option {optionId} could not be applied for " + $"client {OwnerClientId}."); currentOptionIds.Clear(); return applied; } /// /// Server-only: if the player still has an unpicked draft, auto-resolve it by /// applying the first offered option. Called at prep end so a free reward is never /// wasted by an idle player. /// public void ServerAutoResolve() { if (!IsServer) return; if (currentOptionIds.Count == 0) return; ServerResolve(currentOptionIds[0]); } // ----- Owner → server RPCs ---------------------------------------- /// Owning client: pick one of the offered options by id. [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)] public void RequestPickRpc(int optionId) { ServerResolve(optionId); } /// /// Owning client: buy a fresh roll of options for gold. Rejected if the player can't /// afford it or already has an unresolved draft (resolve the current one first so a /// free pick is never silently overwritten). /// [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)] public void RequestBuyRerollRpc() { if (HasActiveDraft) return; // resolve the pending draft before buying another var service = DraftService.Instance; if (service == null) return; var gold = PlayerGoldManager.GetForClient(OwnerClientId); int cost = service.RerollCost; if (gold == null || gold.CurrentGold < cost) return; gold.DeductGold(cost); service.ServerOfferTo(OwnerClientId); } } }