draft options working; spells won't cast though
This commit is contained in:
parent
2429efbf6a
commit
7cf15dcaa6
37 changed files with 1340 additions and 0 deletions
177
Assets/_Project/Scripts/Gameplay/PlayerSpellLoadout.cs
Normal file
177
Assets/_Project/Scripts/Gameplay/PlayerSpellLoadout.cs
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
// Assets/_Project/Scripts/Gameplay/PlayerSpellLoadout.cs
|
||||
using System.Collections.Generic;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using TD.Core;
|
||||
using TD.Gameplay.BuilderSpells;
|
||||
|
||||
namespace TD.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Per-player set of granted builder spells and their cooldowns. Lives on the Player
|
||||
/// prefab alongside <see cref="BuilderUpgradeManager"/> and <see cref="PlayerDraft"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>Slot == grant order == hotkey.</b> <see cref="spells"/> is append-only; a
|
||||
/// slot's index in the list is both its identity and the index into
|
||||
/// <see cref="SpellHotkeys.Layout"/>. There is no player-configurable rebinding.</para>
|
||||
///
|
||||
/// <para><b>Cast flow.</b> The owning client calls <see cref="RequestCastSpellRpc"/> with a
|
||||
/// slot index and an aimed world point. The server re-validates everything (slot exists,
|
||||
/// off cooldown, <see cref="MatchPhase.Playing"/>) since client-side checks in
|
||||
/// <see cref="BuilderSpellCastController"/> are UX-only, resolves the
|
||||
/// <see cref="BuilderSpellDefinition"/> via <see cref="BuilderSpellPool"/>, and calls
|
||||
/// <see cref="BuilderSpellDefinition.ServerCast"/>. A successful cast starts the cooldown
|
||||
/// and fires <see cref="PlayVfxClientRpc"/> so every peer plays the same visual.</para>
|
||||
/// </remarks>
|
||||
public class PlayerSpellLoadout : NetworkBehaviour
|
||||
{
|
||||
/// <summary>Hard cap on granted spells, matching the number of hotkey slots available.</summary>
|
||||
public const int MaxSpellSlots = 4;
|
||||
|
||||
// ----- Static registry (mirrors BuilderUpgradeManager) -----
|
||||
|
||||
private static readonly Dictionary<ulong, PlayerSpellLoadout> s_byClientId
|
||||
= new Dictionary<ulong, PlayerSpellLoadout>();
|
||||
|
||||
/// <summary>Returns the loadout owned by the given client, or null.</summary>
|
||||
public static PlayerSpellLoadout GetForClient(ulong clientId)
|
||||
{
|
||||
s_byClientId.TryGetValue(clientId, out var loadout);
|
||||
return loadout;
|
||||
}
|
||||
|
||||
/// <summary>Convenience: the local client's own loadout.</summary>
|
||||
public static PlayerSpellLoadout Local
|
||||
{
|
||||
get
|
||||
{
|
||||
var nm = NetworkManager.Singleton;
|
||||
if (nm == null || !nm.IsClient) return null;
|
||||
return GetForClient(nm.LocalClientId);
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Networked state --------------------------------------------
|
||||
|
||||
private readonly NetworkList<SpellSlot> spells = new NetworkList<SpellSlot>();
|
||||
|
||||
/// <summary>Fired on every peer when a spell is granted or a cooldown changes.</summary>
|
||||
public event System.Action OnLoadoutChanged;
|
||||
|
||||
// ----- NGO lifecycle ------------------------------------------------
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
s_byClientId[OwnerClientId] = this;
|
||||
spells.OnListChanged += HandleSpellsChanged;
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
spells.OnListChanged -= HandleSpellsChanged;
|
||||
|
||||
if (s_byClientId.TryGetValue(OwnerClientId, out var registered) && registered == this)
|
||||
s_byClientId.Remove(OwnerClientId);
|
||||
}
|
||||
|
||||
private void HandleSpellsChanged(NetworkListEvent<SpellSlot> change) => OnLoadoutChanged?.Invoke();
|
||||
|
||||
// ----- Read API -----------------------------------------------------
|
||||
|
||||
/// <summary>Number of spells currently granted.</summary>
|
||||
public int SlotCount => spells.Count;
|
||||
|
||||
/// <summary>True if this player has already been granted the given spell.</summary>
|
||||
public bool PlayerHasSpell(BuilderSpellKind kind)
|
||||
{
|
||||
for (int i = 0; i < spells.Count; i++)
|
||||
{
|
||||
if (spells[i].Kind == kind) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>The spell kind occupying <paramref name="slot"/>, or null if out of range.</summary>
|
||||
public BuilderSpellKind? GetKind(int slot)
|
||||
=> (slot >= 0 && slot < spells.Count) ? spells[slot].Kind : (BuilderSpellKind?)null;
|
||||
|
||||
/// <summary>True if <paramref name="slot"/> is out of range, empty, or still cooling down.</summary>
|
||||
public bool IsSlotOnCooldown(int slot)
|
||||
{
|
||||
if (slot < 0 || slot >= spells.Count) return true;
|
||||
|
||||
var nm = NetworkManager.Singleton;
|
||||
if (nm == null) return false;
|
||||
|
||||
return spells[slot].CooldownEndServerTime > nm.ServerTime.Time;
|
||||
}
|
||||
|
||||
/// <summary>Seconds remaining on <paramref name="slot"/>'s cooldown, or 0 if ready/invalid.</summary>
|
||||
public float GetCooldownRemaining(int slot)
|
||||
{
|
||||
if (slot < 0 || slot >= spells.Count) return 0f;
|
||||
|
||||
var nm = NetworkManager.Singleton;
|
||||
if (nm == null) return 0f;
|
||||
|
||||
double remaining = spells[slot].CooldownEndServerTime - nm.ServerTime.Time;
|
||||
return remaining > 0d ? (float)remaining : 0f;
|
||||
}
|
||||
|
||||
// ----- Server mutation -----------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Server-only: grants the given spell (a draft pick) into the next free slot.
|
||||
/// No-op if already granted or slots are full. Returns true if actually granted.
|
||||
/// </summary>
|
||||
public bool ServerGrantSpell(BuilderSpellKind kind)
|
||||
{
|
||||
if (!IsServer) return false;
|
||||
if (spells.Count >= MaxSpellSlots) return false;
|
||||
if (PlayerHasSpell(kind)) return false;
|
||||
|
||||
spells.Add(SpellSlot.CreateReady(kind));
|
||||
return true;
|
||||
}
|
||||
|
||||
// ----- Cast RPC -----------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Owning-client entry point. Requests the server resolve a cast of the spell in
|
||||
/// <paramref name="slot"/> at <paramref name="targetPoint"/>. All validation happens
|
||||
/// here on the server; the client only uses this to trigger an attempt.
|
||||
/// </summary>
|
||||
[Rpc(SendTo.Server, RequireOwnership = true)]
|
||||
public void RequestCastSpellRpc(int slot, Vector3 targetPoint)
|
||||
{
|
||||
if (MatchState.Instance == null || MatchState.Instance.Phase != MatchPhase.Playing)
|
||||
return;
|
||||
|
||||
if (slot < 0 || slot >= spells.Count) return;
|
||||
|
||||
var slotValue = spells[slot];
|
||||
if (slotValue.CooldownEndServerTime > NetworkManager.ServerTime.Time) return;
|
||||
|
||||
var definition = BuilderSpellPool.Instance?.Get(slotValue.Kind);
|
||||
if (definition == null)
|
||||
{
|
||||
Debug.LogWarning($"[PlayerSpellLoadout] No BuilderSpellDefinition for {slotValue.Kind}.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!definition.ServerCast(OwnerClientId, targetPoint)) return;
|
||||
|
||||
slotValue.CooldownEndServerTime = NetworkManager.ServerTime.Time + definition.Cooldown;
|
||||
spells[slot] = slotValue;
|
||||
|
||||
PlayVfxClientRpc(slotValue.Kind, targetPoint);
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
private void PlayVfxClientRpc(BuilderSpellKind kind, Vector3 targetPoint)
|
||||
{
|
||||
BuilderSpellPool.Instance?.Get(kind)?.ClientPlayVfx(targetPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue