towers fall from the sky - neat!
This commit is contained in:
parent
d14305d804
commit
362b76ae75
10 changed files with 396 additions and 219 deletions
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue