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
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue