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 _carried = new HashSet(); private readonly Dictionary _origDrag = new Dictionary(); private readonly Dictionary _origGrav = new Dictionary(); // anti-stuck timer private float _nearTimer = 0f; void Reset() { var col = GetComponent(); if (col) col.isTrigger = true; } void Awake() { _birdRb = GetComponent(); 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(); 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(); _targetController = _targetRb.GetComponent(); _targetAI = _targetRb.GetComponent(); _state = SwoopState.Seeking; _nearTimer = 0f; _aiHoldApplied = false; } private Rigidbody PickRandomZibuRigidbody() { List candidates = new List(); 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(); if (rb) candidates.Add(rb); } } else { foreach (var go in GameObject.FindGameObjectsWithTag(zibuTag)) { if (!go || !go.activeInHierarchy) continue; var rb = go.GetComponent(); 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() != 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)); } }