MiniGames/Assets/Scripts/CubeClash/CubeClash_BirdSwoop.cs

699 lines
26 KiB
C#
Raw Normal View History

using UnityEngine;
2025-09-01 05:03:00 +05:00
using System.Collections;
using System.Collections.Generic;
using DG.Tweening;
2025-09-01 05:03:00 +05:00
[RequireComponent(typeof(Collider))]
2025-09-01 05:03:00 +05:00
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")]
2025-09-01 05:03:00 +05:00
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;
}
2025-09-01 05:03:00 +05:00
private Rigidbody PickRandomZibuRigidbody()
2025-09-01 05:03:00 +05:00
{
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
2025-09-01 05:03:00 +05:00
{
foreach (var go in GameObject.FindGameObjectsWithTag(zibuTag))
{
if (!go || !go.activeInHierarchy) continue;
var rb = go.GetComponent<Rigidbody>();
if (rb) candidates.Add(rb);
}
2025-09-01 05:03:00 +05:00
}
if (candidates.Count == 0) return null;
return candidates[Random.Range(0, candidates.Count)];
2025-09-01 05:03:00 +05:00
}
void FixedUpdate()
2025-09-01 05:03:00 +05:00
{
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;
2025-09-01 05:03:00 +05:00
float t = 0f;
while (t < riseTime)
2025-09-01 05:03:00 +05:00
{
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();
2025-09-01 05:03:00 +05:00
}
// 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));
2025-09-01 05:03:00 +05:00
}
}