towers fall from the sky - neat!

This commit is contained in:
Ian Woods 2026-07-03 14:05:08 -07:00
parent d14305d804
commit 362b76ae75
10 changed files with 396 additions and 219 deletions

View file

@ -8,15 +8,16 @@ using TD.VFX;
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.
/// Visual representation of an in-flight <see cref="BuildJob"/>: a static, non-growing
/// site marker (color-tinted by stage) plus the ground targeting reticle
/// (<see cref="TowerDropReticle"/>) that shrinks as construction progresses. One
/// NetworkObject per active job. Despawned when the job is cancelled or when the real
/// <see cref="TowerInstance"/> takes its place at construction-complete, at which point
/// <c>TowerLandingVisual</c> plays the tower's fall-from-sky arrival.
/// </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,
/// has different rendering (translucent green site marker), 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
@ -27,10 +28,9 @@ namespace TD.Gameplay
/// 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>Visual model.</b> The site marker's material swaps by stage (queued/
/// constructing/paused) but never grows or changes shape — construction progress is
/// communicated entirely by the reticle shrinking, not by the marker itself.</para>
///
/// <para><b>No grid stamping here.</b> Walkability and occupancy stamps are
/// driven by the Builder (queue-time stamps occupied=true / walkable=true,
@ -48,10 +48,6 @@ namespace TD.Gameplay
"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;
@ -63,13 +59,6 @@ namespace TD.Gameplay
"Suggested: muted/grey-tinted variant of the constructing material.")]
[SerializeField] private Material pausedMaterial;
[Header("Construction phases")]
[Tooltip("Project-default construction-stage visuals used when the tower being built " +
"doesn't define its own ConstructionPhases. When this (or the tower's set) " +
"has phases, the build site swaps between those phase prefabs as it builds. " +
"Leave empty to fall back to the legacy cube Y-scale growth below.")]
[SerializeField] private ConstructionPhaseSet defaultPhaseSet;
[Header("Drop reticle FX")]
[Tooltip("Optional targeting-reticle effect (ground decal + beam + motes) shown for the " +
"life of this build. Spawned as a child on every peer and fed construction " +
@ -77,16 +66,6 @@ namespace TD.Gameplay
"this visual at completion/cancel. Leave empty to build with no reticle.")]
[SerializeField] private TowerDropReticle dropReticlePrefab;
[Header("Construction stages (cube fallback)")]
[Tooltip("FALLBACK ONLY (no phase set assigned): number of discrete growth stages " +
"while constructing. 4 = 1/4 → 2/4 → 3/4 → 4/4 height.")]
[SerializeField] private int stageCount = 4;
[Tooltip("FALLBACK ONLY: 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 owner slot for color tinting. Mirrors TowerInstance.
@ -133,7 +112,7 @@ namespace TD.Gameplay
readPerm: NetworkVariableReadPermission.Everyone,
writePerm: NetworkVariableWritePermission.Server);
// Current stage. Drives material swap and Y-scale animation.
// Current stage. Drives the site marker's material/tint swap.
private readonly NetworkVariable<BuildStage> currentStage =
new NetworkVariable<BuildStage>(
BuildStage.Queued,
@ -244,17 +223,6 @@ namespace TD.Gameplay
private int pendingGoldSpent;
private bool hasPendingInit;
// ----- Construction-phase runtime (local, all peers) --------------
// Resolved on spawn from the tower's ConstructionPhases or the defaultPhaseSet.
// When non-null with PhaseCount > 0, the build site swaps between these
// instantiated phase prefabs instead of Y-scaling the fallback cube.
private ConstructionPhaseSet effectivePhaseSet;
private GameObject[] phaseInstances; // one instantiated child per phase
private MeshRenderer[][] phaseRenderers; // cached renderers per phase, for tinting
private int activePhaseIndex = -1;
private bool usePhases;
// Spawned child targeting-reticle effect (local, all peers). Fed progress in Update;
// destroyed automatically with this NetworkObject at completion/cancel.
private TowerDropReticle dropReticleInstance;
@ -307,10 +275,6 @@ namespace TD.Gameplay
hasPendingInit = false;
}
// Resolve and instantiate the construction-phase visuals (or fall back to the
// cube). Depends on towerTypeId, which is replicated by now on every peer.
ResolvePhaseSet();
// Spawn the targeting-reticle FX as a local child. It reads this build site's
// replicated progress (fed in Update) and dies with us at completion/cancel.
if (dropReticlePrefab != null)
@ -324,7 +288,7 @@ namespace TD.Gameplay
currentStage.OnValueChanged += HandleStageChanged;
// Apply initial visual state based on the (now-replicated) values.
ApplyStageVisual(currentStage.Value);
ApplyStageMaterialAndTint(currentStage.Value);
}
public override void OnNetworkDespawn()
@ -376,30 +340,10 @@ namespace TD.Gameplay
{
// Drive the targeting reticle on every peer, every stage: 0 while Queued
// (full-size reticle, awaiting a builder), shrinking while Constructing, frozen
// while Paused. Kept before the Constructing early-return so it also animates
// the queued/paused states.
// while Paused. This is the only per-frame visual effect on the build site —
// nothing about the site's mesh grows or changes shape during construction.
if (dropReticleInstance != null)
dropReticleInstance.SetProgress(ComputeProgressNormalized());
// While constructing, advance the visual based on server time. Runs on every
// peer so visuals stay synchronized. Paused does NOT update — the visual is
// frozen at the pause point.
if (currentStage.Value != BuildStage.Constructing) return;
if (usePhases)
{
int index = PhaseIndexFromProgress();
if (index != activePhaseIndex)
{
ShowPhase(index);
// Renderers changed with the swap — re-apply material + owner tint.
ApplyStageMaterialAndTint(BuildStage.Constructing);
}
return;
}
// Fallback: smoothly grow the cube's Y-scale through the stages.
ApplyYScale(ComputeConstructingYScale());
}
// ----- Server API -------------------------------------------------
@ -522,9 +466,9 @@ namespace TD.Gameplay
/// <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.
/// accumulated construction time (freezing the reticle's shrink progress at that
/// point). 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)
{
@ -542,37 +486,11 @@ namespace TD.Gameplay
private void HandleStageChanged(BuildStage previous, BuildStage current)
{
ApplyStageVisual(current);
ApplyStageMaterialAndTint(current);
}
private void ApplyStageVisual(BuildStage stage)
{
if (usePhases)
{
// Queued shows the first phase; constructing/paused show the phase
// matching current progress. ShowPhase repoints tintedRenderers at the
// active phase instance, so material + tint are applied AFTER it.
int index = stage == BuildStage.Queued ? 0 : PhaseIndexFromProgress();
ShowPhase(index);
ApplyStageMaterialAndTint(stage);
return;
}
// Fallback: legacy cube Y-scale growth (no phase set wired).
ApplyStageMaterialAndTint(stage);
switch (stage)
{
case BuildStage.Queued: ApplyYScale(queuedYScale); break;
case BuildStage.Constructing: ApplyYScale(ComputeConstructingYScale()); break;
// ComputePausedYScale uses accumulatedConstructionTime alone when
// constructionStartServerTime is -1 (the pause sentinel).
case BuildStage.Paused: ApplyYScale(ComputePausedYScale()); break;
}
}
// Applies the stage's material + owner tint to whatever renderers are currently
// active — the cube fallback, or the active phase instance's renderers. Paused
// falls back to the constructing material if no paused material is assigned.
// Applies the stage's material + owner tint to the site marker's renderers.
// Paused falls back to the constructing material if no paused material is assigned.
private void ApplyStageMaterialAndTint(BuildStage stage)
{
switch (stage)
@ -591,115 +509,6 @@ namespace TD.Gameplay
}
}
// ----- Construction phases ----------------------------------------
// Resolves the effective phase set (tower's own, else the project default) and
// instantiates one inactive child per phase. Sets usePhases=false (cube fallback)
// if neither set has any phases. Runs on every peer in OnNetworkSpawn.
private void ResolvePhaseSet()
{
var def = TowerPlacementManager.GetDefinition(towerTypeId.Value);
effectivePhaseSet =
(def != null && def.ConstructionPhases != null && def.ConstructionPhases.PhaseCount > 0)
? def.ConstructionPhases
: defaultPhaseSet;
int n = effectivePhaseSet != null ? effectivePhaseSet.PhaseCount : 0;
if (n <= 0)
{
usePhases = false;
return;
}
usePhases = true;
phaseInstances = new GameObject[n];
phaseRenderers = new MeshRenderer[n][];
// Hide the legacy fallback cube — the instantiated phase prefabs replace it.
if (scaleTarget != null) scaleTarget.gameObject.SetActive(false);
for (int i = 0; i < n; i++)
{
var prefab = effectivePhaseSet.GetPhase(i);
if (prefab == null) continue;
var go = Instantiate(prefab, transform);
go.transform.localPosition = Vector3.zero;
go.transform.localRotation = Quaternion.identity;
go.SetActive(false);
phaseInstances[i] = go;
phaseRenderers[i] = go.GetComponentsInChildren<MeshRenderer>(true);
}
}
// Activates only phase <paramref name="index"/> and points tintedRenderers at its
// renderers so the existing material/tint helpers drive the active phase.
private void ShowPhase(int index)
{
if (phaseInstances == null) return;
index = Mathf.Clamp(index, 0, phaseInstances.Length - 1);
for (int i = 0; i < phaseInstances.Length; i++)
if (phaseInstances[i] != null)
phaseInstances[i].SetActive(i == index);
activePhaseIndex = index;
tintedRenderers = phaseRenderers[index];
}
// Maps current normalized progress to a phase index (0..PhaseCount-1).
private int PhaseIndexFromProgress()
{
int n = effectivePhaseSet != null ? effectivePhaseSet.PhaseCount : 1;
if (n <= 1) return 0;
float p = ComputeProgressNormalized();
return Mathf.Clamp(Mathf.FloorToInt(p * n), 0, n - 1);
}
// 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)