699 lines
26 KiB
C#
699 lines
26 KiB
C#
using UnityEngine;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using DG.Tweening;
|
||
|
||
[RequireComponent(typeof(Collider))]
|
||
public class CubeClash_BirdSwoop : MonoBehaviour
|
||
{
|
||
public static int ActiveCount = 0;
|
||
|
||
[Header("Targeting")]
|
||
public bool useStaticList = true;
|
||
public string zibuTag = "Zibu";
|
||
public bool autoDestroyIfNoTarget = true;
|
||
|
||
[Header("Bird Movement (Seek)")]
|
||
public float birdMoveSpeed = 8f;
|
||
public float birdTurnSpeed = 10f;
|
||
public float pickupRadius = 1.2f; // measured vs target collider surface
|
||
public float approachHeightOffset = 2f;
|
||
public float followAboveOffset = 1.0f;
|
||
public bool faceVelocity = true;
|
||
|
||
[Header("Lift / Carry Profile")]
|
||
public float carryHeight = 5f;
|
||
public float riseTime = 0.8f;
|
||
public float hoverTime = 0.4f;
|
||
|
||
[Tooltip("Time the Zibu is carried horizontally to the air offset before dropping.")]
|
||
public float travelToAirOffsetTime = 0.6f;
|
||
|
||
[Header("Drop / Physics")]
|
||
public float dropKickImpulse = 0f;
|
||
public bool disableGravityWhileCarried = true;
|
||
public float extraDrag = 2f;
|
||
public float extraStun = 0.1f;
|
||
public float posGain = 12f;
|
||
public float velGain = 2.5f;
|
||
public float maxDeltaVPerFixedUpdate = 12f;
|
||
|
||
[Header("Pickup Assist / Anti-Stuck")]
|
||
public float freezeAIWhenWithin = 1.8f;
|
||
public float pickupSkin = 0.3f;
|
||
public float forcePickupAfter = 0.6f;
|
||
public float nearRangeFactor = 2.0f;
|
||
public float minApproachHeight = 0.15f;
|
||
|
||
[Header("Random Air Offset (world XZ)")]
|
||
public Vector2 airOffsetX = new Vector2(0f, 1f);
|
||
public Vector2 airOffsetZ = new Vector2(0f, 1f);
|
||
|
||
[Header("Safe Drop (ground, overlap avoidance)")]
|
||
[Tooltip("If > 0, overrides body radius for overlap tests; else derived from collider bounds.")]
|
||
public float zibuBodyRadiusOverride = 0f;
|
||
public float inflateClearance = 0.1f;
|
||
public int maxDropProbeSpots = 16;
|
||
public float probeStepDistance = 0.45f;
|
||
[Tooltip("Layers considered as ground for raycast placement.")]
|
||
public LayerMask groundMask = ~0; // set to your ground/floor layers
|
||
[Tooltip("Layers used to detect other Zibus (set to the Zibu layer).")]
|
||
public LayerMask zibuMask = ~0;
|
||
|
||
[Header("Post-Drop Unstack (layer-based)")]
|
||
[Tooltip("How long after drop we keep checking if the Zibu landed on another Zibu.")]
|
||
public float unstackCheckSeconds = 1.0f;
|
||
[Tooltip("How often to check (seconds).")]
|
||
public float unstackCheckInterval = 0.08f;
|
||
[Tooltip("SphereCast radius used to detect what's directly below the dropped Zibu.")]
|
||
public float unstackProbeRadius = 0.25f;
|
||
[Tooltip("Max downward probe distance from about waist height.")]
|
||
public float unstackProbeDown = 1.5f;
|
||
[Tooltip("Sideways impulse when we detect Zibu-under-Zibu.")]
|
||
public float unstackLateralImpulse = 3.0f;
|
||
[Tooltip("Optional downward impulse to help it slip off.")]
|
||
public float unstackDownImpulse = 0.0f;
|
||
[Tooltip("If still stuck, teleport this many meters sideways to nearest clear ground.")]
|
||
public float unstackTeleportStep = 0.5f;
|
||
[Tooltip("Enable the teleport fallback if impulses didn't clear it.")]
|
||
public bool unstackUseTeleportFallback = true;
|
||
|
||
[Header("Lifecycle")]
|
||
public bool destroyOnRelease = true;
|
||
|
||
private enum SwoopState { Seeking, Carrying, Releasing, Idle }
|
||
private SwoopState _state = SwoopState.Idle;
|
||
|
||
private Rigidbody _targetRb;
|
||
private Collider _targetCol;
|
||
private CubeClash_ZibuController _targetController;
|
||
private CubeClash_ZibuAI _targetAI;
|
||
|
||
private Rigidbody _birdRb;
|
||
private bool _weMadeKinematic = false;
|
||
|
||
// token bookkeeping for AI hold
|
||
private bool _aiHoldApplied = false;
|
||
|
||
// Carry bookkeeping
|
||
private readonly HashSet<Rigidbody> _carried = new HashSet<Rigidbody>();
|
||
private readonly Dictionary<Rigidbody, float> _origDrag = new Dictionary<Rigidbody, float>();
|
||
private readonly Dictionary<Rigidbody, bool> _origGrav = new Dictionary<Rigidbody, bool>();
|
||
|
||
// anti-stuck timer
|
||
private float _nearTimer = 0f;
|
||
|
||
void Reset()
|
||
{
|
||
var col = GetComponent<Collider>();
|
||
if (col) col.isTrigger = true;
|
||
}
|
||
|
||
void Awake()
|
||
{
|
||
_birdRb = GetComponent<Rigidbody>();
|
||
if (_birdRb != null)
|
||
{
|
||
if (!_birdRb.isKinematic)
|
||
{
|
||
_birdRb.isKinematic = true;
|
||
_weMadeKinematic = true;
|
||
}
|
||
_birdRb.interpolation = RigidbodyInterpolation.Interpolate;
|
||
}
|
||
}
|
||
|
||
void OnEnable()
|
||
{
|
||
ActiveCount++;
|
||
PickRandomZibuAndSeek();
|
||
Invoke(nameof(Destroyer), 10);
|
||
}
|
||
void Destroyer()
|
||
{
|
||
Destroy(gameObject);
|
||
}
|
||
void OnDisable()
|
||
{
|
||
// Restore anyone we were carrying or had held
|
||
foreach (var rb in _carried)
|
||
{
|
||
if (!rb) continue;
|
||
if (_origGrav.TryGetValue(rb, out bool g)) rb.useGravity = g;
|
||
if (_origDrag.TryGetValue(rb, out float d)) rb.drag = d;
|
||
|
||
var ai = rb.GetComponent<CubeClash_ZibuAI>();
|
||
if (ai) ai.EndPickupHold(this);
|
||
}
|
||
_carried.Clear();
|
||
_origDrag.Clear();
|
||
_origGrav.Clear();
|
||
|
||
if (_targetAI && _aiHoldApplied)
|
||
{
|
||
_targetAI.EndPickupHold(this);
|
||
_aiHoldApplied = false;
|
||
}
|
||
|
||
ActiveCount = Mathf.Max(0, ActiveCount - 1);
|
||
}
|
||
|
||
void OnDestroy()
|
||
{
|
||
ActiveCount = Mathf.Max(0, ActiveCount - 1);
|
||
}
|
||
|
||
private void PickRandomZibuAndSeek()
|
||
{
|
||
_targetRb = PickRandomZibuRigidbody();
|
||
if (_targetRb == null)
|
||
{
|
||
if (autoDestroyIfNoTarget) Destroy(gameObject);
|
||
return;
|
||
}
|
||
|
||
_targetCol = _targetRb.GetComponent<Collider>();
|
||
_targetController = _targetRb.GetComponent<CubeClash_ZibuController>();
|
||
_targetAI = _targetRb.GetComponent<CubeClash_ZibuAI>();
|
||
_state = SwoopState.Seeking;
|
||
_nearTimer = 0f;
|
||
_aiHoldApplied = false;
|
||
}
|
||
|
||
private Rigidbody PickRandomZibuRigidbody()
|
||
{
|
||
List<Rigidbody> candidates = new List<Rigidbody>();
|
||
|
||
if (useStaticList && CubeClash_ZibuController.AllZibus != null && CubeClash_ZibuController.AllZibus.Count > 0)
|
||
{
|
||
foreach (var z in CubeClash_ZibuController.AllZibus)
|
||
{
|
||
if (!z || !z.gameObject.activeInHierarchy) continue;
|
||
var rb = z.GetComponent<Rigidbody>();
|
||
if (rb) candidates.Add(rb);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
foreach (var go in GameObject.FindGameObjectsWithTag(zibuTag))
|
||
{
|
||
if (!go || !go.activeInHierarchy) continue;
|
||
var rb = go.GetComponent<Rigidbody>();
|
||
if (rb) candidates.Add(rb);
|
||
}
|
||
}
|
||
|
||
if (candidates.Count == 0) return null;
|
||
return candidates[Random.Range(0, candidates.Count)];
|
||
}
|
||
|
||
void FixedUpdate()
|
||
{
|
||
if (_targetRb == null) return;
|
||
|
||
switch (_state)
|
||
{
|
||
case SwoopState.Seeking: SeekStep(); break;
|
||
case SwoopState.Carrying: EscortFollowStep(); break;
|
||
}
|
||
}
|
||
|
||
private Vector3 BirdPos => (_birdRb != null) ? _birdRb.position : transform.position;
|
||
|
||
private void MoveBirdTo(Vector3 newPos)
|
||
{
|
||
if (_birdRb != null) _birdRb.MovePosition(newPos);
|
||
else transform.position = newPos;
|
||
}
|
||
|
||
private void RotateBirdTo(Quaternion newRot, float slerpFactor)
|
||
{
|
||
if (!faceVelocity) return;
|
||
var current = (_birdRb != null) ? _birdRb.rotation : transform.rotation;
|
||
var next = Quaternion.Slerp(current, newRot, slerpFactor);
|
||
if (_birdRb != null) _birdRb.MoveRotation(next);
|
||
else transform.rotation = next;
|
||
}
|
||
|
||
private float DistanceToTargetSurface(Vector3 from)
|
||
{
|
||
if (_targetCol == null) return Vector3.Distance(from, _targetRb.position);
|
||
Vector3 closest = _targetCol.ClosestPoint(from);
|
||
return Vector3.Distance(from, closest);
|
||
}
|
||
|
||
private void SeekStep()
|
||
{
|
||
// Ask Zibu to hold if we’re close-ish to make pickup reliable
|
||
float distSurface = DistanceToTargetSurface(BirdPos);
|
||
if (distSurface <= Mathf.Max(pickupRadius, freezeAIWhenWithin))
|
||
{
|
||
if (_targetAI && !_aiHoldApplied) { _targetAI.BeginPickupHold(this); _aiHoldApplied = true; }
|
||
}
|
||
|
||
// Lower approach height as we get close (prevents hovering above head)
|
||
float tHeight = Mathf.InverseLerp(pickupRadius, pickupRadius * 3f, distSurface);
|
||
float dynHeight = Mathf.Lerp(minApproachHeight, approachHeightOffset, Mathf.Clamp01(tHeight));
|
||
Vector3 aim = _targetRb.position + Vector3.up * dynHeight;
|
||
|
||
// Move toward aim
|
||
Vector3 to = aim - BirdPos;
|
||
float dist = to.magnitude;
|
||
if (dist > 0.001f)
|
||
{
|
||
Vector3 dir = to / dist;
|
||
Vector3 step = dir * birdMoveSpeed * Time.fixedDeltaTime;
|
||
if (step.magnitude > dist) step = to;
|
||
|
||
MoveBirdTo(BirdPos + step);
|
||
|
||
if (faceVelocity && step.sqrMagnitude > 1e-6f)
|
||
{
|
||
Quaternion targetRot = Quaternion.LookRotation(step.normalized, Vector3.up);
|
||
RotateBirdTo(targetRot, birdTurnSpeed * Time.fixedDeltaTime);
|
||
}
|
||
}
|
||
|
||
// Track "near" time and force pickup if we linger too long
|
||
if (distSurface <= pickupRadius * nearRangeFactor)
|
||
_nearTimer += Time.fixedDeltaTime;
|
||
else
|
||
_nearTimer = 0f;
|
||
|
||
if (distSurface <= pickupRadius + pickupSkin || _nearTimer >= forcePickupAfter)
|
||
{
|
||
StartCoroutine(CarryRoutine(_targetRb));
|
||
_state = SwoopState.Carrying;
|
||
_nearTimer = 0f;
|
||
}
|
||
}
|
||
|
||
// Safety: if our trigger touches the target, start carry immediately
|
||
void OnTriggerEnter(Collider other)
|
||
{
|
||
if (_state != SwoopState.Seeking || _targetRb == null) return;
|
||
if (other && other.attachedRigidbody == _targetRb)
|
||
{
|
||
StartCoroutine(CarryRoutine(_targetRb));
|
||
_state = SwoopState.Carrying;
|
||
_nearTimer = 0f;
|
||
}
|
||
}
|
||
|
||
private void EscortFollowStep()
|
||
{
|
||
if (_targetRb == null) return;
|
||
|
||
Vector3 follow = _targetRb.position + Vector3.up * followAboveOffset;
|
||
Vector3 to = follow - BirdPos;
|
||
float dist = to.magnitude;
|
||
|
||
if (dist > 0.001f)
|
||
{
|
||
Vector3 dir = to / dist;
|
||
Vector3 step = dir * birdMoveSpeed * Time.fixedDeltaTime;
|
||
if (step.magnitude > dist) step = to;
|
||
|
||
MoveBirdTo(BirdPos + step);
|
||
|
||
if (faceVelocity && step.sqrMagnitude > 1e-6f)
|
||
{
|
||
Quaternion targetRot = Quaternion.LookRotation(step.normalized, Vector3.up);
|
||
RotateBirdTo(targetRot, birdTurnSpeed * Time.fixedDeltaTime);
|
||
}
|
||
}
|
||
}
|
||
private IEnumerator CarryRoutine(Rigidbody rb)
|
||
{
|
||
_carried.Add(rb);
|
||
|
||
float plannedStun = Mathf.Max(0.05f, riseTime + hoverTime + extraStun);
|
||
if (_targetController) _targetController.ApplyPushStun(plannedStun);
|
||
|
||
if (!_origDrag.ContainsKey(rb)) _origDrag[rb] = rb.drag;
|
||
if (!_origGrav.ContainsKey(rb)) _origGrav[rb] = rb.useGravity;
|
||
|
||
if (disableGravityWhileCarried) rb.useGravity = false;
|
||
rb.drag = _origDrag[rb] + Mathf.Max(0f, extraDrag);
|
||
|
||
// Hold AI while carried
|
||
if (_targetAI && !_aiHoldApplied) { _targetAI.BeginPickupHold(this); _aiHoldApplied = true; }
|
||
|
||
// === Phase 1: Rise to carry height ===
|
||
Vector3 pickupPos = rb.position;
|
||
float apexY = pickupPos.y + carryHeight;
|
||
|
||
float t = 0f;
|
||
while (t < riseTime)
|
||
{
|
||
t += Time.fixedDeltaTime;
|
||
float a = Mathf.Clamp01(t / Mathf.Max(0.0001f, riseTime));
|
||
Vector3 targetPos = new Vector3(rb.position.x, Mathf.Lerp(pickupPos.y, apexY, a), rb.position.z);
|
||
ApplyPDStep(rb, targetPos);
|
||
yield return new WaitForFixedUpdate();
|
||
}
|
||
|
||
// Short hover to stabilize
|
||
float h = 0f;
|
||
while (h < hoverTime)
|
||
{
|
||
h += Time.fixedDeltaTime;
|
||
Vector3 targetPos = new Vector3(rb.position.x, apexY, rb.position.z);
|
||
ApplyPDStep(rb, targetPos);
|
||
yield return new WaitForFixedUpdate();
|
||
}
|
||
|
||
// === Phase 2: Carry horizontally to a random air offset (XZ ∈ [0..1]) ===
|
||
Vector3 airOffset = new Vector3(
|
||
Random.Range(airOffsetX.x, airOffsetX.y), 0f,
|
||
Random.Range(airOffsetZ.x, airOffsetZ.y)
|
||
);
|
||
Vector3 desiredAirPos = new Vector3(pickupPos.x + airOffset.x, apexY, pickupPos.z + airOffset.z);
|
||
|
||
float travel = 0f;
|
||
Vector3 startCarryPos = rb.position;
|
||
while (travel < travelToAirOffsetTime)
|
||
{
|
||
travel += Time.fixedDeltaTime;
|
||
float a = Mathf.Clamp01(travel / Mathf.Max(0.0001f, travelToAirOffsetTime));
|
||
Vector3 targetPos = Vector3.Lerp(startCarryPos, desiredAirPos, a);
|
||
ApplyPDStep(rb, targetPos);
|
||
yield return new WaitForFixedUpdate();
|
||
}
|
||
|
||
// === Phase 3: Choose a safe ground XZ (avoid other Zibus), then drop naturally ===
|
||
GetZibuBodyMetrics(out float bodyRadius, out float bodyHeight);
|
||
bodyRadius += inflateClearance;
|
||
|
||
Vector3 safeGround = FindClearDropGround(desiredAirPos, bodyRadius, bodyHeight);
|
||
|
||
// Align only XZ to the safe spot; KEEP CURRENT Y so gravity does the fall
|
||
//rb.position = new Vector3(safeGround.x, rb.position.y, safeGround.z);
|
||
|
||
// Restore physics (release)
|
||
if (_origGrav.TryGetValue(rb, out bool g)) rb.useGravity = g;
|
||
if (_origDrag.TryGetValue(rb, out float d)) rb.drag = d;
|
||
|
||
// Open parachute on drop (requires TriggerParachuteDrop on controller)
|
||
if (_targetController) _targetController.TriggerParachuteDrop(0.15f);
|
||
|
||
// Ensure a clear "start falling" cue
|
||
float kick = (dropKickImpulse > 0f ? dropKickImpulse : 0.5f);
|
||
rb.AddForce(Vector3.down * kick, ForceMode.Impulse);
|
||
|
||
// Release AI hold so the Zibu can move after landing
|
||
if (_targetAI && _aiHoldApplied) { _targetAI.EndPickupHold(this); _aiHoldApplied = false; }
|
||
|
||
// === Phase 4: Post-drop unstack if we land on another Zibu ===
|
||
yield return StartCoroutine(PostDropUnstack(rb, bodyRadius, bodyHeight));
|
||
|
||
// Cleanup bookkeeping
|
||
_carried.Remove(rb);
|
||
_origDrag.Remove(rb);
|
||
_origGrav.Remove(rb);
|
||
|
||
_state = SwoopState.Releasing;
|
||
_targetRb = null;
|
||
_targetController = null;
|
||
_targetAI = null;
|
||
|
||
if (destroyOnRelease) Destroy(gameObject);
|
||
else _state = SwoopState.Idle;
|
||
}
|
||
|
||
//private IEnumerator CarryRoutine(Rigidbody rb)
|
||
//{
|
||
// _carried.Add(rb);
|
||
|
||
// float plannedStun = Mathf.Max(0.05f, riseTime + hoverTime + extraStun);
|
||
// if (_targetController) _targetController.ApplyPushStun(plannedStun);
|
||
|
||
// if (!_origDrag.ContainsKey(rb)) _origDrag[rb] = rb.drag;
|
||
// if (!_origGrav.ContainsKey(rb)) _origGrav[rb] = rb.useGravity;
|
||
|
||
// if (disableGravityWhileCarried) rb.useGravity = false;
|
||
// rb.drag = _origDrag[rb] + Mathf.Max(0f, extraDrag);
|
||
|
||
// // Ensure AI is held while rising/hovering/transport
|
||
// if (_targetAI && !_aiHoldApplied) { _targetAI.BeginPickupHold(this); _aiHoldApplied = true; }
|
||
|
||
// // === Phase 1: Rise to carry height ===
|
||
// Vector3 pickupPos = rb.position;
|
||
// float targetY = pickupPos.y + carryHeight;
|
||
|
||
// float t = 0f;
|
||
// while (t < riseTime)
|
||
// {
|
||
// t += Time.fixedDeltaTime;
|
||
// float a = Mathf.Clamp01(t / Mathf.Max(0.0001f, riseTime));
|
||
// Vector3 targetPos = new Vector3(rb.position.x, Mathf.Lerp(pickupPos.y, targetY, a), rb.position.z);
|
||
// ApplyPDStep(rb, targetPos);
|
||
// yield return new WaitForFixedUpdate();
|
||
// }
|
||
|
||
// // Short hover to stabilize
|
||
// float h = 0f;
|
||
// while (h < hoverTime)
|
||
// {
|
||
// h += Time.fixedDeltaTime;
|
||
// Vector3 targetPos = new Vector3(rb.position.x, targetY, rb.position.z);
|
||
// ApplyPDStep(rb, targetPos);
|
||
// yield return new WaitForFixedUpdate();
|
||
// }
|
||
|
||
// // === Phase 2: Carry horizontally to a random air offset (XZ ∈ [0..1]) ===
|
||
// Vector3 airOffset = new Vector3(Random.Range(airOffsetX.x, airOffsetX.y), 0f, Random.Range(airOffsetZ.x, airOffsetZ.y));
|
||
// Vector3 desiredAirPos = new Vector3(pickupPos.x + airOffset.x, targetY, pickupPos.z + airOffset.z);
|
||
|
||
// float travel = 0f;
|
||
// Vector3 startCarryPos = rb.position;
|
||
// while (travel < travelToAirOffsetTime)
|
||
// {
|
||
// travel += Time.fixedDeltaTime;
|
||
// float a = Mathf.Clamp01(travel / Mathf.Max(0.0001f, travelToAirOffsetTime));
|
||
// Vector3 targetPos = Vector3.Lerp(startCarryPos, desiredAirPos, a);
|
||
// ApplyPDStep(rb, targetPos);
|
||
// yield return new WaitForFixedUpdate();
|
||
// }
|
||
|
||
// // === Phase 3: Choose a safe ground point (avoid other Zibus) & drop ===
|
||
// GetZibuBodyMetrics(out float bodyRadius, out float bodyHeight);
|
||
// bodyRadius += inflateClearance;
|
||
|
||
// Vector3 safeGround = FindClearDropGround(desiredAirPos, bodyRadius, bodyHeight);
|
||
|
||
// // Place at safe ground (slight lift to avoid ground clipping)
|
||
// //rb.position = new Vector3(safeGround.x, safeGround.y + 0.02f, safeGround.z);
|
||
// // replace the snap line with:
|
||
// //transform.DOMove(new Vector3(safeGround.x, rb.position.y, safeGround.z), 0.75f);
|
||
// //rb.position = new Vector3(safeGround.x, rb.position.y, safeGround.z);
|
||
// yield return new WaitForSeconds(0.2f);
|
||
// _state = SwoopState.Releasing;
|
||
// // (then you restore gravity and optionally add a small downward impulse)
|
||
|
||
|
||
// // Restore physics
|
||
// if (_origGrav.TryGetValue(rb, out bool g)) rb.useGravity = g;
|
||
// if (_origDrag.TryGetValue(rb, out float d)) rb.drag = d;
|
||
|
||
// if (dropKickImpulse > 0f)
|
||
// rb.AddForce(Vector3.down * dropKickImpulse, ForceMode.Impulse);
|
||
|
||
// // Release AI hold so it can move right after landing
|
||
// if (_targetAI && _aiHoldApplied) { _targetAI.EndPickupHold(this); _aiHoldApplied = false; }
|
||
|
||
// // === Phase 4: Post-drop unstack if we landed on another Zibu (layer check) ===
|
||
// yield return StartCoroutine(PostDropUnstack(rb, bodyRadius, bodyHeight));
|
||
// _carried.Remove(rb);
|
||
|
||
// // Cleanup bookkeeping
|
||
// _origDrag.Remove(rb);
|
||
// _origGrav.Remove(rb);
|
||
|
||
// //_state = SwoopState.Releasing;
|
||
// _targetRb = null;
|
||
// _targetController = null;
|
||
// _targetAI = null;
|
||
|
||
// if (destroyOnRelease) Destroy(gameObject);
|
||
// else _state = SwoopState.Idle;
|
||
//}
|
||
|
||
private void ApplyPDStep(Rigidbody rb, Vector3 targetPos)
|
||
{
|
||
Vector3 to = targetPos - rb.position;
|
||
Vector3 toXZ = new Vector3(to.x, 0f, to.z);
|
||
Vector3 toY = new Vector3(0f, to.y, 0f);
|
||
|
||
Vector3 dvXZ = posGain * toXZ - velGain * new Vector3(rb.velocity.x, 0f, rb.velocity.z);
|
||
Vector3 dvY = posGain * toY - velGain * new Vector3(0f, rb.velocity.y, 0f);
|
||
Vector3 deltaV = (dvXZ + dvY) * Time.fixedDeltaTime;
|
||
|
||
if (maxDeltaVPerFixedUpdate > 0f && deltaV.magnitude > maxDeltaVPerFixedUpdate)
|
||
deltaV = deltaV.normalized * maxDeltaVPerFixedUpdate;
|
||
|
||
rb.AddForce(deltaV, ForceMode.VelocityChange);
|
||
}
|
||
|
||
// ===== Safe-drop utilities =====
|
||
|
||
private void GetZibuBodyMetrics(out float bodyRadius, out float bodyHeight)
|
||
{
|
||
if (zibuBodyRadiusOverride > 0f)
|
||
{
|
||
bodyRadius = zibuBodyRadiusOverride;
|
||
bodyHeight = Mathf.Max(1.0f, (_targetCol ? _targetCol.bounds.size.y : 1.6f));
|
||
return;
|
||
}
|
||
|
||
if (_targetCol is CapsuleCollider cap)
|
||
{
|
||
bodyRadius = cap.radius;
|
||
bodyHeight = Mathf.Max(cap.height, cap.radius * 2f);
|
||
}
|
||
else if (_targetCol is SphereCollider sph)
|
||
{
|
||
bodyRadius = sph.radius;
|
||
bodyHeight = sph.radius * 2f;
|
||
}
|
||
else if (_targetCol)
|
||
{
|
||
var b = _targetCol.bounds;
|
||
bodyRadius = Mathf.Max(b.extents.x, b.extents.z) * 0.9f;
|
||
bodyHeight = Mathf.Max(1.0f, b.size.y);
|
||
}
|
||
else
|
||
{
|
||
bodyRadius = 0.45f;
|
||
bodyHeight = 1.6f;
|
||
}
|
||
}
|
||
|
||
private Vector3 FindClearDropGround(Vector3 desiredAirPos, float bodyRadius, float bodyHeight)
|
||
{
|
||
// Project desired XZ to ground
|
||
Vector3 desiredGround = ProjectToGround(desiredAirPos);
|
||
|
||
if (!OverlapsZibuCapsule(desiredGround, bodyRadius, bodyHeight))
|
||
return desiredGround;
|
||
|
||
// Golden-angle spiral search
|
||
const float GOLDEN_ANGLE = 2.39996323f; // radians (~137.5°)
|
||
for (int i = 1; i <= maxDropProbeSpots; i++)
|
||
{
|
||
float r = i * probeStepDistance;
|
||
float a = i * GOLDEN_ANGLE;
|
||
Vector3 probeXZ = desiredGround + new Vector3(Mathf.Cos(a) * r, 0f, Mathf.Sin(a) * r);
|
||
Vector3 probeGround = ProjectToGround(probeXZ);
|
||
if (!OverlapsZibuCapsule(probeGround, bodyRadius, bodyHeight))
|
||
return probeGround;
|
||
}
|
||
|
||
// fallback to desired ground
|
||
return desiredGround;
|
||
}
|
||
|
||
private Vector3 ProjectToGround(Vector3 worldPos)
|
||
{
|
||
// Raycast down from above carry height to place on ground
|
||
Vector3 origin = new Vector3(worldPos.x, worldPos.y + 3f, worldPos.z);
|
||
if (Physics.Raycast(origin, Vector3.down, out RaycastHit hit, 10f, groundMask, QueryTriggerInteraction.Ignore))
|
||
{
|
||
return hit.point;
|
||
}
|
||
// Fallback: keep same XZ, reduce Y a bit
|
||
return new Vector3(worldPos.x, worldPos.y - 1f, worldPos.z);
|
||
}
|
||
|
||
private bool OverlapsZibuCapsule(Vector3 groundCenter, float radius, float height)
|
||
{
|
||
// Capsule from feet (at ground) to top at body height
|
||
Vector3 p0 = groundCenter + Vector3.up * 0.05f;
|
||
Vector3 p1 = groundCenter + Vector3.up * Mathf.Max(0.2f, height);
|
||
var cols = Physics.OverlapCapsule(p0, p1, radius, zibuMask, QueryTriggerInteraction.Collide);
|
||
|
||
foreach (var c in cols)
|
||
{
|
||
if (!c || c.attachedRigidbody == null) continue;
|
||
if (c.attachedRigidbody == _targetRb) continue;
|
||
if (c.GetComponentInParent<CubeClash_ZibuController>() != null)
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// ===== Post-drop unstack (layer-based) =====
|
||
|
||
private IEnumerator PostDropUnstack(Rigidbody rb, float bodyRadius, float bodyHeight)
|
||
{
|
||
float timer = 0f;
|
||
bool teleportedOnce = false;
|
||
|
||
while (timer < unstackCheckSeconds)
|
||
{
|
||
if (IsStandingOnZibu(rb.position, out RaycastHit hitBelow))
|
||
{
|
||
// Push sideways away from the Zibu beneath
|
||
Vector3 belowCenter = hitBelow.collider.bounds.center;
|
||
Vector3 dir = new Vector3(rb.position.x - belowCenter.x, 0f, rb.position.z - belowCenter.z);
|
||
if (dir.sqrMagnitude < 1e-6f) dir = Random.insideUnitSphere; // avoid zero vector
|
||
dir.y = 0f; dir.Normalize();
|
||
|
||
if (unstackLateralImpulse > 0f)
|
||
rb.AddForce(dir * unstackLateralImpulse, ForceMode.Impulse);
|
||
if (unstackDownImpulse > 0f)
|
||
rb.AddForce(Vector3.down * unstackDownImpulse, ForceMode.Impulse);
|
||
|
||
// Optional teleport fallback if impulses didn't clear in the next check
|
||
if (unstackUseTeleportFallback && !teleportedOnce)
|
||
{
|
||
// Wait a tick to allow the impulse to act
|
||
yield return new WaitForSeconds(unstackCheckInterval * 1.25f);
|
||
timer += unstackCheckInterval * 1.25f;
|
||
|
||
if (IsStandingOnZibu(rb.position, out _))
|
||
{
|
||
Vector3 targetXZ = rb.position + (dir * unstackTeleportStep);
|
||
Vector3 ground = ProjectToGround(targetXZ);
|
||
rb.position = new Vector3(ground.x, ground.y + 0.02f, ground.z);
|
||
teleportedOnce = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
yield return new WaitForSeconds(unstackCheckInterval);
|
||
timer += unstackCheckInterval;
|
||
}
|
||
|
||
// Final sanity: if we still overlap a Zibu capsule at end, nudge to nearest clear ground spot.
|
||
if (OverlapsZibuCapsule(ProjectToGround(rb.position), bodyRadius, bodyHeight))
|
||
{
|
||
Vector3 fallback = FindClearDropGround(rb.position, bodyRadius, bodyHeight);
|
||
rb.position = new Vector3(fallback.x, fallback.y + 0.02f, fallback.z);
|
||
}
|
||
}
|
||
|
||
// True if FIRST thing below is on zibuMask (i.e., standing on another Zibu instead of ground)
|
||
private bool IsStandingOnZibu(Vector3 pos, out RaycastHit hit)
|
||
{
|
||
Vector3 origin = pos + Vector3.up * Mathf.Clamp(unstackProbeDown * 0.5f, 0.4f, 1.2f);
|
||
// Check zibu or ground, nearest first
|
||
if (Physics.SphereCast(origin, unstackProbeRadius, Vector3.down, out hit, unstackProbeDown, zibuMask | groundMask, QueryTriggerInteraction.Ignore))
|
||
{
|
||
int layer = hit.collider.gameObject.layer;
|
||
bool firstIsZibu = (zibuMask & (1 << layer)) != 0;
|
||
bool firstIsGround = (groundMask & (1 << layer)) != 0;
|
||
return firstIsZibu && !firstIsGround;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void OnDrawGizmosSelected()
|
||
{
|
||
Gizmos.color = Color.cyan;
|
||
Gizmos.DrawWireSphere(BirdPos, pickupRadius);
|
||
Gizmos.color = Color.yellow;
|
||
Gizmos.DrawWireSphere(BirdPos, Mathf.Max(pickupRadius, freezeAIWhenWithin));
|
||
}
|
||
}
|