Decals, ghost textures, placement functionality, builder stub ins, a new camera system, and more.
197 lines
No EOL
7.8 KiB
C#
197 lines
No EOL
7.8 KiB
C#
using System.Collections.Generic;
|
|
using Unity.Netcode;
|
|
using UnityEngine;
|
|
|
|
namespace TD.Gameplay
|
|
{
|
|
/// <summary>
|
|
/// Per-player gold pool. One instance is spawned per connected client (via
|
|
/// <see cref="NetworkManager.PlayerPrefab"/>) and owned by that client.
|
|
///
|
|
/// Replaces the earlier singleton-style GoldManager. Same server-authoritative
|
|
/// pattern (NetworkVariable + server-validated Rpc), but every player has their
|
|
/// own pool instead of sharing one.
|
|
///
|
|
/// Three-beat pattern (unchanged from the original template):
|
|
/// 1. State lives in NetworkVariables (server-only writes).
|
|
/// 2. The owning client REQUESTS spends via a server-targeted Rpc.
|
|
/// 3. The server VALIDATES before applying.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Lookup ergonomics: each instance registers itself in a static dictionary
|
|
/// keyed by OwnerClientId during OnNetworkSpawn and removes itself during
|
|
/// OnNetworkDespawn. Server code that needs to award gold to a specific
|
|
/// player can call <see cref="GetForClient(ulong)"/>.
|
|
///
|
|
/// This component is expected to live on the Player Prefab assigned to
|
|
/// NetworkManager. NGO handles spawn-on-connect and ownership assignment
|
|
/// automatically.
|
|
/// </remarks>
|
|
public class PlayerGoldManager : NetworkBehaviour
|
|
{
|
|
// --- Static registry ---------------------------------------------
|
|
|
|
// Keyed by OwnerClientId. Populated on every peer when an instance
|
|
// spawns; the same client can be looked up on the server (for awards)
|
|
// and on clients (for UI). Kept private — access goes through GetForClient.
|
|
private static readonly Dictionary<ulong, PlayerGoldManager> s_byClientId
|
|
= new Dictionary<ulong, PlayerGoldManager>();
|
|
|
|
/// <summary>
|
|
/// Returns the PlayerGoldManager owned by the given client, or null if
|
|
/// none is currently spawned. Safe to call on server or client.
|
|
/// </summary>
|
|
public static PlayerGoldManager GetForClient(ulong clientId)
|
|
{
|
|
s_byClientId.TryGetValue(clientId, out var manager);
|
|
return manager;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convenience: the local client's own gold manager. Returns null on a
|
|
/// dedicated server or before the local player has spawned.
|
|
/// </summary>
|
|
public static PlayerGoldManager Local
|
|
{
|
|
get
|
|
{
|
|
var nm = NetworkManager.Singleton;
|
|
if (nm == null || !nm.IsClient) return null;
|
|
return GetForClient(nm.LocalClientId);
|
|
}
|
|
}
|
|
|
|
// --- Tunables ----------------------------------------------------
|
|
|
|
[Tooltip("How much gold this player starts with when the match begins.")]
|
|
[SerializeField] private int startingGold = 100;
|
|
|
|
// --- Networked state ---------------------------------------------
|
|
|
|
// readPerm = Everyone so any client can read any other player's gold
|
|
// (needed for scoreboard / opponent UI). writePerm = Server keeps
|
|
// authority where it belongs.
|
|
private readonly NetworkVariable<int> currentGold = new NetworkVariable<int>(
|
|
value: 0,
|
|
readPerm: NetworkVariableReadPermission.Everyone,
|
|
writePerm: NetworkVariableWritePermission.Server
|
|
);
|
|
|
|
public int CurrentGold => currentGold.Value;
|
|
|
|
// --- Lifecycle ---------------------------------------------------
|
|
|
|
public override void OnNetworkSpawn()
|
|
{
|
|
Debug.Log($"[PlayerGoldManager] OnNetworkSpawn. OwnerClientId={OwnerClientId}, " +
|
|
$"IsOwner={IsOwner}, IsServer={IsServer}");
|
|
|
|
currentGold.OnValueChanged += HandleGoldChanged;
|
|
s_byClientId[OwnerClientId] = this;
|
|
|
|
if (IsServer)
|
|
{
|
|
currentGold.Value = startingGold;
|
|
}
|
|
}
|
|
|
|
public override void OnNetworkDespawn()
|
|
{
|
|
currentGold.OnValueChanged -= HandleGoldChanged;
|
|
|
|
// Only remove if the entry still points to this instance — guards
|
|
// against the (unlikely) case where a new instance for the same
|
|
// client has already overwritten the slot.
|
|
if (s_byClientId.TryGetValue(OwnerClientId, out var registered) && registered == this)
|
|
{
|
|
s_byClientId.Remove(OwnerClientId);
|
|
}
|
|
}
|
|
|
|
private void HandleGoldChanged(int previous, int current)
|
|
{
|
|
Debug.Log($"[PlayerGoldManager] Client {OwnerClientId} gold: {previous} -> {current}");
|
|
}
|
|
|
|
// --- Public API --------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Owning-client entry point for spending gold. Sends a server Rpc;
|
|
/// the server validates and applies. Calling this on a non-owning
|
|
/// client will be rejected by the Rpc's Owner permission.
|
|
/// </summary>
|
|
public void RequestSpendGold(int amount)
|
|
{
|
|
SpendGoldRpc(amount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Server-side entry point for awarding gold (wave clear, enemy kill).
|
|
/// Direct call — not Rpc-wrapped — because awards always originate
|
|
/// from server-authoritative game events.
|
|
/// </summary>
|
|
public void AwardGold(int amount)
|
|
{
|
|
if (!IsServer)
|
|
{
|
|
Debug.LogError("[PlayerGoldManager] AwardGold called on a client. " +
|
|
"Only server code should call this directly.");
|
|
return;
|
|
}
|
|
|
|
if (amount <= 0) return;
|
|
currentGold.Value += amount;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Server-side entry point for deducting gold (tower placement, other costs).
|
|
/// Direct call — not Rpc-wrapped — because deductions always originate from
|
|
/// server-authoritative validation (e.g., <c>TowerPlacementManager</c> after
|
|
/// all checks have passed). Clamps to zero; gold cannot go negative.
|
|
/// </summary>
|
|
public void DeductGold(int amount)
|
|
{
|
|
if (!IsServer)
|
|
{
|
|
Debug.LogError("[PlayerGoldManager] DeductGold called on a client. " +
|
|
"Only server code should call this directly.");
|
|
return;
|
|
}
|
|
|
|
if (amount <= 0) return;
|
|
currentGold.Value = Mathf.Max(0, currentGold.Value - amount);
|
|
}
|
|
|
|
// --- Server-side Rpc ---------------------------------------------
|
|
|
|
// InvokePermission = Owner: only the client that owns this NetworkObject
|
|
// can invoke the Rpc. NGO will reject calls from anyone else, so a
|
|
// malicious client can't spend another player's gold.
|
|
//
|
|
// This replaces the old "RequireOwnership = false" + the implicit
|
|
// shared-pool semantics. The deprecation warning is gone.
|
|
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)]
|
|
private void SpendGoldRpc(int amount, RpcParams rpcParams = default)
|
|
{
|
|
// Validation 1: positive amount.
|
|
if (amount <= 0)
|
|
{
|
|
Debug.LogWarning($"[PlayerGoldManager] Rejected spend of {amount} " +
|
|
$"from client {rpcParams.Receive.SenderClientId}: " +
|
|
$"amount must be positive.");
|
|
return;
|
|
}
|
|
|
|
// Validation 2: sufficient funds.
|
|
if (currentGold.Value < amount)
|
|
{
|
|
Debug.LogWarning($"[PlayerGoldManager] Rejected spend of {amount} " +
|
|
$"from client {rpcParams.Receive.SenderClientId}: " +
|
|
$"insufficient funds (have {currentGold.Value}).");
|
|
return;
|
|
}
|
|
|
|
currentGold.Value -= amount;
|
|
}
|
|
}
|
|
} |