using UnityEngine; using RR.Common.Interfaces; using RR.Game2D.Core; using RR.Game2D.Behaviour; using RR.Game2D.Core.Interfaces; namespace RR.Game2D.Entities.Projectiles { [DisallowMultipleComponent] [RequireComponent(typeof(Rigidbody2D), typeof(CapsuleCollider2D))] public class PickableProjectile : PhysicsProjectile, IPickable, IBouncy, IRecallable { public static PickableProjectile Instance { get; private set; } public enum State { Held, Airborne, Grounded, Returning } public State CurrentState { get; private set; } = State.Grounded; [Header("Подбор и прикрепление")] [SerializeField] private Vector3 heldLocalOffset = new(0.25f, 0.1f, 0f); [SerializeField] private float heldScale = 1.5f; [SerializeField] private float ignoreOwnerTimeOnThrow = 0.1f; [Header("Приземление")] [SerializeField] private float groundVelocity = 1f; [SerializeField] private float maxGroundSlideTime = 1f; [SerializeField] private bool lockWhenLanded = true; [Header("Отскоки")] [SerializeField] private int maxBounces = 3; [SerializeField] private float minBounceSpeed = 0.8f; [SerializeField] private float stickDelayAfterImpact = 0.03f; [Header("Возврат")] [SerializeField, Range(0, 100)] int recallScreenMarginPercent = 5; [SerializeField] private float recallArriveDistance = 1f; [SerializeField] private float recallSpeed = 16f; [SerializeField] private float recallStartDistance = 8f; [Header("Gizmos")] [SerializeField] private bool drawGizmos; [SerializeField] private Color gizmoColor = new (0.2f, 0.8f, 1f, 0.35f); private Transform holderAttachPoint; private int bounceCounter; private float lastImpactTime = -999f; private readonly Vector3 baseLocalScale = Vector3.one; private bool hadBeenHeld; private GameObject lastKnownHolder; private float lastGroundTouchTime; private GameObject jumpPad; // ------------------------------------------- // Методы Unity // #region Unity Methods //--------------------------- // Singleton //--------------------------- protected override void Awake() { rb = GetComponent(); col = GetComponent(); col.isTrigger = true; if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; jumpPad = new GameObject("Projectile Jump Pad"); jumpPad.AddComponent(); jumpPad.AddComponent(); jumpPad.GetComponent().isTrigger = true; Vector3 jumpPadScale = jumpPad.transform.localScale; jumpPadScale.y = 0.1f; jumpPad.transform.localScale = jumpPadScale; jumpPad.SetActive(false); jumpPad.GetComponent().PlayerJumped += OnPlayerJumped; } private void OnDestroy() { if (Instance == this) Instance = null; Destroy(jumpPad); } //--------------------------- // //--------------------------- private void Start() { rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous; col.isTrigger = false; if (CurrentState != State.Held) BecomeGrounded(); } protected override void FixedUpdate() { base.FixedUpdate(); if (!lockWhenLanded && CurrentState == State.Airborne) { if (rb.linearVelocity.sqrMagnitude < (groundVelocity * groundVelocity)) BecomeGrounded(); } if ((CurrentState == State.Airborne || CurrentState == State.Returning) && rb.linearVelocity.sqrMagnitude > 0.0001f) transform.up = rb.linearVelocity.normalized; if (CurrentState == State.Airborne && rb.linearVelocity.magnitude < groundVelocity) BecomeGrounded(); if (hadBeenHeld && CurrentState != State.Held) Recall(); } protected override void OnCollisionEnter2D(Collision2D collision) { var otherRb = collision.rigidbody ? collision.rigidbody : collision.otherRigidbody; var otherGO = (collision.collider ? collision.collider.attachedRigidbody?.gameObject : null) ?? otherRb?.gameObject ?? collision.gameObject; if (otherGO != null) { if (otherGO.CompareTag("Player") && CurrentState != State.Held) { if (TryPickup(otherGO)) return; } else if (otherGO.CompareTag("Enemy")) { EnemyController enemyController = otherGO.GetComponent(); if (enemyController != null) { enemyController.TakeDamage(projectileDamage); } } } if (!IsInHitLayer(collision.collider.gameObject.layer)) return; Vector2 point = collision.GetContact(0).point; OnHit(collision, point, collision.relativeVelocity); if (CurrentState == State.Airborne) { if (TryBounce(collision)) { lastImpactTime = Time.time; return; } BecomeGrounded(); } if (collision.collider.CompareTag("Ground")) lastGroundTouchTime = Time.time; } private void OnCollisionStay2D(Collision2D collision) { if (collision.collider.CompareTag("Ground")) { if (Time.time - lastGroundTouchTime > maxGroundSlideTime && CurrentState != State.Returning) BecomeGrounded(); } } protected override void OnTriggerEnter2D(Collider2D other) { if (other.CompareTag("Player")) { if (TryPickup(other.gameObject)) return; } else if (other.CompareTag("Enemy")) { other.TryGetComponent(out var controller); controller.TakeDamage(projectileDamage); } else { base.OnTriggerEnter2D(other); } } // // // ------------------------------------------- #endregion // ------------------------------------------- // Переопределенные методы родителей // #region Parent Methods protected override void OnHit(Collision2D collision, Vector2 point, Vector2 relativeVelocity) { //TryDealDamage(collision.collider, point, relativeVelocity); PlayFX(point); OnImpactFinal(collision); } protected override void OnImpactFinal(Collision2D collision) { if (!TryBounce(collision) && CurrentState == State.Airborne) BecomeGrounded(); } protected override void OnImpactFinal(Collider2D other) { if (Time.time - lastImpactTime > stickDelayAfterImpact && CurrentState == State.Airborne) BecomeGrounded(); } #endregion // // // ------------------------------------------- // ------------------------------------------- // Подбор // #region Pickup public bool TryPickup(GameObject newOwner, Transform optionalAttachPoint = null) { if (CurrentState == State.Held) return true; owner = newOwner; lastKnownHolder = newOwner; holderAttachPoint = optionalAttachPoint ?? FindAttachPoint(newOwner); var fireScript = newOwner.GetComponent(); if (!holderAttachPoint && fireScript != null) holderAttachPoint = fireScript.attachPoint; if (!holderAttachPoint) holderAttachPoint = newOwner.transform; rb.linearVelocity = Vector2.zero; rb.angularVelocity = 0f; rb.bodyType = RigidbodyType2D.Kinematic; col.enabled = false; transform.SetParent(holderAttachPoint); transform.SetPositionAndRotation(holderAttachPoint.TransformPoint(heldLocalOffset), new Quaternion(0, 90f, 0, 0)); transform.localScale = baseLocalScale * heldScale; if (fireScript != null) fireScript.BindProjectile(this); //StartCoroutine(IgnoreOwnerTemporarily(owner, ignoreOwnerTimeOnThrow)); CurrentState = State.Held; launched = false; hadBeenHeld = true; jumpPad.SetActive(false); return true; } /// /// Метод для получения точки привязки на игроке. /// /// Игрок, который держит снаряд /// Опциональная точка привязки. При передаче, снаряд будет привязан на нее /// Transform attachPoint - точка привязки private Transform FindAttachPoint(GameObject holder, Transform preferred = null) { if (preferred) return preferred; PlayerFireScript fireScript = holder.GetComponent(); if (fireScript && fireScript.attachPoint) return fireScript.attachPoint; return holder.transform; } #endregion // // // ------------------------------------------- // ------------------------------------------- // Отскок // #region Bounce public bool TryBounce(Collision2D collision) { if (Time.time - lastImpactTime < stickDelayAfterImpact) return true; if (bounceCounter > maxBounces) return false; Vector2 velocityIn = rb.linearVelocity; float speed = velocityIn.magnitude; if (speed < minBounceSpeed) return false; Vector2 direction = Vector2.Reflect(velocityIn, collision.contacts[0].normal); rb.linearVelocity = col.sharedMaterial.bounciness * direction; if (collision.collider.CompareTag("Ground") || collision.collider.CompareTag("Trap")) bounceCounter++; return true; } #endregion // // // ------------------------------------------- // ------------------------------------------- // Запуск // #region Launch public override void Launch(Vector2 direction, float speed = -1, GameObject thrower = null) { if (CurrentState == State.Held) transform.SetParent(null); owner = thrower; lastKnownHolder = thrower; holderAttachPoint = null; rb.bodyType = RigidbodyType2D.Dynamic; col.enabled = true; col.isTrigger = false; launched = true; Vector2 launchDirection = direction.sqrMagnitude > 0.0001f ? direction.normalized : Vector2.right; float finalSpeed = (speed > 0f) ? speed : initialSpeed; rb.linearVelocity = Vector2.zero; rb.angularVelocity = 0f; rb.AddForce(finalSpeed * rb.mass * launchDirection, ForceMode2D.Impulse); transform.up = launchDirection; transform.localScale = baseLocalScale; if (thrower != null && ignoreOwnerTimeOnThrow > 0f) StartCoroutine(IgnoreOwnerTemporarily(thrower, ignoreOwnerTimeOnThrow)); CurrentState = State.Airborne; bounceCounter = 0; } #endregion // // // ------------------------------------------- // ------------------------------------------- // Возвращение // #region Recall public void Recall() { GameObject playerGo = lastKnownHolder; if (playerGo == null || !playerGo.activeInHierarchy) playerGo = (GameManager.Instance && GameManager.Instance.PlayerObj) ? GameManager.Instance.PlayerObj : GameObject.FindWithTag("Player"); if (!playerGo) return; var attachPoint = FindAttachPoint(playerGo); bool isVisible; var mainCamera = Camera.main; float margin = recallScreenMarginPercent / 100f; if (mainCamera) { Vector3 viewPoint = mainCamera.WorldToViewportPoint(transform.position); isVisible = viewPoint.x >= -margin && viewPoint.x <= 1f + margin && viewPoint.y >= -margin && viewPoint.y <= 1f + margin; } else { Vector2 toOwnerTest = (Vector2)attachPoint.position - rb.position; isVisible = (toOwnerTest.magnitude <= recallStartDistance); } Vector2 toOwner = (Vector2)attachPoint.position - rb.position; float dist = toOwner.magnitude; if (CurrentState != State.Returning) { if (!isVisible) { CurrentState = State.Returning; rb.bodyType = RigidbodyType2D.Dynamic; col.isTrigger = true; } else { return; } } Vector2 dir = (dist > 0.001f) ? (toOwner / dist) : rb.linearVelocity.normalized; rb.linearVelocity = dir * recallSpeed; if (rb.linearVelocity.sqrMagnitude > 0.01f) transform.up = rb.linearVelocity.normalized; if (dist <= recallArriveDistance) TryPickup(playerGo); } #endregion // // // ------------------------------------------- // ------------------------------------------- // Собственные незаисимые методы // #region Self methods /// /// Метод для заморозки снаряда на земле. /// private void BecomeGrounded() { launched = false; CurrentState = State.Grounded; if (lockWhenLanded) { rb.linearVelocity = Vector2.zero; rb.angularVelocity = 0f; rb.bodyType = RigidbodyType2D.Kinematic; } else { rb.bodyType = RigidbodyType2D.Dynamic; } col.enabled = true; col.isTrigger = true; transform.SetParent(null, true); bounceCounter = 0; MoveJumpPad(); jumpPad.SetActive(true); } /// /// Метод для перемещения прыжковой платформы Тамагочика /// private void MoveJumpPad() { float sizeX = col.bounds.size.y; float sizeY = col.bounds.size.x; Vector3 jumpPadPos = transform.position; float jumpPadY = Mathf.Sqrt(sizeX * sizeX * Mathf.Pow(Mathf.Cos(transform.localEulerAngles.z), 2) + sizeY * sizeY * Mathf.Pow(Mathf.Sin(transform.localEulerAngles.z), 2)); jumpPadPos.y = jumpPadPos.y + 0.5f * jumpPadY; jumpPad.transform.position = jumpPadPos; } /// /// Метод для обработки событий при отпрыгивания игрока от прыжковой платформы. /// /// private void OnPlayerJumped(GameObject playerObj) { TryPickup(playerObj); } #endregion // // // ------------------------------------------- } }