104 lines
4.7 KiB
C#
104 lines
4.7 KiB
C#
// 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();
|
|
}
|
|
}
|
|
}
|