Adding the bones of the drafting system by removing the wall and siege towers from the base set and adding them temporarily as choices for the draft. Also adding Project_Context and Project_Roadmap documents to use with Claude
This commit is contained in:
parent
1d03689dad
commit
3ceebed8f6
22 changed files with 1060 additions and 431 deletions
8
Assets/_Project/Scripts/Gameplay/Draft.meta
Normal file
8
Assets/_Project/Scripts/Gameplay/Draft.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a1b2c3d4e5f60718293a4b5c6d7e8f90
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
59
Assets/_Project/Scripts/Gameplay/Draft/DraftOption.cs
Normal file
59
Assets/_Project/Scripts/Gameplay/Draft/DraftOption.cs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// Assets/_Project/Scripts/Gameplay/Draft/DraftOption.cs
|
||||
using UnityEngine;
|
||||
|
||||
namespace TD.Gameplay.Draft
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for one authored draft choice. The draft system offers a player a
|
||||
/// weighted-random selection of these between waves; the player picks one and the
|
||||
/// server applies its effect.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>One asset per choice.</b> Concrete subclasses (e.g.
|
||||
/// <see cref="NewTowerDraftOption"/>) carry the payload and implement
|
||||
/// <see cref="ServerApply"/>. New choice types (systemic upgrade, builder ability,
|
||||
/// relic quest, enemy debuff) are added as new subclasses — the draft spine
|
||||
/// (<see cref="DraftService"/>, <see cref="PlayerDraft"/>, HUD) is type-agnostic.</para>
|
||||
///
|
||||
/// <para><b>Identity.</b> Options live in <see cref="DraftPool"/> and are referenced
|
||||
/// over the network by their pool index (<c>DraftOptionId</c>) — the same
|
||||
/// stable-within-a-match index pattern used by the tower catalog.</para>
|
||||
///
|
||||
/// <para><b>Server-only logic.</b> <see cref="IsValidFor"/> and
|
||||
/// <see cref="ServerApply"/> run on the server and operate on the target player's
|
||||
/// per-player components (deck, gold, etc.). The ScriptableObject holds no per-player
|
||||
/// state, so a single asset is shared across all players and matches.</para>
|
||||
/// </remarks>
|
||||
public abstract class DraftOption : ScriptableObject
|
||||
{
|
||||
[Header("Presentation")]
|
||||
[Tooltip("Name shown on the draft card.")]
|
||||
public string DisplayName;
|
||||
|
||||
[Tooltip("Short description shown on the draft card.")]
|
||||
[TextArea(2, 4)]
|
||||
public string Description;
|
||||
|
||||
[Tooltip("Icon shown on the draft card. Optional for placeholder content.")]
|
||||
public Sprite Icon;
|
||||
|
||||
[Header("Generation")]
|
||||
[Tooltip("Relative draw weight. Higher = offered more often. Rarer rewards use " +
|
||||
"lower weights. Must be > 0.")]
|
||||
[Min(0.0001f)]
|
||||
public float Weight = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Server-only: can this option validly be OFFERED to the given player right now?
|
||||
/// Lets the generator skip options that would be a no-op (e.g. a tower the player
|
||||
/// already owns). Default true; override when an option has prerequisites.
|
||||
/// </summary>
|
||||
public virtual bool IsValidFor(ulong clientId) => true;
|
||||
|
||||
/// <summary>
|
||||
/// Server-only: apply this option's effect to the given player. Returns false if
|
||||
/// it could not be applied (caller treats as a no-op and logs).
|
||||
/// </summary>
|
||||
public abstract bool ServerApply(ulong clientId);
|
||||
}
|
||||
}
|
||||
11
Assets/_Project/Scripts/Gameplay/Draft/DraftOption.cs.meta
Normal file
11
Assets/_Project/Scripts/Gameplay/Draft/DraftOption.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3f9b1c7a2e8d4a16b0c5e9f2a7d34b81
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
64
Assets/_Project/Scripts/Gameplay/Draft/DraftPool.cs
Normal file
64
Assets/_Project/Scripts/Gameplay/Draft/DraftPool.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
// Assets/_Project/Scripts/Gameplay/Draft/DraftPool.cs
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace TD.Gameplay.Draft
|
||||
{
|
||||
/// <summary>
|
||||
/// Scene singleton holding every <see cref="DraftOption"/> available this match. The
|
||||
/// array index is the option's <c>DraftOptionId</c> — the stable identifier used to
|
||||
/// replicate offers and picks over the network (mirrors the tower catalog pattern).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Plain MonoBehaviour: identical on every peer (same assets), so there is nothing to
|
||||
/// sync. The server reads it to generate offers and apply picks; clients read it to
|
||||
/// render draft cards from the replicated option ids.
|
||||
/// </remarks>
|
||||
public class DraftPool : MonoBehaviour
|
||||
{
|
||||
public static DraftPool Instance { get; private set; }
|
||||
|
||||
[Tooltip("Every DraftOption asset that can be offered this match. Array index is " +
|
||||
"the DraftOptionId used over the network.")]
|
||||
[SerializeField] private DraftOption[] options;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Debug.LogError("[DraftPool] Multiple instances detected. Only one per scene.");
|
||||
return;
|
||||
}
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
|
||||
/// <summary>Number of options in the pool.</summary>
|
||||
public int Count => options?.Length ?? 0;
|
||||
|
||||
/// <summary>Returns the option at <paramref name="id"/> (its DraftOptionId), or null.</summary>
|
||||
public DraftOption Get(int id)
|
||||
=> (options != null && id >= 0 && id < options.Length) ? options[id] : null;
|
||||
|
||||
/// <summary>
|
||||
/// Server-only helper: collects the ids of every option currently valid to offer
|
||||
/// to <paramref name="clientId"/> (skips nulls and options whose
|
||||
/// <see cref="DraftOption.IsValidFor"/> returns false). Used by
|
||||
/// <see cref="DraftService"/> as the candidate set for the weighted draw.
|
||||
/// </summary>
|
||||
public void CollectValidIds(ulong clientId, List<int> into)
|
||||
{
|
||||
into.Clear();
|
||||
if (options == null) return;
|
||||
for (int i = 0; i < options.Length; i++)
|
||||
{
|
||||
if (options[i] == null) continue;
|
||||
if (options[i].IsValidFor(clientId)) into.Add(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Project/Scripts/Gameplay/Draft/DraftPool.cs.meta
Normal file
11
Assets/_Project/Scripts/Gameplay/Draft/DraftPool.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5a7e1f3c9d2b4867a1c0e4f8b3d6792a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
132
Assets/_Project/Scripts/Gameplay/Draft/DraftService.cs
Normal file
132
Assets/_Project/Scripts/Gameplay/Draft/DraftService.cs
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
// Assets/_Project/Scripts/Gameplay/Draft/DraftService.cs
|
||||
using System.Collections.Generic;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace TD.Gameplay.Draft
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-authoritative draft orchestrator: generates weighted-random option sets and
|
||||
/// offers them to players' <see cref="PlayerDraft"/> components. Scene singleton.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Plain MonoBehaviour — the replicated state lives on <see cref="PlayerDraft"/>; this
|
||||
/// is pure server logic plus two designer tunables (<see cref="optionsPerDraft"/>,
|
||||
/// <see cref="rerollCost"/>). It exists on every peer (scene object) but only the server
|
||||
/// drives it; clients read <see cref="RerollCost"/> for the HUD's "buy roll" button.
|
||||
///
|
||||
/// <para><b>Driven by <see cref="WaveManager"/>:</b> <see cref="ServerOfferToAll"/> at
|
||||
/// the start of each prep phase; <see cref="ServerAutoResolveAll"/> at prep end (and on
|
||||
/// dev force-advance) so unpicked free drafts aren't wasted.</para>
|
||||
/// </remarks>
|
||||
public class DraftService : MonoBehaviour
|
||||
{
|
||||
public static DraftService Instance { get; private set; }
|
||||
|
||||
[Tooltip("How many options each draft offers (cards shown). Standard roguelike draw is 3.")]
|
||||
[SerializeField] private int optionsPerDraft = 3;
|
||||
|
||||
[Tooltip("Gold cost for a player to buy one extra draft roll from the shop.")]
|
||||
[SerializeField] private int rerollCost = 50;
|
||||
|
||||
/// <summary>Gold cost of one bought reroll. Read by the HUD and the buy RPC.</summary>
|
||||
public int RerollCost => rerollCost;
|
||||
|
||||
// Scratch lists reused across generation to avoid per-draft GC. Server is
|
||||
// single-threaded, so sharing them across the per-player loop is safe.
|
||||
private readonly List<int> candidateScratch = new List<int>();
|
||||
private readonly List<int> resultScratch = new List<int>();
|
||||
|
||||
private static bool IsServer
|
||||
=> NetworkManager.Singleton != null && NetworkManager.Singleton.IsServer;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Debug.LogError("[DraftService] Multiple instances detected. Only one per scene.");
|
||||
return;
|
||||
}
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
|
||||
// ----- Server orchestration ---------------------------------------
|
||||
|
||||
/// <summary>Server-only: offer a fresh draft to every connected player.</summary>
|
||||
public void ServerOfferToAll()
|
||||
{
|
||||
if (!IsServer) return;
|
||||
if (DraftPool.Instance == null)
|
||||
{
|
||||
Debug.LogWarning("[DraftService] No DraftPool in scene — no drafts will be offered.");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var pms in PlayerMatchState.AllPlayers)
|
||||
ServerOfferTo(pms.OwnerClientId);
|
||||
}
|
||||
|
||||
/// <summary>Server-only: offer a fresh draft to one player (also used by the paid reroll).</summary>
|
||||
public void ServerOfferTo(ulong clientId)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
var draft = PlayerDraft.GetForClient(clientId);
|
||||
if (draft == null) return;
|
||||
|
||||
GenerateOptionIds(clientId, optionsPerDraft, resultScratch);
|
||||
draft.ServerOffer(resultScratch);
|
||||
}
|
||||
|
||||
/// <summary>Server-only: auto-resolve any unpicked drafts (called at prep end).</summary>
|
||||
public void ServerAutoResolveAll()
|
||||
{
|
||||
if (!IsServer) return;
|
||||
foreach (var pms in PlayerMatchState.AllPlayers)
|
||||
PlayerDraft.GetForClient(pms.OwnerClientId)?.ServerAutoResolve();
|
||||
}
|
||||
|
||||
// ----- Generation -------------------------------------------------
|
||||
|
||||
// Weighted random draw WITHOUT replacement: picks `count` distinct option ids from
|
||||
// the set currently valid for this player. If fewer valid options exist than
|
||||
// requested, offers what's available (possibly zero).
|
||||
private void GenerateOptionIds(ulong clientId, int count, List<int> into)
|
||||
{
|
||||
into.Clear();
|
||||
var pool = DraftPool.Instance;
|
||||
if (pool == null) return;
|
||||
|
||||
pool.CollectValidIds(clientId, candidateScratch);
|
||||
|
||||
int draws = Mathf.Min(count, candidateScratch.Count);
|
||||
for (int n = 0; n < draws; n++)
|
||||
{
|
||||
float total = 0f;
|
||||
for (int i = 0; i < candidateScratch.Count; i++)
|
||||
total += OptionWeight(pool, candidateScratch[i]);
|
||||
|
||||
float r = Random.value * total;
|
||||
int chosen = candidateScratch.Count - 1; // fallback to last on float drift
|
||||
for (int i = 0; i < candidateScratch.Count; i++)
|
||||
{
|
||||
r -= OptionWeight(pool, candidateScratch[i]);
|
||||
if (r <= 0f) { chosen = i; break; }
|
||||
}
|
||||
|
||||
into.Add(candidateScratch[chosen]);
|
||||
candidateScratch.RemoveAt(chosen); // without replacement → distinct options
|
||||
}
|
||||
}
|
||||
|
||||
private static float OptionWeight(DraftPool pool, int id)
|
||||
{
|
||||
var opt = pool.Get(id);
|
||||
return opt != null ? Mathf.Max(0.0001f, opt.Weight) : 0.0001f;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Project/Scripts/Gameplay/Draft/DraftService.cs.meta
Normal file
11
Assets/_Project/Scripts/Gameplay/Draft/DraftService.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9d3a7c5e1b8f42606a2d4e9f1c7b3850
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
// Assets/_Project/Scripts/Gameplay/Draft/NewTowerDraftOption.cs
|
||||
using UnityEngine;
|
||||
using TD.Towers;
|
||||
|
||||
namespace TD.Gameplay.Draft
|
||||
{
|
||||
/// <summary>
|
||||
/// Draft choice #1 — "add a new tower to your arsenal". Grants the player a tower,
|
||||
/// adding it to their <see cref="PlayerTowerDeck"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only offered when the tower is in the match catalog AND the player doesn't already
|
||||
/// own it, so the draft never wastes a slot on a no-op.
|
||||
/// </remarks>
|
||||
[CreateAssetMenu(fileName = "NewTowerOption", menuName = "TD/Draft/New Tower Option")]
|
||||
public class NewTowerDraftOption : DraftOption
|
||||
{
|
||||
[Header("Payload")]
|
||||
[Tooltip("The tower this option grants. Must also be present in the " +
|
||||
"TowerPlacementManager catalog (that's where its TowerTypeId comes from).")]
|
||||
public TowerDefinition Tower;
|
||||
|
||||
public override bool IsValidFor(ulong clientId)
|
||||
{
|
||||
var deck = PlayerTowerDeck.GetForClient(clientId);
|
||||
var pm = TowerPlacementManager.Instance;
|
||||
if (deck == null || pm == null || Tower == null) return false;
|
||||
|
||||
// Not in the catalog → no TypeId → can't be granted or placed.
|
||||
if (!pm.TryGetTypeId(Tower, out int typeId)) return false;
|
||||
|
||||
// Only offer towers the player hasn't unlocked yet.
|
||||
return !deck.Contains(typeId);
|
||||
}
|
||||
|
||||
public override bool ServerApply(ulong clientId)
|
||||
{
|
||||
var deck = PlayerTowerDeck.GetForClient(clientId);
|
||||
var pm = TowerPlacementManager.Instance;
|
||||
if (deck == null || pm == null || Tower == null) return false;
|
||||
|
||||
if (!pm.TryGetTypeId(Tower, out int typeId))
|
||||
{
|
||||
Debug.LogError($"[NewTowerDraftOption] '{Tower.name}' is not in the tower catalog; " +
|
||||
$"cannot grant. Add it to TowerPlacementManager.towerDefinitions.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return deck.ServerGrantTower(typeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8c2d6e4f1a9b4c73d05e8f1a2b6c9d40
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
167
Assets/_Project/Scripts/Gameplay/Draft/PlayerDraft.cs
Normal file
167
Assets/_Project/Scripts/Gameplay/Draft/PlayerDraft.cs
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
// Assets/_Project/Scripts/Gameplay/Draft/PlayerDraft.cs
|
||||
using System.Collections.Generic;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace TD.Gameplay.Draft
|
||||
{
|
||||
/// <summary>
|
||||
/// Per-player draft state — the set of <see cref="DraftOption"/>s currently offered to
|
||||
/// one player. Lives on the Player prefab alongside <see cref="PlayerTowerDeck"/>,
|
||||
/// <see cref="PlayerGoldManager"/>, etc.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>Offered set.</b> <see cref="currentOptionIds"/> 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.</para>
|
||||
///
|
||||
/// <para><b>Authority.</b> The server offers (<see cref="ServerOffer"/>), resolves
|
||||
/// (<see cref="ServerResolve"/> / <see cref="ServerAutoResolve"/>), and applies the
|
||||
/// chosen option. The owning client only sends intent via
|
||||
/// <see cref="RequestPickRpc"/> and <see cref="RequestBuyRerollRpc"/>.</para>
|
||||
///
|
||||
/// <para><b>Generation</b> lives in <see cref="DraftService"/>; this component is just
|
||||
/// the replicated state + the pick/buy entry points.</para>
|
||||
/// </remarks>
|
||||
public class PlayerDraft : NetworkBehaviour
|
||||
{
|
||||
// ----- Static registry (mirrors PlayerGoldManager / PlayerTowerDeck) -----
|
||||
|
||||
private static readonly Dictionary<ulong, PlayerDraft> s_byClientId
|
||||
= new Dictionary<ulong, PlayerDraft>();
|
||||
|
||||
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<int> currentOptionIds;
|
||||
|
||||
/// <summary>Fired on every peer when the offered set changes (offered, picked,
|
||||
/// cleared). The HUD subscribes to show/hide/rebuild the draft overlay.</summary>
|
||||
public event System.Action OnDraftChanged;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
currentOptionIds = new NetworkList<int>();
|
||||
}
|
||||
|
||||
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<int> _) => OnDraftChanged?.Invoke();
|
||||
|
||||
// ----- Read API ---------------------------------------------------
|
||||
|
||||
/// <summary>True if the player currently has options to choose from.</summary>
|
||||
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 ------------------------------------
|
||||
|
||||
/// <summary>Server-only: replace the offered set with <paramref name="ids"/>.</summary>
|
||||
public void ServerOffer(IReadOnlyList<int> ids)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
currentOptionIds.Clear();
|
||||
if (ids == null) return;
|
||||
for (int i = 0; i < ids.Count; i++)
|
||||
currentOptionIds.Add(ids[i]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public void ServerAutoResolve()
|
||||
{
|
||||
if (!IsServer) return;
|
||||
if (currentOptionIds.Count == 0) return;
|
||||
ServerResolve(currentOptionIds[0]);
|
||||
}
|
||||
|
||||
// ----- Owner → server RPCs ----------------------------------------
|
||||
|
||||
/// <summary>Owning client: pick one of the offered options by id.</summary>
|
||||
[Rpc(SendTo.Server, RequireOwnership = true)]
|
||||
public void RequestPickRpc(int optionId)
|
||||
{
|
||||
ServerResolve(optionId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
[Rpc(SendTo.Server, RequireOwnership = true)]
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Project/Scripts/Gameplay/Draft/PlayerDraft.cs.meta
Normal file
11
Assets/_Project/Scripts/Gameplay/Draft/PlayerDraft.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2b8f4d1e6c3a497051d8e2f7a9c1b063
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -3,6 +3,7 @@ using System.Collections;
|
|||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using TD.Core;
|
||||
using TD.Gameplay.Draft;
|
||||
using TD.Levels;
|
||||
using TD.UI;
|
||||
|
||||
|
|
@ -318,6 +319,11 @@ namespace TD.Gameplay
|
|||
activeEnemyCount = 0;
|
||||
spawningComplete = false;
|
||||
heldEnemies.Clear();
|
||||
|
||||
// Force-advancing interrupts the prep phase, so resolve any drafts the players
|
||||
// had open before skipping ahead.
|
||||
DraftService.Instance?.ServerAutoResolveAll();
|
||||
|
||||
StartNextWave(skipPrep: true);
|
||||
}
|
||||
|
||||
|
|
@ -330,6 +336,10 @@ namespace TD.Gameplay
|
|||
// observe the value via NetworkVariable replication.
|
||||
if (!skipPrep)
|
||||
{
|
||||
// Open a draft for every player at the start of the build phase. They pick
|
||||
// during prep; any unpicked draft is auto-resolved when the timer expires.
|
||||
DraftService.Instance?.ServerOfferToAll();
|
||||
|
||||
prepCountdown.Value = def.PrepTime;
|
||||
float remaining = def.PrepTime;
|
||||
// Throttle network sync to ~10 Hz. NetworkVariable replicates on every
|
||||
|
|
@ -348,6 +358,10 @@ namespace TD.Gameplay
|
|||
nextSync = remaining - NetworkSyncInterval;
|
||||
}
|
||||
}
|
||||
|
||||
// Prep timer expired — auto-resolve any draft the player didn't pick so
|
||||
// the free reward isn't wasted.
|
||||
DraftService.Instance?.ServerAutoResolveAll();
|
||||
}
|
||||
// Ensure the countdown reads zero entering the spawn phase, regardless of
|
||||
// whether prep was skipped or just expired.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue