// 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, } }