// Assets/_Project/Scripts/Combat/TowerLandingVisual.cs
using System.Collections;
using TD.Gameplay;
using UnityEngine;
namespace TD.Combat
{
///
/// Local-only "falls from the sky and lands" spawn visual. Subscribes to
/// and, on every client (never on a
/// dedicated server, which has no camera to show it to), animates the tower's
/// transform down from above its already-correct
/// resting position over seconds.
///
///
/// No NetworkTransform involved. Tower prefabs have no NetworkTransform —
/// position is a one-shot snapshot sent at spawn. This animation is purely local per
/// peer (same pattern as 's audio cue); each peer computes
/// an identical animation independently from the same replicated landing position and
/// the same authored /
/// values, so no networking is required.
///
/// Safe on a host despite sharing one Transform with server combat logic.
/// withholds all targeting/attack logic for
/// seconds after spawn (see its
/// landingCooldown field), so it never reads transform.position while this
/// animation is moving it. The two timers start from the same value in the same call
/// ( fires from OnNetworkSpawn), so they
/// finish within a frame of each other.
///
/// Root transform, not a child wrapper. Tower prefabs render their mesh
/// directly on the root GameObject (no separate "Model" child exists to offset
/// instead), so this animates transform.position directly. Footprint/pathing is
/// unaffected — stamps walkability from its replicated
/// anchor tile (grid data), not from transform.position.
///
[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();
}
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();
}
}
}