// Assets/_Project/Scripts/Gameplay/BuildJob.cs
using System;
using Unity.Netcode;
using UnityEngine;
namespace TD.Gameplay
{
///
/// One entry in a 's build queue. Replicated as part of a
/// on the Builder so all clients can render queued
/// ghosts and progress without per-job RPCs.
///
///
/// Identity. 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.
///
/// Stage transitions. Jobs progress
/// → →
/// (removed when complete). Only the head of the queue can transition to
/// Constructing; tail jobs stay Queued until they reach the head.
///
/// Time fields. is set on
/// the server using NetworkManager.ServerTime.TimeAsFloat at the moment
/// construction begins. Clients compute current stage as
/// floor(elapsed / (BuildTime / 4)) against the same server time, so all
/// peers see identical staging without per-stage RPC chatter.
///
/// INetworkSerializable. Required for use in
/// . Serializes only the minimum needed fields;
/// derived data (footprint size, gold cost) is looked up from the
/// TowerDefinition by on each peer.
///
[Serializable]
public struct BuildJob : INetworkSerializable, IEquatable
{
// ----- Persistent fields ------------------------------------------
/// Server-assigned unique ID. Stable across NetworkList reorderings.
public ulong JobId;
/// Footprint anchor (SW corner, world-tile coords).
public Vector2Int Anchor;
/// Index into TowerPlacementManager.towerDefinitions[].
public int TowerTypeId;
/// Current stage. See .
public BuildStage Stage;
///
/// 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
/// (now - ConstructionStartServerTime) + AccumulatedConstructionTime.
///
public float ConstructionStartServerTime;
///
/// Construction time accumulated across previous Constructing runs.
/// Used to preserve progress across pause/resume cycles.
/// At pause, set to elapsed_in_current_run + previous_accumulated.
/// At resume, ConstructionStartServerTime is reset to "now" and
/// total progress is computed as
/// (now - ConstructionStartServerTime) + AccumulatedConstructionTime.
/// 0 for jobs that have never been paused.
///
public float AccumulatedConstructionTime;
///
/// 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.
///
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(BufferSerializer 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 requires IEquatable 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();
}
///
/// Lifecycle stage of a .
///
///
/// Backed by byte to keep the serialized payload small and to allow the byte
/// round-trip in .
///
public enum BuildStage : byte
{
///
/// 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.
///
Queued = 0,
///
/// 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.
///
Constructing = 1,
///
/// 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.
///
Paused = 2,
}
}