Adding a ton of funcitonality to the builder's movement and build queue
This commit is contained in:
parent
a63cce53e2
commit
f05734e19b
31 changed files with 3104 additions and 339 deletions
478
Assets/_Project/Scripts/Gameplay/BuildSiteVisual.cs
Normal file
478
Assets/_Project/Scripts/Gameplay/BuildSiteVisual.cs
Normal file
|
|
@ -0,0 +1,478 @@
|
|||
// Assets/_Project/Scripts/Gameplay/BuildSiteVisual.cs
|
||||
using Unity.Collections;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using TD.Core;
|
||||
using TD.Towers;
|
||||
|
||||
namespace TD.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Visual representation of an in-flight <see cref="BuildJob"/>: the green
|
||||
/// queued ghost, and the staged construction animation (4 stages of growing
|
||||
/// height for the testing cube). One NetworkObject per active job. Despawned
|
||||
/// when the job is cancelled or when the real <see cref="TowerInstance"/>
|
||||
/// takes its place at construction-complete.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>Why a separate prefab from TowerInstance.</b> The build-site visual
|
||||
/// has different rendering (transparent green or partial-height cube), no combat,
|
||||
/// no grid-stamping (the Builder owns those state transitions), and a much
|
||||
/// shorter lifecycle. Sharing a prefab with TowerInstance would mean adding
|
||||
/// "am I real or a ghost" branching to every TowerInstance code path. Two
|
||||
/// prefabs, two responsibilities.</para>
|
||||
///
|
||||
/// <para><b>Stage replication.</b> Stage and ConstructionStartServerTime are
|
||||
/// replicated as NetworkVariables so all peers compute identical visuals locally
|
||||
/// from <c>NetworkManager.ServerTime.TimeAsFloat</c>. Only the server writes;
|
||||
/// clients read.</para>
|
||||
///
|
||||
/// <para><b>Visual model.</b> The prefab inspector points at a re-tinted copy
|
||||
/// of the tower's mesh (the same testing cube). At Stage = Queued, the visual
|
||||
/// is a translucent green cube at full footprint scale but reduced Y. At
|
||||
/// Stage = Constructing, the cube grows in 4 sub-stages over BuildTime.</para>
|
||||
///
|
||||
/// <para><b>No grid stamping here.</b> Walkability and occupancy stamps are
|
||||
/// driven by the Builder (queue-time stamps occupied=true / walkable=true,
|
||||
/// construction-start stamps walkable=false, completion is unchanged because
|
||||
/// TowerInstance takes over). Adding stamping here would create double-write
|
||||
/// races with the Builder. See lessons in the project context doc.</para>
|
||||
/// </remarks>
|
||||
[RequireComponent(typeof(NetworkObject))]
|
||||
public class BuildSiteVisual : NetworkBehaviour
|
||||
{
|
||||
// ----- Inspector --------------------------------------------------
|
||||
|
||||
[Header("Visuals")]
|
||||
[Tooltip("Renderers tinted with the queued-ghost color. Typically every " +
|
||||
"MeshRenderer on the prefab. Auto-populated from children if empty.")]
|
||||
[SerializeField] private MeshRenderer[] tintedRenderers;
|
||||
|
||||
[Tooltip("Transform that gets Y-scaled to represent construction progress. " +
|
||||
"Typically the visual mesh's transform. The growth axis is local Y.")]
|
||||
[SerializeField] private Transform scaleTarget;
|
||||
|
||||
[Tooltip("Material applied while the job is Queued (translucent green).")]
|
||||
[SerializeField] private Material queuedMaterial;
|
||||
|
||||
[Tooltip("Material applied while the job is Constructing (opaque, tinted by owner).")]
|
||||
[SerializeField] private Material constructingMaterial;
|
||||
|
||||
[Tooltip("Material applied while the job is Paused. Visually distinct from " +
|
||||
"Constructing so players can tell at a glance which builds need a builder. " +
|
||||
"Suggested: muted/grey-tinted variant of the constructing material.")]
|
||||
[SerializeField] private Material pausedMaterial;
|
||||
|
||||
[Header("Construction stages")]
|
||||
[Tooltip("Number of discrete growth stages while constructing. 4 matches the " +
|
||||
"design doc (1/4 → 2/4 → 3/4 → 4/4 height).")]
|
||||
[SerializeField] private int stageCount = 4;
|
||||
|
||||
[Tooltip("Y-scale applied to scaleTarget when Stage == Queued. Visually " +
|
||||
"distinct from any constructing height so the queued ghost reads as " +
|
||||
"'intent, not progress'.")]
|
||||
[SerializeField] private float queuedYScale = 0.15f;
|
||||
|
||||
// ----- Networked state --------------------------------------------
|
||||
|
||||
// Replicated definition name so clients can resolve the source TowerDefinition
|
||||
// (matches TowerInstance's pattern). Used for footprint size only — the visual
|
||||
// prefab is the same regardless of tower type for now.
|
||||
private readonly NetworkVariable<FixedString64Bytes> definitionName =
|
||||
new NetworkVariable<FixedString64Bytes>(
|
||||
default,
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server);
|
||||
|
||||
// Replicated owner slot for color tinting. Mirrors TowerInstance.
|
||||
private readonly NetworkVariable<PlayerSlot> ownerSlot =
|
||||
new NetworkVariable<PlayerSlot>(
|
||||
PlayerSlot.None,
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server);
|
||||
|
||||
// Anchor tile (SW corner of the footprint, world-tile coords). Replicated so
|
||||
// shelved visuals are self-describing — when a player clicks one to resume,
|
||||
// the server can rebuild a BuildJob from these fields without consulting any
|
||||
// separate registry.
|
||||
private readonly NetworkVariable<Vector2Int> anchor =
|
||||
new NetworkVariable<Vector2Int>(
|
||||
Vector2Int.zero,
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server);
|
||||
|
||||
// Tower type ID (index into TowerPlacementManager.towerDefinitions[]). Replicated
|
||||
// for the same self-describing reason as Anchor.
|
||||
private readonly NetworkVariable<int> towerTypeId =
|
||||
new NetworkVariable<int>(
|
||||
0,
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server);
|
||||
|
||||
// Gold the player paid to queue this build. Carried on the visual so resume
|
||||
// can refund the correct amount if the player later cancels. (Cancellation
|
||||
// gesture deferred to HUD; this field is the data dependency.)
|
||||
private readonly NetworkVariable<int> goldSpent =
|
||||
new NetworkVariable<int>(
|
||||
0,
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server);
|
||||
|
||||
// True iff this build site has been "shelved" — removed from its owning Builder's
|
||||
// queue and now a standalone object waiting to be resumed via right-click.
|
||||
// Shelved visuals are responsible for their own grid-state cleanup on despawn
|
||||
// (since the Builder no longer tracks them in jobIdToVisual).
|
||||
private readonly NetworkVariable<bool> isShelved =
|
||||
new NetworkVariable<bool>(
|
||||
false,
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server);
|
||||
|
||||
// Current stage. Drives material swap and Y-scale animation.
|
||||
private readonly NetworkVariable<BuildStage> currentStage =
|
||||
new NetworkVariable<BuildStage>(
|
||||
BuildStage.Queued,
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server);
|
||||
|
||||
// Server time at which the current Constructing run began. -1 while Queued or Paused.
|
||||
// Used together with accumulatedConstructionTime to compute total progress:
|
||||
// total = (now - constructionStartServerTime) + accumulatedConstructionTime.
|
||||
private readonly NetworkVariable<float> constructionStartServerTime =
|
||||
new NetworkVariable<float>(
|
||||
-1f,
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server);
|
||||
|
||||
// Construction time accumulated across previous Constructing runs (for pause/resume).
|
||||
// 0 for jobs that have never been paused. At pause, set to the elapsed time of the
|
||||
// current run added to whatever was already accumulated.
|
||||
private readonly NetworkVariable<float> accumulatedConstructionTime =
|
||||
new NetworkVariable<float>(
|
||||
0f,
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server);
|
||||
|
||||
// BuildTime is replicated rather than looked up so clients don't need to
|
||||
// resolve the TowerDefinition before they can render progress.
|
||||
// (Resolution can race with the first stage update otherwise.)
|
||||
private readonly NetworkVariable<float> buildTime =
|
||||
new NetworkVariable<float>(
|
||||
0f,
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server);
|
||||
|
||||
// ----- Public accessors (read by the input controller on click) --
|
||||
|
||||
// NOTE: ownership identity comes from the inherited NetworkBehaviour.OwnerClientId,
|
||||
// which is correct because we use SpawnWithOwnership in Builder.SpawnBuildSiteVisual.
|
||||
// No redundant NetworkVariable for ownership.
|
||||
|
||||
/// <summary>The current build stage. Read by clients for click-target tests.</summary>
|
||||
public BuildStage CurrentStage => currentStage.Value;
|
||||
|
||||
/// <summary>True iff this visual has been shelved (no longer in its Builder's queue).</summary>
|
||||
public bool IsShelved => isShelved.Value;
|
||||
|
||||
/// <summary>Footprint anchor (SW corner). Used by server to rebuild a BuildJob on resume.</summary>
|
||||
public Vector2Int Anchor => anchor.Value;
|
||||
|
||||
/// <summary>Tower type ID. Used by server to rebuild a BuildJob on resume.</summary>
|
||||
public int TowerTypeId => towerTypeId.Value;
|
||||
|
||||
/// <summary>Gold paid for this build. Used by server to rebuild a BuildJob on resume.</summary>
|
||||
public int GoldSpent => goldSpent.Value;
|
||||
|
||||
/// <summary>Accumulated construction time (across pause/resume cycles). Used on resume.</summary>
|
||||
public float AccumulatedConstructionTime => accumulatedConstructionTime.Value;
|
||||
|
||||
// ----- Pre-spawn init data (server) -------------------------------
|
||||
|
||||
private string pendingDefName;
|
||||
private PlayerSlot pendingOwner = PlayerSlot.None;
|
||||
private float pendingBuildTime;
|
||||
private Vector2Int pendingAnchor;
|
||||
private int pendingTowerTypeId;
|
||||
private int pendingGoldSpent;
|
||||
private bool hasPendingInit;
|
||||
|
||||
// ----- Lifecycle --------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Server-only: stores the data that <see cref="OnNetworkSpawn"/> will write
|
||||
/// into NetworkVariables. Must be called between Instantiate and Spawn().
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Owner identity is conveyed through NGO ownership (via SpawnWithOwnership),
|
||||
/// not through this method — see <see cref="NetworkBehaviour.OwnerClientId"/>.
|
||||
/// </remarks>
|
||||
public void InitializeServer(TowerDefinition def, PlayerSlot owner,
|
||||
Vector2Int anchorTile, int towerTypeIdValue,
|
||||
int goldSpentValue)
|
||||
{
|
||||
var nm = NetworkManager.Singleton;
|
||||
if (nm == null || !nm.IsServer)
|
||||
{
|
||||
Debug.LogError("[BuildSiteVisual] InitializeServer called when not running as server.");
|
||||
return;
|
||||
}
|
||||
|
||||
pendingDefName = def != null ? def.name : string.Empty;
|
||||
pendingOwner = owner;
|
||||
pendingBuildTime = def != null ? def.BuildTime : 0f;
|
||||
pendingAnchor = anchorTile;
|
||||
pendingTowerTypeId = towerTypeIdValue;
|
||||
pendingGoldSpent = goldSpentValue;
|
||||
hasPendingInit = true;
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
// Auto-populate tinted renderers if not configured in the inspector.
|
||||
if (tintedRenderers == null || tintedRenderers.Length == 0)
|
||||
tintedRenderers = GetComponentsInChildren<MeshRenderer>();
|
||||
|
||||
// Server: now that the NetworkObject is spawned, write the pending init
|
||||
// values into NetworkVariables. NGO captures these into the initial sync
|
||||
// message so clients see correct values on their first OnNetworkSpawn.
|
||||
if (IsServer && hasPendingInit)
|
||||
{
|
||||
definitionName.Value = new FixedString64Bytes(pendingDefName ?? string.Empty);
|
||||
ownerSlot.Value = pendingOwner;
|
||||
buildTime.Value = pendingBuildTime;
|
||||
anchor.Value = pendingAnchor;
|
||||
towerTypeId.Value = pendingTowerTypeId;
|
||||
goldSpent.Value = pendingGoldSpent;
|
||||
hasPendingInit = false;
|
||||
}
|
||||
|
||||
// Subscribe to value changes so visual updates are reactive.
|
||||
currentStage.OnValueChanged += HandleStageChanged;
|
||||
|
||||
// Apply initial visual state based on the (now-replicated) values.
|
||||
ApplyStageVisual(currentStage.Value);
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
currentStage.OnValueChanged -= HandleStageChanged;
|
||||
|
||||
// Server-only cleanup: if this visual was shelved at the time it was
|
||||
// despawned (e.g., the player disconnected while a tower was shelved),
|
||||
// restore walkability and occupancy on the footprint. Non-shelved
|
||||
// visuals are owned by their Builder, which handles cleanup via
|
||||
// jobIdToVisual; we mustn't double-free in that case.
|
||||
if (IsServer && isShelved.Value)
|
||||
{
|
||||
RestoreFootprintGridState();
|
||||
}
|
||||
}
|
||||
|
||||
// Server-only: restore walkability=true and occupancy=false on this build site's
|
||||
// footprint. Called when a shelved visual is despawned without going through a
|
||||
// normal "resume → cancel" cycle (e.g., player disconnect cleanup).
|
||||
private void RestoreFootprintGridState()
|
||||
{
|
||||
var loader = LevelLoader.Instance;
|
||||
if (loader == null || !loader.IsLoaded) return;
|
||||
|
||||
var def = TowerPlacementManager.GetDefinition(towerTypeId.Value);
|
||||
if (def == null) return;
|
||||
|
||||
foreach (var tile in GridCoordinates.GetFootprintTiles(
|
||||
anchor.Value, def.FootprintSize))
|
||||
{
|
||||
loader.SetOccupied(tile, false);
|
||||
loader.SetWalkable(tile, true);
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Per-frame visual update (all peers) ------------------------
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// While constructing, smoothly interpolate Y-scale through the stages
|
||||
// based on server time. This runs on every peer (server + clients) so
|
||||
// visuals stay synchronized regardless of who's looking.
|
||||
// Paused stage does NOT update — Y-scale is frozen at the pause point.
|
||||
if (currentStage.Value != BuildStage.Constructing) return;
|
||||
|
||||
float yScale = ComputeConstructingYScale();
|
||||
ApplyYScale(yScale);
|
||||
}
|
||||
|
||||
// ----- Server API -------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Server-only: marks this visual as shelved. The visual remains in the world
|
||||
/// but no longer belongs to the owning Builder's queue. Stage stays Paused;
|
||||
/// this method just flips the IsShelved flag so the visual takes responsibility
|
||||
/// for its own grid-state cleanup on despawn.
|
||||
/// </summary>
|
||||
public void ServerMarkShelved()
|
||||
{
|
||||
if (!IsServer) return;
|
||||
isShelved.Value = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-only: marks this visual as unshelved (taken back into a Builder's queue).
|
||||
/// Called when the player right-clicks a shelved visual to resume construction.
|
||||
/// Grid-state cleanup is once again the Builder's responsibility via jobIdToVisual.
|
||||
/// </summary>
|
||||
public void ServerMarkUnshelved()
|
||||
{
|
||||
if (!IsServer) return;
|
||||
isShelved.Value = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-only: transitions the visual from Queued (or Paused) to Constructing
|
||||
/// and records the server time for stage progression. Caller is responsible
|
||||
/// for setting <paramref name="accumulatedTimeBeforeThisRun"/> from the BuildJob's
|
||||
/// AccumulatedConstructionTime — non-zero values mean this is a resume.
|
||||
/// </summary>
|
||||
public void ServerBeginConstructing(float accumulatedTimeBeforeThisRun)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
if (currentStage.Value == BuildStage.Constructing) return;
|
||||
|
||||
accumulatedConstructionTime.Value = accumulatedTimeBeforeThisRun;
|
||||
constructionStartServerTime.Value =
|
||||
(float)NetworkManager.Singleton.ServerTime.Time;
|
||||
currentStage.Value = BuildStage.Constructing;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-only: transitions Constructing → Paused AND writes the new
|
||||
/// accumulated construction time. Y-scale freezes at the level matching
|
||||
/// the accumulated time. After this returns, the visual is fully self-
|
||||
/// describing — it carries enough state to be shelved and later resumed.
|
||||
/// </summary>
|
||||
public void ServerPauseAndPersistAccumulated(float totalAccumulated)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
if (currentStage.Value != BuildStage.Constructing) return;
|
||||
|
||||
accumulatedConstructionTime.Value = totalAccumulated;
|
||||
// Reset constructionStartServerTime to -1 so any future progress reads
|
||||
// know there's no active timer running.
|
||||
constructionStartServerTime.Value = -1f;
|
||||
currentStage.Value = BuildStage.Paused;
|
||||
}
|
||||
|
||||
// ----- Visual state machine ---------------------------------------
|
||||
|
||||
private void HandleStageChanged(BuildStage previous, BuildStage current)
|
||||
{
|
||||
ApplyStageVisual(current);
|
||||
}
|
||||
|
||||
private void ApplyStageVisual(BuildStage stage)
|
||||
{
|
||||
switch (stage)
|
||||
{
|
||||
case BuildStage.Queued:
|
||||
SwapMaterial(queuedMaterial);
|
||||
ApplyYScale(queuedYScale);
|
||||
break;
|
||||
|
||||
case BuildStage.Constructing:
|
||||
SwapMaterial(constructingMaterial);
|
||||
ApplyOwnerTint();
|
||||
ApplyYScale(ComputeConstructingYScale());
|
||||
break;
|
||||
|
||||
case BuildStage.Paused:
|
||||
// Use paused material if assigned; fall back to constructing
|
||||
// material if not (still readable, just less distinct).
|
||||
SwapMaterial(pausedMaterial != null ? pausedMaterial : constructingMaterial);
|
||||
ApplyOwnerTint();
|
||||
// Freeze Y-scale at whatever the accumulated progress represents.
|
||||
// ComputeConstructingYScale uses accumulatedConstructionTime alone
|
||||
// when constructionStartServerTime is -1 (the pause sentinel).
|
||||
ApplyYScale(ComputePausedYScale());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Stage index 0..stageCount-1 based on elapsed server time PLUS any accumulated
|
||||
// time from previous Constructing runs (resume support).
|
||||
// Returned Y-scale is (stageIndex + 1) / stageCount, so stage 0 = 1/4,
|
||||
// stage 1 = 2/4, ..., stage stageCount-1 = 4/4 = full height.
|
||||
private float ComputeConstructingYScale()
|
||||
{
|
||||
float bt = buildTime.Value;
|
||||
if (bt <= 0f || stageCount <= 0) return 1f;
|
||||
|
||||
float currentRunElapsed = (float)NetworkManager.Singleton.ServerTime.Time
|
||||
- constructionStartServerTime.Value;
|
||||
float total = currentRunElapsed + accumulatedConstructionTime.Value;
|
||||
|
||||
float perStage = bt / stageCount;
|
||||
int stageIndex = Mathf.Clamp(
|
||||
Mathf.FloorToInt(total / perStage),
|
||||
0, stageCount - 1);
|
||||
|
||||
return (stageIndex + 1f) / stageCount;
|
||||
}
|
||||
|
||||
// While Paused, only the accumulated time matters (no current run is in flight).
|
||||
private float ComputePausedYScale()
|
||||
{
|
||||
float bt = buildTime.Value;
|
||||
if (bt <= 0f || stageCount <= 0) return 1f;
|
||||
|
||||
float total = accumulatedConstructionTime.Value;
|
||||
float perStage = bt / stageCount;
|
||||
int stageIndex = Mathf.Clamp(
|
||||
Mathf.FloorToInt(total / perStage),
|
||||
0, stageCount - 1);
|
||||
|
||||
return (stageIndex + 1f) / stageCount;
|
||||
}
|
||||
|
||||
private void ApplyYScale(float y)
|
||||
{
|
||||
if (scaleTarget == null) return;
|
||||
Vector3 s = scaleTarget.localScale;
|
||||
scaleTarget.localScale = new Vector3(s.x, y, s.z);
|
||||
}
|
||||
|
||||
// ----- Material handling ------------------------------------------
|
||||
|
||||
private void SwapMaterial(Material mat)
|
||||
{
|
||||
if (mat == null || tintedRenderers == null) return;
|
||||
foreach (var rend in tintedRenderers)
|
||||
{
|
||||
if (rend == null) continue;
|
||||
rend.sharedMaterial = mat;
|
||||
}
|
||||
}
|
||||
|
||||
// Reused per-instance to avoid GC. Lazy because Unity disallows
|
||||
// construction in field initializers.
|
||||
private MaterialPropertyBlock colorPropertyBlock;
|
||||
private static readonly int ColorPropertyId = Shader.PropertyToID("_Color");
|
||||
private static readonly int BaseColorPropertyId = Shader.PropertyToID("_BaseColor");
|
||||
|
||||
private void ApplyOwnerTint()
|
||||
{
|
||||
if (tintedRenderers == null) return;
|
||||
|
||||
Color c = PlayerColors.Get(ownerSlot.Value);
|
||||
c.a = 1f;
|
||||
|
||||
colorPropertyBlock ??= new MaterialPropertyBlock();
|
||||
colorPropertyBlock.SetColor(ColorPropertyId, c);
|
||||
colorPropertyBlock.SetColor(BaseColorPropertyId, c);
|
||||
|
||||
foreach (var rend in tintedRenderers)
|
||||
{
|
||||
if (rend == null) continue;
|
||||
rend.SetPropertyBlock(colorPropertyBlock);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue