using System.Threading; using Cysharp.Threading.Tasks; using DanroJump.Audio; using DanroJump.Runtime.Gameplay.Equipment; using UnityEngine; using UnityEngine.VFX; namespace DanroJump.Gameplay { /// /// Джетпак как оборудование платформы: поднимает игрока вверх и крепит visual к точке экипировки. /// [DisallowMultipleComponent] public sealed class JetpackEquipment : PlatformEquipmentBase, IPickable, ILandingEquipment { [SerializeField] [Min(0f)] private float liftSpeed = 18f; [SerializeField] [Min(0f)] private float liftHeight = 100f; [SerializeField] private string attachmentSlotId = "Jetpack"; [SerializeField] private bool replaceExistingInSlot = true; [SerializeField] private string equippedSortingLayerName = "Player"; [SerializeField] private int equippedSortingOrder = 160; [SerializeField] private int equippedEffectSortingOrder = 155; [SerializeField] private float detachWhenFallingVelocity = -0.01f; [SerializeField] [Min(0f)] private float detachHorizontalSpeed = 2.4f; [SerializeField] private float detachVerticalSpeed = 2.2f; [SerializeField] [Min(0f)] private float detachedGravityScale = 2.2f; [SerializeField] private float detachedAngularVelocity = 180f; private readonly System.Collections.Generic.List colliders = new(); private readonly System.Collections.Generic.List spriteRenderers = new(); private Rigidbody2D pickupBody; private IGameAudioService audioService; private bool isCollected; private bool isAttachedToPlayer; private CancellationTokenSource lifetimeCancellation; private CancellationTokenSource destroyWhenOutsideCameraCancellation; [Zenject.Inject] public void Construct([Zenject.InjectOptional] IGameAudioService injectedAudioService = null) { audioService = injectedAudioService; } /// /// Сбрасывает состояние подбора при повторном спавне оборудования. /// public override void ResetEquipment() { CancelLifetimeTask(); CancelDestroyWhenOutsideCameraTask(); isCollected = false; isAttachedToPlayer = false; EnsurePickupCollidersAreTriggers(); ResetPickupBody(); SetPickupPhysicsEnabled(true); CacheSpriteRenderers(); StopAttachedEffects(gameObject); } public override void HandlePlayerContact(JumpPlayerController player, Component source) { TryPickUp(player); } public bool TryActivateFromLanding(JumpPlayerController player) { if (!TryPickUp(player)) { return false; } NotifyPlatformLandingHandlers(player); return true; } /// /// Подбирает джетпак и крепит его visual к точке экипировки игрока. /// public bool TryPickUp(JumpPlayerController player) { if (isCollected || player == null || player.IgnoresEnvironmentInteractions) { return false; } audioService?.Play(GameAudioEvent.JetpackCollected, transform.position); var attachment = player.GetComponent(); if (attachment == null) { Debug.LogWarning($"{nameof(JetpackEquipment)} requires {nameof(PlayerEquipmentAttachment)} on the player.", this); isCollected = true; player.ApplyHeightLift(liftSpeed, liftHeight); player.PlayJumpMidAirAnimationIfNeeded(); MarkDetachedFromPlatform(); DestroyReplacementPickup(); return true; } if (!attachment.TryReserveSlot(attachmentSlotId)) { isCollected = true; player.ApplyHeightLift(liftSpeed, liftHeight); player.PlayJumpMidAirAnimationIfNeeded(); if (attachment.TryGetAttached(attachmentSlotId, out var attachedJetpack)) { if (attachedJetpack.TryGetComponent(out JetpackEquipment equippedJetpack)) { equippedJetpack.RestartAttachedLifecycle(player, attachment); } else { PlayAttachedEffects(attachedJetpack); } } DestroyReplacementPickup(); return true; } isCollected = true; player.ApplyHeightLift(liftSpeed, liftHeight); player.PlayJumpMidAirAnimationIfNeeded(); MarkDetachedFromPlatform(); AttachToPlayer(player, attachment); return true; } private void AttachToPlayer(JumpPlayerController player, PlayerEquipmentAttachment attachment) { if (player == null || attachment == null) { if (attachment != null) { attachment.ReleaseReservation(attachmentSlotId); } return; } SetPickupPhysicsEnabled(false); ApplyEquippedSorting(); var attachedObject = attachment.Attach(gameObject, attachmentSlotId, replaceExistingInSlot); if (attachedObject == null) { attachment.ReleaseReservation(attachmentSlotId); DestroyReplacementPickup(); return; } isAttachedToPlayer = true; attachedObject.transform.localScale = Vector3.one; player.RefreshVisualRenderers(); PlayAttachedEffects(attachedObject); StartAttachedLifetime(player, attachment); } private void RestartAttachedLifecycle(JumpPlayerController player, PlayerEquipmentAttachment attachment) { isAttachedToPlayer = true; PlayAttachedEffects(gameObject); StartAttachedLifetime(player, attachment); } private void DestroyReplacementPickup() { MarkDetachedFromPlatform(); StopAttachedEffects(gameObject); SetPickupPhysicsEnabled(false); transform.SetParent(null, true); Destroy(gameObject); } private void OnDestroy() { CancelLifetimeTask(); CancelDestroyWhenOutsideCameraTask(); StopAttachedEffects(gameObject); } private void StartAttachedLifetime(JumpPlayerController player, PlayerEquipmentAttachment attachment) { CancelLifetimeTask(); lifetimeCancellation = new CancellationTokenSource(); DetachWhenFallingAsync(player, attachment, lifetimeCancellation).Forget(Debug.LogException); } private async UniTask DetachWhenFallingAsync( JumpPlayerController player, PlayerEquipmentAttachment attachment, CancellationTokenSource cancellationSource) { try { var token = cancellationSource.Token; var hasObservedActiveLift = false; while (player != null && attachment != null && !token.IsCancellationRequested) { if (!attachment.TryGetAttached(attachmentSlotId, out var attachedObject) || attachedObject != gameObject) { return; } if (player.IsDead) { DetachFromPlayer(player, attachment); return; } if (player.IsHeightLiftActive) { hasObservedActiveLift = true; } var fallingThreshold = Mathf.Max(0f, detachWhenFallingVelocity); if (hasObservedActiveLift && player.VerticalVelocity <= fallingThreshold) { DetachFromPlayer(player, attachment); return; } await UniTask.Yield(PlayerLoopTiming.FixedUpdate, token); } } catch (System.OperationCanceledException) { } finally { if (lifetimeCancellation == cancellationSource) { lifetimeCancellation = null; cancellationSource.Dispose(); } } } private void DetachFromPlayer(JumpPlayerController player, PlayerEquipmentAttachment attachment) { if (!isAttachedToPlayer) { return; } isAttachedToPlayer = false; StopAttachedEffects(gameObject); if (attachment == null || !attachment.TryDetach(attachmentSlotId, gameObject, out _)) { transform.SetParent(null, true); } SetDetachedPhysicsMotion(player); CancelDestroyWhenOutsideCameraTask(); destroyWhenOutsideCameraCancellation = new CancellationTokenSource(); DestroyWhenOutsideCameraAsync(destroyWhenOutsideCameraCancellation).Forget(Debug.LogException); } private async UniTask DestroyWhenOutsideCameraAsync(CancellationTokenSource cancellationSource) { try { var token = cancellationSource.Token; while (!IsOutsideCamera() && !token.IsCancellationRequested) { await UniTask.Yield(PlayerLoopTiming.Update, token); } if (!token.IsCancellationRequested) { Destroy(gameObject); } } catch (System.OperationCanceledException) { } finally { if (destroyWhenOutsideCameraCancellation == cancellationSource) { destroyWhenOutsideCameraCancellation = null; cancellationSource.Dispose(); } } } private bool IsOutsideCamera() { var gameplayCamera = Camera.main; if (gameplayCamera == null) { return false; } var viewportPosition = gameplayCamera.WorldToViewportPoint(transform.position); return viewportPosition.y < 0f || viewportPosition.y > 1f || viewportPosition.x < 0f || viewportPosition.x > 1f; } private void MarkDetachedFromPlatform() { if (TryGetComponent(out PlatformSpawnedContent marker)) { marker.DetachFromPlatform(); } } private void CancelLifetimeTask() { if (lifetimeCancellation == null) { return; } var cancellation = lifetimeCancellation; lifetimeCancellation = null; cancellation.Cancel(); cancellation.Dispose(); } private void CancelDestroyWhenOutsideCameraTask() { if (destroyWhenOutsideCameraCancellation == null) { return; } var cancellation = destroyWhenOutsideCameraCancellation; destroyWhenOutsideCameraCancellation = null; cancellation.Cancel(); cancellation.Dispose(); } private void SetPickupPhysicsEnabled(bool isEnabled) { pickupBody ??= GetComponent(); if (pickupBody != null) { pickupBody.simulated = isEnabled; } SetPickupCollidersEnabled(isEnabled); } private void ResetPickupBody() { pickupBody ??= GetComponent(); if (pickupBody == null) { return; } pickupBody.bodyType = RigidbodyType2D.Kinematic; pickupBody.gravityScale = 0f; pickupBody.linearVelocity = Vector2.zero; pickupBody.angularVelocity = 0f; } private void SetDetachedPhysicsMotion(JumpPlayerController player) { SetPickupCollidersEnabled(false); pickupBody ??= GetComponent(); if (pickupBody == null) { return; } var horizontalDirection = -1f; if (player != null) { var deltaX = transform.position.x - player.transform.position.x; if (Mathf.Abs(deltaX) > 0.01f) { horizontalDirection = Mathf.Sign(deltaX); } } pickupBody.bodyType = RigidbodyType2D.Dynamic; pickupBody.gravityScale = detachedGravityScale; pickupBody.simulated = true; pickupBody.linearVelocity = new Vector2(horizontalDirection * detachHorizontalSpeed, detachVerticalSpeed); pickupBody.angularVelocity = horizontalDirection * detachedAngularVelocity; } private void SetPickupCollidersEnabled(bool isEnabled) { GetComponentsInChildren(true, colliders); for (var index = 0; index < colliders.Count; index++) { var pickupCollider = colliders[index]; if (pickupCollider != null) { pickupCollider.compositeOperation = Collider2D.CompositeOperation.None; pickupCollider.enabled = isEnabled; } } colliders.Clear(); } private void EnsurePickupCollidersAreTriggers() { GetComponentsInChildren(true, colliders); for (var index = 0; index < colliders.Count; index++) { var pickupCollider = colliders[index]; if (pickupCollider != null) { pickupCollider.isTrigger = true; pickupCollider.compositeOperation = Collider2D.CompositeOperation.None; } } colliders.Clear(); } private void CacheSpriteRenderers() { spriteRenderers.Clear(); GetComponentsInChildren(true, spriteRenderers); } private void ApplyEquippedSorting() { CacheSpriteRenderers(); for (var index = 0; index < spriteRenderers.Count; index++) { var spriteRenderer = spriteRenderers[index]; if (spriteRenderer == null) { continue; } spriteRenderer.sortingLayerName = equippedSortingLayerName; spriteRenderer.sortingOrder = equippedSortingOrder; } var vfxRenderers = GetComponentsInChildren(true); for (var index = 0; index < vfxRenderers.Length; index++) { var vfxRenderer = vfxRenderers[index]; if (vfxRenderer == null) { continue; } vfxRenderer.sortingLayerName = equippedSortingLayerName; vfxRenderer.sortingOrder = equippedEffectSortingOrder; } } private void PlayAttachedEffects(GameObject attachedObject) { if (attachedObject == null) { return; } var effects = attachedObject.GetComponentsInChildren(true); for (var index = 0; index < effects.Length; index++) { var effect = effects[index]; if (effect == null) { continue; } effect.gameObject.SetActive(true); if (effect.TryGetComponent(out var settings)) { settings.Play(equippedSortingLayerName, equippedEffectSortingOrder); continue; } effect.enabled = true; if (effect.TryGetComponent(out var vfxRenderer)) { vfxRenderer.enabled = true; vfxRenderer.sortingLayerName = equippedSortingLayerName; vfxRenderer.sortingOrder = equippedEffectSortingOrder; } effect.Reinit(); effect.Play(); } } private static void StopAttachedEffects(GameObject attachedObject) { if (attachedObject == null) { return; } var effects = attachedObject.GetComponentsInChildren(true); for (var index = 0; index < effects.Length; index++) { var effect = effects[index]; if (effect == null) { continue; } if (effect.TryGetComponent(out var settings)) { settings.Stop(); } else { effect.Stop(); effect.enabled = false; } } } } }