// 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(); } } }