using UnityEngine; [DisallowMultipleComponent] [RequireComponent(typeof(Rigidbody2D))] [RequireComponent(typeof(Collider2D))] public class PickableProjectileTemp : BasicProjectile { // --- Singleton --- public static PickableProjectileTemp 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 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; [Header("Recall")] public float recallStartDistance = 8f; 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 bool hadBeenHeld = false; private GameObject lastKnownHolder; // локальный «таймер» игнора владельца после броска, // чтобы не подбирать мгновенно владельцем при столкновении private float ignoreOwnerUntil = -999f; 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); } public bool TryPickup(GameObject newHolder, Transform optionalAttachPoint = null) { if (CurrentState == State.Held) return true; owner = newHolder; lastKnownHolder = newHolder; holderAttachPoint = optionalAttachPoint ?? FindAttachPointOn(owner); if (holderAttachPoint == null) holderAttachPoint = owner.transform; rb.linearVelocity = Vector2.zero; rb.angularVelocity = 0f; rb.bodyType = RigidbodyType2D.Kinematic; col.enabled = false; transform.SetParent(holderAttachPoint, worldPositionStays: false); transform.localPosition = heldLocalOffset; transform.localRotation = Quaternion.identity; transform.localScale = Vector3.one * 2; // чуть-чуть игнорим владельца, чтобы не словить повторные коллизии ignoreOwnerUntil = Time.time + ignoreOwnerTimeOnThrow; StartCoroutine(IgnoreOwnerTemporarily(owner, ignoreOwnerTimeOnThrow)); CurrentState = State.Held; launched = false; hadBeenHeld = true; // передадим ссылку игроку (если у него есть PlayerFireScript), // чтобы он знал «свой» снаряд var pfs = owner.GetComponent(); //if (pfs) pfs.BindProjectile(this); return true; } public void Launch(Vector2 direction, float speed, GameObject throwOwner = null) { if (CurrentState == State.Held) transform.SetParent(null, 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; if (throwOwner != null && ignoreOwnerTimeOnThrow > 0f) { ignoreOwnerUntil = Time.time + ignoreOwnerTimeOnThrow; StartCoroutine(IgnoreOwnerTemporarily(throwOwner, ignoreOwnerTimeOnThrow)); } CurrentState = State.Airborne; bounceCounter = 0; } protected override void OnCollisionEnter2D(Collision2D collision) { // 1) если врезались в игрока — пробуем подобрать до логики рикошета 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 (otherGo != owner || Time.time >= ignoreOwnerUntil) { var hand = FindAttachPointOn(otherGo) ?? otherGo.transform; if (TryPickup(otherGo, hand)) return; // успешно подобрали — выходим } } // 2) обычная логика попадания/рикошета base.OnCollisionEnter2D(collision); if (CurrentState == State.Airborne) { if (TryBounce(collision)) { lastImpactTime = Time.time; return; } BecomeGrounded(collision); } } 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 FindAttachPointOn(GameObject who) { if (string.IsNullOrWhiteSpace(attachPointChildName)) return null; return who.transform.Find(attachPointChildName); } 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(); } private void HandleRecallToPlayer() { GameObject playerGo = lastKnownHolder; if (playerGo == null || !playerGo.activeInHierarchy) playerGo = GameObject.FindWithTag("Player"); if (!playerGo) return; var hand = FindAttachPointOn(playerGo) ?? playerGo.transform; 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, hand); } 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 }