320 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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