MiniGames/Assets/Scripts/CubeClash/CubeClash_BirdSwoop.cs

699 lines
26 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;
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 were 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));
}
}