Adding a ton of funcitonality to the builder's movement and build queue

This commit is contained in:
Matt F 2026-05-05 22:01:40 -07:00
parent a63cce53e2
commit f05734e19b
31 changed files with 3104 additions and 339 deletions

View file

@ -0,0 +1,182 @@
// Assets/_Project/Scripts/Gameplay/BuildJob.cs
using System;
using Unity.Netcode;
using UnityEngine;
namespace TD.Gameplay
{
/// <summary>
/// One entry in a <see cref="Builder"/>'s build queue. Replicated as part of a
/// <see cref="NetworkList{T}"/> on the Builder so all clients can render queued
/// ghosts and progress without per-job RPCs.
/// </summary>
/// <remarks>
/// <para><b>Identity.</b> <see cref="JobId"/> is a server-assigned, monotonically
/// increasing identifier so cancel/lookup operations can target a specific job
/// regardless of its current index in the NetworkList. Index-based addressing
/// breaks the moment any job is removed.</para>
///
/// <para><b>Stage transitions.</b> Jobs progress
/// <see cref="BuildStage.Queued"/> → <see cref="BuildStage.Constructing"/> →
/// (removed when complete). Only the head of the queue can transition to
/// Constructing; tail jobs stay Queued until they reach the head.</para>
///
/// <para><b>Time fields.</b> <see cref="ConstructionStartServerTime"/> is set on
/// the server using <c>NetworkManager.ServerTime.TimeAsFloat</c> at the moment
/// construction begins. Clients compute current stage as
/// <c>floor(elapsed / (BuildTime / 4))</c> against the same server time, so all
/// peers see identical staging without per-stage RPC chatter.</para>
///
/// <para><b>INetworkSerializable.</b> Required for use in
/// <see cref="NetworkList{T}"/>. Serializes only the minimum needed fields;
/// derived data (footprint size, gold cost) is looked up from the
/// TowerDefinition by <see cref="TowerTypeId"/> on each peer.</para>
/// </remarks>
[Serializable]
public struct BuildJob : INetworkSerializable, IEquatable<BuildJob>
{
// ----- Persistent fields ------------------------------------------
/// <summary>Server-assigned unique ID. Stable across NetworkList reorderings.</summary>
public ulong JobId;
/// <summary>Footprint anchor (SW corner, world-tile coords).</summary>
public Vector2Int Anchor;
/// <summary>Index into <c>TowerPlacementManager.towerDefinitions[]</c>.</summary>
public int TowerTypeId;
/// <summary>Current stage. See <see cref="BuildStage"/>.</summary>
public BuildStage Stage;
/// <summary>
/// Server time (seconds) at which the current Constructing run began.
/// -1 while the job is Queued or Paused (no active timer running).
/// Read by clients to compute the current visual stage locally as
/// <c>(now - ConstructionStartServerTime) + AccumulatedConstructionTime</c>.
/// </summary>
public float ConstructionStartServerTime;
/// <summary>
/// Construction time accumulated across previous Constructing runs.
/// Used to preserve progress across pause/resume cycles.
/// At pause, set to <c>elapsed_in_current_run + previous_accumulated</c>.
/// At resume, <c>ConstructionStartServerTime</c> is reset to "now" and
/// total progress is computed as
/// <c>(now - ConstructionStartServerTime) + AccumulatedConstructionTime</c>.
/// 0 for jobs that have never been paused.
/// </summary>
public float AccumulatedConstructionTime;
/// <summary>
/// Gold the player paid when this job was queued. Used to refund on
/// cancellation. Stored on the job (not looked up from the definition)
/// so that a future "tower price change mid-match" mechanic refunds
/// what was actually paid.
/// </summary>
public int GoldSpent;
// ----- Convenience constructors -----------------------------------
public static BuildJob CreateQueued(ulong jobId, Vector2Int anchor, int towerTypeId, int goldSpent)
{
return new BuildJob
{
JobId = jobId,
Anchor = anchor,
TowerTypeId = towerTypeId,
Stage = BuildStage.Queued,
ConstructionStartServerTime = -1f,
AccumulatedConstructionTime = 0f,
GoldSpent = goldSpent,
};
}
// ----- INetworkSerializable ---------------------------------------
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref JobId);
serializer.SerializeValue(ref Anchor);
serializer.SerializeValue(ref TowerTypeId);
// BuildStage is a byte-backed enum; serialize as byte for forward-compat
// and to avoid relying on default enum serialization assumptions.
byte stageByte = (byte)Stage;
serializer.SerializeValue(ref stageByte);
Stage = (BuildStage)stageByte;
serializer.SerializeValue(ref ConstructionStartServerTime);
serializer.SerializeValue(ref AccumulatedConstructionTime);
serializer.SerializeValue(ref GoldSpent);
}
// ----- IEquatable -------------------------------------------------
//
// NetworkList<T> requires IEquatable<T> in NGO 2.x. Critically, NGO's
// indexer setter (jobs[i] = newValue) short-circuits the write when
// Equals returns true — see NGO 2.7.0 changelog. If we only compared
// by JobId, mutating Stage or ConstructionStartServerTime on a job and
// writing it back would be silently dropped, leaving the list with the
// old struct values forever. Compare every field that we ever mutate
// after creation.
//
// GetHashCode uses JobId only because that's the unique identity and is
// sufficient for hashing — the full-field comparison only happens on
// hash collision (or where IEquatable bypasses the hash entirely,
// which is the case in NetworkList's indexer setter).
public bool Equals(BuildJob other)
{
return JobId == other.JobId
&& Anchor == other.Anchor
&& TowerTypeId == other.TowerTypeId
&& Stage == other.Stage
&& ConstructionStartServerTime == other.ConstructionStartServerTime
&& AccumulatedConstructionTime == other.AccumulatedConstructionTime
&& GoldSpent == other.GoldSpent;
}
public override bool Equals(object obj) => obj is BuildJob other && Equals(other);
public override int GetHashCode() => JobId.GetHashCode();
}
/// <summary>
/// Lifecycle stage of a <see cref="BuildJob"/>.
/// </summary>
/// <remarks>
/// Backed by byte to keep the serialized payload small and to allow the byte
/// round-trip in <see cref="BuildJob.NetworkSerialize{T}"/>.
/// </remarks>
public enum BuildStage : byte
{
/// <summary>
/// Job is in the queue but the builder has not yet arrived at it.
/// Tile is occupied (no other tower can be queued/placed there) but
/// remains walkable — enemies pass through queued ghosts because the
/// ghost represents intent, not a structure.
/// </summary>
Queued = 0,
/// <summary>
/// Builder has arrived and construction is in progress. Tile is
/// occupied AND non-walkable — this is the moment the maze actually
/// changes. Path re-validation runs at the transition into this stage.
/// </summary>
Constructing = 1,
/// <summary>
/// Construction was interrupted by the builder being moved away.
/// Tile remains occupied and non-walkable (the half-built tower is
/// still physical, still blocks enemies). The cube's Y-scale is frozen
/// at the level it reached when paused. Resume returns to Constructing
/// stage with previously-accumulated progress preserved.
///
/// While the head job is Paused, the queue contains exactly this one
/// job — all other queued jobs are refunded and removed at the moment
/// of pausing.
/// </summary>
Paused = 2,
}
}