using UnityEngine; [DisallowMultipleComponent] [RequireComponent(typeof(Rigidbody2D))] [RequireComponent(typeof(Collider2D))] public class PickableProjectile : BasicProjectile { public static PickableProjectile Instance { get; private set; } public enum State { Held, Airborne, Grounded, Returning } public State CurrentState { get; private set; } = State.Grounded; [Header("Pickup/Attach")] public Vector3 heldLocalOffset = new(0.25f, 0.1f, 0f); public bool snapExactlyToAttachPoint = true; public float ignoreOwnerTimeOnThrow = 0.25f; public string attachPointChildName = "HandPoint"; [Header("Physics on ground")] public float sleepVelocity = 0.05f; public bool lockWhenLanded = true; [Header("Bouncing")] public int maxBounces = 3; public float minBounceSpeed = 0.8f; [Range(0f, 1f)] public float restitution = 0.6f; [Range(0f, 1f)] public float tangentDamping = 0.85f; [Range(0f, 1f)] public float groundUpTolerance = 0.25f; public float stickDelayAfterImpact = 0.03f; [Tooltip("Старт возврата, если дальше этой дистанции от точки руки.")] public float recallStartDistance = 8f; [Tooltip("На этой дистанции от руки считаем, что прилетел и подбираем.")] public float recallArriveDistance = 1f; public float recallMaxSpeed = 16f; public float recallAcceleration = 60f; [Header("Gizmos")] public bool drawGizmos; public Color gizmoColor = new Color(0.2f, 0.8f, 1f, 0.35f); private Transform holderAttachPoint; private int bounceCounter = 0; private float lastImpactTime = -999f; private Vector3 baseLocalScale = Vector3.one; private bool hadBeenHeld = false; private GameObject lastKnownHolder; protected override void Awake() { base.Awake(); if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; } private void OnDestroy() { if (Instance == this) Instance = null; } private void Start() { rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous; col.isTrigger = false; if (CurrentState != State.Held) BecomeGrounded(null); } protected override void FixedUpdate() { base.FixedUpdate(); if (!lockWhenLanded && CurrentState == State.Airborne) { if (rb.linearVelocity.sqrMagnitude < (sleepVelocity * sleepVelocity)) BecomeGrounded(null); } // Бумеранг только если снаряд когда-то уже был в руке if (hadBeenHeld && CurrentState != State.Held) HandleRecallToPlayer(); } 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 && otherGo.CompareTag("Player") && CurrentState != State.Held) { if (TryPickup(otherGo)) return; } base.OnCollisionEnter2D(collision); if (CurrentState == State.Airborne) { if (TryBounce(collision)) { lastImpactTime = Time.time; return; } BecomeGrounded(collision); } } public bool TryPickup(GameObject newHolder, Transform optionalAttachPoint = null) { if (CurrentState == State.Held) return true; owner = newHolder; lastKnownHolder = newHolder; holderAttachPoint = optionalAttachPoint ?? FindAttachPoint(newHolder); var fireScript = newHolder.GetComponent(); if (holderAttachPoint == null && fireScript != null) holderAttachPoint = fireScript.attachPoint; if (holderAttachPoint == null) holderAttachPoint = newHolder.transform; rb.linearVelocity = Vector2.zero; rb.angularVelocity = 0f; rb.bodyType = RigidbodyType2D.Kinematic; col.enabled = false; transform.SetParent(holderAttachPoint, worldPositionStays: true); transform.position = holderAttachPoint.TransformPoint(heldLocalOffset); transform.rotation = holderAttachPoint.rotation; transform.localScale = baseLocalScale; if (fireScript != null) fireScript.BindProjectile(this); StartCoroutine(IgnoreOwnerTemporarily(owner, ignoreOwnerTimeOnThrow)); CurrentState = State.Held; launched = false; hadBeenHeld = true; return true; } private Transform ResolveAttachPoint(GameObject holder, Transform preferred = null) { if (preferred) return preferred; var fs = holder.GetComponent(); if (fs && fs.attachPoint) return fs.attachPoint; return holder.transform; } public void Launch(Vector2 direction, float speed, GameObject throwOwner = null) { if (CurrentState == State.Held) transform.SetParent(null, worldPositionStays: true); owner = throwOwner; lastKnownHolder = throwOwner; holderAttachPoint = null; rb.bodyType = RigidbodyType2D.Dynamic; col.enabled = true; launched = true; Vector2 dir = direction.sqrMagnitude > 0.0001f ? direction.normalized : Vector2.right; float finalSpeed = (speed > 0f) ? speed : initialSpeed; rb.linearVelocity = Vector2.zero; rb.angularVelocity = 0f; rb.AddForce(dir * finalSpeed * rb.mass, ForceMode2D.Impulse); transform.right = dir; transform.localScale = baseLocalScale; if (throwOwner != null && ignoreOwnerTimeOnThrow > 0f) StartCoroutine(IgnoreOwnerTemporarily(throwOwner, ignoreOwnerTimeOnThrow)); CurrentState = State.Airborne; bounceCounter = 0; } protected override void OnImpactFinal(Collision2D collision) { if (!TryBounce(collision)) BecomeGrounded(collision); } protected override void OnImpactFinal(Collider2D other) { if (Time.time - lastImpactTime > stickDelayAfterImpact) BecomeGrounded(null); } private bool TryBounce(Collision2D collision) { if (Time.time - lastImpactTime < stickDelayAfterImpact) return true; var v = rb.linearVelocity; float speed = v.magnitude; if (speed < minBounceSpeed) return false; Vector2 avgNormal = Vector2.up; if (collision.contactCount > 0) { avgNormal = Vector2.zero; for (int i = 0; i < collision.contactCount; i++) avgNormal += collision.GetContact(i).normal; avgNormal.Normalize(); } Vector2 reflected = Vector2.Reflect(v, avgNormal) * restitution; Vector2 n = avgNormal; Vector2 vn = Vector2.Dot(reflected, n) * n; Vector2 vt = reflected - vn; reflected = vn + vt * tangentDamping; rb.linearVelocity = reflected; rb.position += n * 0.005f; bounceCounter++; if (bounceCounter >= maxBounces) return false; CurrentState = State.Airborne; return true; } private void BecomeGrounded(Collision2D collision) { 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; transform.SetParent(null, true); bounceCounter = 0; } private Transform FindAttachPoint(GameObject who) { if (string.IsNullOrWhiteSpace(attachPointChildName)) return null; var t = who.transform.Find(attachPointChildName); return t; } private void HandleRecallToPlayer() { GameObject playerGo = lastKnownHolder; if (playerGo == null || !playerGo.activeInHierarchy) playerGo = (GameManager.Instance && GameManager.Instance.playerObj) ? GameManager.Instance.playerObj : GameObject.FindWithTag("Player"); if (!playerGo) return; var hand = ResolveAttachPoint(playerGo); Vector2 toHand = (Vector2)hand.position - rb.position; float dist = toHand.magnitude; if (dist > recallStartDistance) { if (CurrentState != State.Returning) { CurrentState = State.Returning; rb.bodyType = RigidbodyType2D.Dynamic; col.enabled = true; } Vector2 desired = toHand.normalized * recallMaxSpeed; rb.linearVelocity = Vector2.MoveTowards( rb.linearVelocity, desired, recallAcceleration * Time.fixedDeltaTime); if (rb.linearVelocity.sqrMagnitude > 0.01f) transform.right = rb.linearVelocity.normalized; if (dist <= recallArriveDistance) TryPickup(playerGo); } else if (CurrentState == State.Returning) { CurrentState = rb.bodyType == RigidbodyType2D.Dynamic ? State.Airborne : State.Grounded; } } #if UNITY_EDITOR private void OnDrawGizmosSelected() { if (!drawGizmos) return; Gizmos.color = gizmoColor; Gizmos.matrix = transform.localToWorldMatrix; Gizmos.DrawCube(new Vector3(heldLocalOffset.x, heldLocalOffset.y, 0), new Vector3(0.12f, 0.12f, 0.02f)); } #endif }