towers fall from the sky - neat!
This commit is contained in:
parent
d14305d804
commit
362b76ae75
10 changed files with 396 additions and 219 deletions
|
|
@ -70,6 +70,8 @@ GameObject:
|
|||
- component: {fileID: 805962841523123163}
|
||||
- component: {fileID: 8853488620519990682}
|
||||
- component: {fileID: 5360241685303413177}
|
||||
- component: {fileID: 5793816278935258120}
|
||||
- component: {fileID: 3412005213656160288}
|
||||
m_Layer: 0
|
||||
m_Name: Tower_BasicArrow
|
||||
m_TagString: Untagged
|
||||
|
|
@ -212,6 +214,8 @@ MonoBehaviour:
|
|||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
tintedRenderers:
|
||||
- {fileID: 4028055828417179692}
|
||||
dropHeight: 15
|
||||
landingDuration: 0.4
|
||||
--- !u!114 &805962841523123163
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -252,12 +256,67 @@ MonoBehaviour:
|
|||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 93bc533262394e1e8b122bbfad89391a, type: 3}
|
||||
m_Name:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Visuals.ConeMesh
|
||||
sides: 4
|
||||
radius: 0.7071068
|
||||
height: 1.5
|
||||
angularOffsetDegrees: 45
|
||||
--- !u!114 &5793816278935258120
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6482414459531823157}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 22d67b2aa780d786a93acfe06d1909b0, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Combat.TowerLandedSound
|
||||
landedSound:
|
||||
clip: {fileID: 0}
|
||||
volume: 0
|
||||
minPitch: 0
|
||||
maxPitch: 0
|
||||
--- !u!114 &3412005213656160288
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6482414459531823157}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bf337521317d9e297a652e97c666376f, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Combat.TowerLandingVisual
|
||||
fallCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
impactVfxPrefab: {fileID: 0}
|
||||
impactVfxLifetime: 3
|
||||
--- !u!1 &7580197837852108944
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@ GameObject:
|
|||
- component: {fileID: 9137031893466587143}
|
||||
- component: {fileID: 805962841523123163}
|
||||
- component: {fileID: 8853488620519990682}
|
||||
- component: {fileID: 3021373172845454683}
|
||||
- component: {fileID: 4003901452716391977}
|
||||
m_Layer: 0
|
||||
m_Name: Tower_SiegeCannon
|
||||
m_TagString: Untagged
|
||||
|
|
@ -181,9 +183,9 @@ MonoBehaviour:
|
|||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
|
||||
m_Name:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
|
||||
GlobalObjectIdHash: 3344551122
|
||||
GlobalObjectIdHash: 2153184693
|
||||
InScenePlacedSourceGlobalObjectIdHash: 0
|
||||
DeferredDespawnTick: 0
|
||||
Ownership: 1
|
||||
|
|
@ -206,11 +208,13 @@ MonoBehaviour:
|
|||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fb111fc88b3d6a340a3abde5a1502af3, type: 3}
|
||||
m_Name:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.TowerInstance
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
tintedRenderers:
|
||||
- {fileID: 4028055828417179692}
|
||||
dropHeight: 15
|
||||
landingDuration: 0.4
|
||||
--- !u!114 &805962841523123163
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -221,7 +225,7 @@ MonoBehaviour:
|
|||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 7eb6cce6cd96b23478b7d2173cebf74d, type: 3}
|
||||
m_Name:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Combat.TowerCombat
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
enemyLayerMask:
|
||||
|
|
@ -237,10 +241,65 @@ MonoBehaviour:
|
|||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: c0edb0c5206ca454bbd7c300c6cc7574, type: 3}
|
||||
m_Name:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Combat.TowerRangeIndicator
|
||||
rangeProjector: {fileID: 8255517343120954594}
|
||||
projectionDepth: 50
|
||||
--- !u!114 &3021373172845454683
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6482414459531823157}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 22d67b2aa780d786a93acfe06d1909b0, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Combat.TowerLandedSound
|
||||
landedSound:
|
||||
clip: {fileID: 0}
|
||||
volume: 0
|
||||
minPitch: 0
|
||||
maxPitch: 0
|
||||
--- !u!114 &4003901452716391977
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6482414459531823157}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bf337521317d9e297a652e97c666376f, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Combat.TowerLandingVisual
|
||||
fallCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
impactVfxPrefab: {fileID: 0}
|
||||
impactVfxLifetime: 3
|
||||
--- !u!1 &7580197837852108944
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -283,7 +342,7 @@ MonoBehaviour:
|
|||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 0777d029ed3dffa4692f417d4aba19ca, type: 3}
|
||||
m_Name:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.DecalProjector
|
||||
m_Material: {fileID: 2100000, guid: f99227cbde481ce47a2527e6bca709d2, type: 2}
|
||||
m_DrawDistance: 1000
|
||||
|
|
|
|||
|
|
@ -101,6 +101,8 @@ GameObject:
|
|||
- component: {fileID: 4524938955752569428}
|
||||
- component: {fileID: -2590971686077483651}
|
||||
- component: {fileID: 1288485127287735786}
|
||||
- component: {fileID: 1829137298732744432}
|
||||
- component: {fileID: 8125712573731754244}
|
||||
m_Layer: 0
|
||||
m_Name: Tower_TeslaCoil
|
||||
m_TagString: Untagged
|
||||
|
|
@ -147,7 +149,7 @@ MonoBehaviour:
|
|||
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
|
||||
GlobalObjectIdHash: 280718094
|
||||
GlobalObjectIdHash: 1363936954
|
||||
InScenePlacedSourceGlobalObjectIdHash: 0
|
||||
DeferredDespawnTick: 0
|
||||
Ownership: 1
|
||||
|
|
@ -175,6 +177,8 @@ MonoBehaviour:
|
|||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
tintedRenderers:
|
||||
- {fileID: 0}
|
||||
dropHeight: 15
|
||||
landingDuration: 0.4
|
||||
--- !u!114 &805962841523123163
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -268,6 +272,61 @@ MonoBehaviour:
|
|||
volume: 0.496
|
||||
minPitch: 1
|
||||
maxPitch: 1
|
||||
--- !u!114 &1829137298732744432
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6482414459531823157}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 22d67b2aa780d786a93acfe06d1909b0, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Combat.TowerLandedSound
|
||||
landedSound:
|
||||
clip: {fileID: 0}
|
||||
volume: 0
|
||||
minPitch: 0
|
||||
maxPitch: 0
|
||||
--- !u!114 &8125712573731754244
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6482414459531823157}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bf337521317d9e297a652e97c666376f, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Combat.TowerLandingVisual
|
||||
fallCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
impactVfxPrefab: {fileID: 0}
|
||||
impactVfxLifetime: 3
|
||||
--- !u!1 &7101790219931971190
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
|
|||
|
|
@ -58,6 +58,12 @@ namespace TD.Combat
|
|||
// is in range, the tower fires and the timer resets to 1 / AttacksPerSecond.
|
||||
private float attackCooldown;
|
||||
|
||||
// Counts down from towerInstance.LandingDuration, set in OnNetworkSpawn. Combat is
|
||||
// fully inert until it reaches zero, so a tower can't target/fire while its mesh is
|
||||
// still visually falling (TowerLandingVisual). Server-only; independent of whatever
|
||||
// the client-side fall visual is doing to the (possibly shared, on a host) transform.
|
||||
private float landingCooldown;
|
||||
|
||||
// Cached on OnNetworkSpawn — avoids GetComponent every Update.
|
||||
private TowerInstance towerInstance;
|
||||
|
||||
|
|
@ -102,6 +108,7 @@ namespace TD.Combat
|
|||
public override void OnNetworkSpawn()
|
||||
{
|
||||
towerInstance = GetComponent<TowerInstance>();
|
||||
landingCooldown = towerInstance.LandingDuration;
|
||||
replicatedTarget.OnValueChanged += HandleReplicatedTargetChanged;
|
||||
|
||||
// Late-joining clients receive the NV's current value in the initial
|
||||
|
|
@ -128,6 +135,12 @@ namespace TD.Combat
|
|||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
if (landingCooldown > 0f)
|
||||
{
|
||||
landingCooldown -= Time.deltaTime;
|
||||
return;
|
||||
}
|
||||
|
||||
TowerDefinition def = towerInstance?.Definition;
|
||||
if (def == null || def.Range <= 0f || def.AttacksPerSecond <= 0f) return;
|
||||
|
||||
|
|
|
|||
41
Assets/_Project/Scripts/Combat/TowerLandedSound.cs
Normal file
41
Assets/_Project/Scripts/Combat/TowerLandedSound.cs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Assets/_Project/Scripts/Combat/TowerLandedSound.cs
|
||||
using TD.Audio;
|
||||
using TD.Gameplay;
|
||||
using UnityEngine;
|
||||
|
||||
namespace TD.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// Local-only audio for the tower's landing impact. Subscribes to
|
||||
/// <see cref="TowerInstance.OnLandedClient"/> and plays a sound on every peer when the
|
||||
/// tower's fall animation (<see cref="TowerLandingVisual"/>) finishes.
|
||||
/// Attach to any tower prefab and assign its clip in the Inspector.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(TowerInstance))]
|
||||
public class TowerLandedSound : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private SoundConfig landedSound;
|
||||
|
||||
private TowerInstance towerInstance;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
towerInstance = GetComponent<TowerInstance>();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (towerInstance != null) towerInstance.OnLandedClient += HandleLanded;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (towerInstance != null) towerInstance.OnLandedClient -= HandleLanded;
|
||||
}
|
||||
|
||||
private void HandleLanded()
|
||||
{
|
||||
AudioManager.Instance?.Play(landedSound.clip, AudioCategory.UI, landedSound.RandomPitch(), landedSound.volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Project/Scripts/Combat/TowerLandedSound.cs.meta
Normal file
2
Assets/_Project/Scripts/Combat/TowerLandedSound.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 22d67b2aa780d786a93acfe06d1909b0
|
||||
104
Assets/_Project/Scripts/Combat/TowerLandingVisual.cs
Normal file
104
Assets/_Project/Scripts/Combat/TowerLandingVisual.cs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// Assets/_Project/Scripts/Combat/TowerLandingVisual.cs
|
||||
using System.Collections;
|
||||
using TD.Gameplay;
|
||||
using UnityEngine;
|
||||
|
||||
namespace TD.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// Local-only "falls from the sky and lands" spawn visual. Subscribes to
|
||||
/// <see cref="TowerInstance.OnBuiltClient"/> and, on every client (never on a
|
||||
/// dedicated server, which has no camera to show it to), animates the tower's
|
||||
/// transform down from <see cref="TowerInstance.DropHeight"/> above its already-correct
|
||||
/// resting position over <see cref="TowerInstance.LandingDuration"/> seconds.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>No NetworkTransform involved.</b> Tower prefabs have no NetworkTransform —
|
||||
/// position is a one-shot snapshot sent at spawn. This animation is purely local per
|
||||
/// peer (same pattern as <see cref="TowerBuiltSound"/>'s audio cue); each peer computes
|
||||
/// an identical animation independently from the same replicated landing position and
|
||||
/// the same authored <see cref="TowerInstance.DropHeight"/>/<see cref="TowerInstance.LandingDuration"/>
|
||||
/// values, so no networking is required.</para>
|
||||
///
|
||||
/// <para><b>Safe on a host despite sharing one Transform with server combat logic.</b>
|
||||
/// <see cref="TowerCombat"/> withholds all targeting/attack logic for
|
||||
/// <see cref="TowerInstance.LandingDuration"/> seconds after spawn (see its
|
||||
/// <c>landingCooldown</c> field), so it never reads <c>transform.position</c> while this
|
||||
/// animation is moving it. The two timers start from the same value in the same call
|
||||
/// (<see cref="TowerInstance.OnBuiltClient"/> fires from <c>OnNetworkSpawn</c>), so they
|
||||
/// finish within a frame of each other.</para>
|
||||
///
|
||||
/// <para><b>Root transform, not a child wrapper.</b> Tower prefabs render their mesh
|
||||
/// directly on the root GameObject (no separate "Model" child exists to offset
|
||||
/// instead), so this animates <c>transform.position</c> directly. Footprint/pathing is
|
||||
/// unaffected — <see cref="TowerInstance"/> stamps walkability from its replicated
|
||||
/// anchor tile (grid data), not from <c>transform.position</c>.</para>
|
||||
/// </remarks>
|
||||
[RequireComponent(typeof(TowerInstance))]
|
||||
public class TowerLandingVisual : MonoBehaviour
|
||||
{
|
||||
[Tooltip("Eases the fall over LandingDuration. X = normalized time, Y = normalized " +
|
||||
"fraction of DropHeight already descended (0 = still at drop height, 1 = landed).")]
|
||||
[SerializeField] private AnimationCurve fallCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
|
||||
|
||||
[Tooltip("Optional impact VFX (dust burst, etc.) instantiated at the landing point when " +
|
||||
"the fall finishes. Leave empty for no VFX.")]
|
||||
[SerializeField] private GameObject impactVfxPrefab;
|
||||
|
||||
[Tooltip("Seconds before the instantiated impact VFX is destroyed. Ignored if " +
|
||||
"impactVfxPrefab is unassigned.")]
|
||||
[SerializeField] private float impactVfxLifetime = 3f;
|
||||
|
||||
private TowerInstance towerInstance;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
towerInstance = GetComponent<TowerInstance>();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (towerInstance != null) towerInstance.OnBuiltClient += HandleBuilt;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (towerInstance != null) towerInstance.OnBuiltClient -= HandleBuilt;
|
||||
}
|
||||
|
||||
private void HandleBuilt()
|
||||
{
|
||||
// Dedicated (non-host) servers have no camera/visuals and must never move the
|
||||
// shared transform. TowerCombat's landing gate is fully independent of this
|
||||
// animation, so skipping it here on a pure server is a harmless no-op.
|
||||
if (!towerInstance.IsClient) return;
|
||||
|
||||
StartCoroutine(AnimateDrop());
|
||||
}
|
||||
|
||||
private IEnumerator AnimateDrop()
|
||||
{
|
||||
Vector3 landedPosition = transform.position; // already correct (SeatOnGround, pre-spawn)
|
||||
float dropHeight = towerInstance.DropHeight;
|
||||
float duration = towerInstance.LandingDuration;
|
||||
|
||||
transform.position = landedPosition + Vector3.up * dropHeight;
|
||||
|
||||
float t = 0f;
|
||||
while (t < duration)
|
||||
{
|
||||
t += Time.deltaTime;
|
||||
float u = fallCurve.Evaluate(Mathf.Clamp01(t / duration));
|
||||
transform.position = landedPosition + Vector3.up * dropHeight * (1f - u);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
transform.position = landedPosition; // snap exactly, avoid float drift
|
||||
|
||||
if (impactVfxPrefab != null)
|
||||
Destroy(Instantiate(impactVfxPrefab, landedPosition, Quaternion.identity), impactVfxLifetime);
|
||||
|
||||
towerInstance.ClientNotifyLanded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bf337521317d9e297a652e97c666376f
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -55,6 +55,18 @@ namespace TD.Gameplay
|
|||
"to exclude specific children.")]
|
||||
[SerializeField] private MeshRenderer[] tintedRenderers;
|
||||
|
||||
[Header("Landing (post-construction drop)")]
|
||||
[Tooltip("World-unit height the tower visually drops from when construction completes. " +
|
||||
"Consumed only by the client-side fall visual (TowerLandingVisual).")]
|
||||
[SerializeField] private float dropHeight = 15f;
|
||||
|
||||
[Tooltip("Seconds the fall-and-land animation takes. Single source of truth: " +
|
||||
"TowerCombat withholds targeting/attacks for exactly this long " +
|
||||
"(server-authoritative), and TowerLandingVisual uses the same value to time " +
|
||||
"its client-side animation, so the two stay in lockstep without duplicating " +
|
||||
"the number.")]
|
||||
[SerializeField] private float landingDuration = 0.4f;
|
||||
|
||||
// ----- Networked state ------------------------------------------------
|
||||
|
||||
// The TowerTypeId (index into TowerPlacementManager's catalog) for this tower.
|
||||
|
|
@ -132,6 +144,13 @@ namespace TD.Gameplay
|
|||
/// <summary>The footprint anchor tile (SW corner, world-tile coords).</summary>
|
||||
public Vector2Int AnchorTile => anchorTile.Value;
|
||||
|
||||
/// <summary>World-unit height the post-construction drop animation falls from.</summary>
|
||||
public float DropHeight => dropHeight;
|
||||
|
||||
/// <summary>Seconds the drop/land animation takes. Also how long TowerCombat withholds
|
||||
/// targeting/attacks after construction completes.</summary>
|
||||
public float LandingDuration => landingDuration;
|
||||
|
||||
// ----- ISelectable ----------------------------------------------------
|
||||
|
||||
// Absolute world-unit margin that the selection ring extends beyond the
|
||||
|
|
@ -171,6 +190,16 @@ namespace TD.Gameplay
|
|||
/// <summary>Fired on ALL peers when this tower finishes construction and spawns.</summary>
|
||||
public event System.Action OnBuiltClient;
|
||||
|
||||
/// <summary>Fired locally on this peer when the client-side fall/land animation
|
||||
/// finishes (see <see cref="TD.Combat.TowerLandingVisual"/>). Analogous to
|
||||
/// <see cref="OnBuiltClient"/>, but delayed by <see cref="LandingDuration"/>.</summary>
|
||||
public event System.Action OnLandedClient;
|
||||
|
||||
/// <summary>Called by <see cref="TD.Combat.TowerLandingVisual"/> when its local
|
||||
/// fall/land animation finishes. Purely local — never invoked on a dedicated
|
||||
/// (non-client) server, which never runs that animation.</summary>
|
||||
public void ClientNotifyLanded() => OnLandedClient?.Invoke();
|
||||
|
||||
// ----- Server-only initialization -------------------------------------
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue