using UnityEngine; using System.Collections; using DG.Tweening; #if UNITY_INPUT_SYSTEM using UnityEngine.InputSystem; #endif public class ClawController : MonoBehaviour { [Header("Hierarchy (Crane)")] public Transform craneA; // base rotates around Y public Transform craneB; public Transform craneC; public Transform craneD; public Transform craneE; // telescopes by scaling Z public Transform clawPivot; // tip / grab point public Transform grabPoint; // tip / grab point [Header("Rotation (CraneA)")] public float rotateSpeed = 90f; // deg/sec public float minYaw = -75f; // clamp local Y (signed) public float maxYaw = 75f; // clamp local Y (signed) public bool invertX = false; // flip stick if needed public bool jamBlocksClockwise = true; // Shock blocks +X when true [Header("Vertical Adjust (localPosition.y)")] public bool enableVerticalAdjust = true; public Transform verticalTarget; // defaults to grabPoint in Awake() public float verticalSpeed = 0.004f; // units/second (range is 0.002, so this moves end-to-end in ~0.5s) public float minLocalY = -0.004f; // lower limit public float maxLocalY = -0.002f; // upper limit public bool invertY = false; [Header("Drop / Grab")] [Tooltip("Seconds the claw takes to descend.")] public float descendDuration = 1.5f; [Tooltip("Cooldown between button presses.")] public float pressCooldown = 0.35f; [Tooltip("Claw down scale (Z) for craneE; larger = extend more (go down).")] public float downZ = 2.5f; [Tooltip("Claw up/resting scale (Z) for craneE.")] public float upZ = 1.0f; [Tooltip("Detection around clawPivot at the bottom position.")] public float grabRadius = 0.25f; public float grabRayLength = 1.0f; public LayerMask bubbleMask = ~0; [Header("Lift Visuals")] public float snapToPivotDuration = 0.25f; public Ease snapEase = Ease.OutQuad; public float raiseDuration = 0.5f; [Header("SFX (optional)")] public AudioSource sfx; public AudioClip sfxDropStart; public AudioClip sfxGrab; public AudioClip sfxDropRelease; public AudioClip sfxEmpty; // ---- State ---- private bool canControl = true; private bool jammed = false; // Shock effect flag private bool busy = false; // block re-entrancy during tween/coroutine private float lastPressTime = -999f; private GameObject heldObject; private Rigidbody heldRb; public Joystick joystick; void Awake() { if (descendDuration < 1.5f) descendDuration = 1.5f; // per spec if (craneE != null) craneE.localScale = new Vector3(craneE.localScale.x, craneE.localScale.y, upZ); if (verticalTarget == null) verticalTarget = grabPoint; // default } void Update() { HandleRotate(); HandleVerticalAdjust(); #if !UNITY_INPUT_SYSTEM // (Optional) Space/gamepad as a shortcut for testing alongside UI button: if (Input.GetKeyDown(KeyCode.Space)) OnDropButtonClicked(); #endif // Debug ray if (clawPivot) Debug.DrawRay(clawPivot.position, Vector3.down * grabRayLength, heldObject ? Color.cyan : Color.yellow); } // ======================= // ROTATION-BASED MOVEMENT // ======================= private void HandleRotate() { if (!canControl || craneA == null) return; float h = 0f; #if UNITY_INPUT_SYSTEM if (Gamepad.current != null) h = Gamepad.current.leftStick.x.ReadValue(); if (Keyboard.current != null) { if (Keyboard.current.leftArrowKey.isPressed || Keyboard.current.aKey.isPressed) h -= 1f; if (Keyboard.current.rightArrowKey.isPressed || Keyboard.current.dKey.isPressed) h += 1f; } #else //h = Input.GetAxisRaw("Horizontal"); if (Input.GetAxisRaw("Horizontal") > 0 || Input.GetAxisRaw("Horizontal") < 0) h = Input.GetAxisRaw("Horizontal"); else h = joystick.Horizontal; #endif if (invertX) h = -h; // Shock jam: block one rotation direction if (jammed) { if (jamBlocksClockwise && h > 0f) h = 0f; if (!jamBlocksClockwise && h < 0f) h = 0f; } if (Mathf.Approximately(h, 0f)) return; // Current signed local Y (-180..180) Vector3 e = craneA.localEulerAngles; float currYaw = Mathf.DeltaAngle(0f, e.y); // Apply delta and clamp float yawDelta = h * rotateSpeed * Time.deltaTime; float newYaw = Mathf.Clamp(currYaw + yawDelta, minYaw, maxYaw); // Write back as 0..360 e.y = Mathf.Repeat(newYaw + 360f, 360f); craneA.localEulerAngles = e; } private void HandleVerticalAdjust() { if (!enableVerticalAdjust || verticalTarget == null) return; float v = 0f; #if UNITY_INPUT_SYSTEM // Gamepad + keyboard (W/S or Up/Down) if (Gamepad.current != null) v = Gamepad.current.leftStick.y.ReadValue(); if (Keyboard.current != null) { if (Keyboard.current.upArrowKey.isPressed || Keyboard.current.wKey.isPressed) v += 1f; if (Keyboard.current.downArrowKey.isPressed || Keyboard.current.sKey.isPressed) v -= 1f; } #else // Legacy Input: prefer axis if present, else use on-screen joystick float axisV = Input.GetAxisRaw("Vertical"); if (Mathf.Abs(axisV) > 0.0001f) v = axisV; else if (joystick != null) v = joystick.Vertical; #endif if (invertY) v = -v; if (Mathf.Abs(v) < 0.0001f) return; // incremental move, clamped to [-0.004, -0.002] Vector3 lp = verticalTarget.localPosition; lp.y = Mathf.Clamp(lp.y + v * verticalSpeed * Time.deltaTime, minLocalY, maxLocalY); verticalTarget.localPosition = lp; } // ========================== // UI BUTTON CLICK ENTRYPOINT // ========================== public void OnDropButtonClicked() { if (!canControl || busy) return; if (Time.time - lastPressTime < pressCooldown) return; lastPressTime = Time.time; if (heldObject == null) StartCoroutine(Press_GrabFlow()); else StartCoroutine(Press_DropFlow()); } // ================================= // PRESS → GRAB FLOW (no object held) // ================================= private IEnumerator Press_GrabFlow() { busy = true; // Descend // if (sfx && sfxDropStart) sfx.PlayOneShot(sfxDropStart); yield return TweenCraneZ(downZ, descendDuration); // Try grab at bottom TryGrabAtBottom(); // Small settle if grabbed for nicer snap if (heldObject != null) yield return new WaitForSeconds(0.05f); // Ascend yield return TweenCraneZ(upZ, raiseDuration); busy = false; } // ================================= // PRESS → DROP FLOW (object is held) // ================================= private IEnumerator Press_DropFlow() { busy = true; // Descend if (sfx && sfxDropStart) sfx.PlayOneShot(sfxDropStart); yield return TweenCraneZ(downZ/2, descendDuration/2); // Drop in place (release physics) ReleaseHeldAtBottom(); // Ascend yield return TweenCraneZ(upZ, raiseDuration); busy = false; } // ------------- // Tween helpers // ------------- private IEnumerator TweenCraneZ(float targetZ, float duration) { if (craneE == null) { Debug.LogError("[ClawController] craneE is not assigned."); yield break; } if (duration <= 0f) { // Set instantly if duration is 0 var s = craneE.localScale; s.z = targetZ; craneE.localScale = s; yield break; } // Kill any existing scale tween on craneE so it doesn't conflict DOTween.Kill(craneE, complete: false); // Do the tween on Z only Tween t = craneE.DOScaleZ(targetZ, duration) .SetEase(Ease.Linear) .SetTarget(craneE); // so Kill(craneE) finds it next time yield return t.WaitForCompletion(); } // ------------- // Grab / Drop // ------------- private void TryGrabAtBottom() { if (heldObject != null) return; ClawBubble candidate = null; float bestSqr = float.MaxValue; // Sphere overlap var hits = Physics.OverlapSphere(clawPivot.position, grabRadius, bubbleMask, QueryTriggerInteraction.Ignore); foreach (var c in hits) { var cb = c.GetComponentInParent(); if (!cb) continue; float d = (cb.transform.position - clawPivot.position).sqrMagnitude; if (d < bestSqr) { bestSqr = d; candidate = cb; } } // Fallback ray if (candidate == null) { if (Physics.Raycast(clawPivot.position, Vector3.down, out var hit, grabRayLength, bubbleMask, QueryTriggerInteraction.Ignore)) candidate = hit.collider.GetComponentInParent(); } if (candidate == null) { if (sfx && sfxEmpty) sfx.PlayOneShot(sfxEmpty); return; } // Grab: parent + freeze physics heldObject = candidate.gameObject; heldRb = heldObject.GetComponent(); if (heldRb) { heldRb.velocity = Vector3.zero; heldRb.angularVelocity = Vector3.zero; heldRb.isKinematic = true; } var t = heldObject.transform; t.SetParent(grabPoint, worldPositionStays: true); t.DOLocalMove(Vector3.zero, snapToPivotDuration).SetEase(snapEase); if (sfx && sfxGrab) sfx.PlayOneShot(sfxGrab); } private void ReleaseHeldAtBottom() { if (heldObject == null) return; // Unparent heldObject.transform.SetParent(null, true); // Restore physics so it falls freely if (heldRb) { heldRb.isKinematic = false; heldRb.useGravity = true; heldRb.constraints = RigidbodyConstraints.None; // no rotation/position restrictions heldRb = null; } if (sfx && sfxDropRelease) sfx.PlayOneShot(sfxDropRelease); heldObject = null; } // ========= // FX Hooks // ========= /// Bomb: disable all control for 'seconds'. public void ApplyDisable(float seconds) { if (!gameObject.activeInHierarchy) return; StartCoroutine(_Disable(seconds)); } private IEnumerator _Disable(float s) { bool prev = canControl; canControl = false; yield return new WaitForSeconds(s); canControl = prev; } /// Shock: jam one rotation direction for 'seconds'. public void ApplyJam(float seconds) { if (!gameObject.activeInHierarchy) return; StartCoroutine(_Jam(seconds)); } private IEnumerator _Jam(float s) { jammed = true; yield return new WaitForSeconds(s); jammed = false; } // ===== // Gizmo // ===== void OnDrawGizmosSelected() { if (clawPivot) { Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(clawPivot.position, grabRadius); } } } //using UnityEngine; //using System.Collections; //using DG.Tweening; //#if UNITY_INPUT_SYSTEM //using UnityEngine.InputSystem; //#endif //public class ClawController : MonoBehaviour //{ // [Header("Hierarchy (Crane)")] // public Transform craneA; // base that ROTATES around Y with joystick // public Transform craneB; // public Transform craneC; // public Transform craneD; // public Transform craneE; // telescopes by scaling Z // public Transform clawPivot; // tip / grab point // [Header("Rotation (CraneA)")] // public float rotateSpeed = 90f; // deg/sec // public float minYaw = -75f; // clamp local Y (signed) // public float maxYaw = 75f; // clamp local Y (signed) // public bool invertX = false; // flip stick if needed // public bool jamBlocksClockwise = true; // Shock: block +X when true // [Header("Drop / Grab")] // [Tooltip("Seconds the claw takes to descend (doc: 1.5s).")] // public float descendDuration = 1.5f; // [Tooltip("Cooldown between drops.")] // public float dropCooldown = 2.0f; // [Tooltip("Claw down scale (Z) for craneE; larger = extend more (go down).")] // public float downZ = 2.5f; // [Tooltip("Claw up/resting scale (Z) for craneE.")] // public float upZ = 1.0f; // [Tooltip("Detection around clawPivot at the bottom position.")] // public float grabRadius = 0.25f; // public float grabRayLength = 1.0f; // public LayerMask bubbleMask = ~0; // [Header("Delivery")] // [Tooltip("Optional: where to toss/deliver. If null, deliver in place.")] // public Transform rewardsChute; // public float liftDuration = 0.5f; // public float deliverMoveDuration = 0.4f; // public float deliverArcHeight = 0.6f; // [Header("SFX (optional)")] // public AudioSource sfx; // public AudioClip sfxDropStart; // public AudioClip sfxGrab; // public AudioClip sfxEmpty; // // ---- State ---- // private bool canControl = true; // private bool jammed = false; // Shock effect flag // private float lastDropTime = -999f; // private GameObject heldObject; // private Rigidbody heldRb; // private Vector3 craneEUpScale; // public Joystick joystick; // void Awake() // { // if (craneE != null) craneEUpScale = craneE.localScale; // if (descendDuration < 1.5f) descendDuration = 1.5f; // enforce doc spec minimum // } // void Update() // { // if (clawPivot) // Debug.DrawRay(clawPivot.position, Vector3.down * grabRayLength, heldObject ? Color.cyan : Color.yellow); // HandleMove(); // HandleDropInput(); // } // // ======================= // // ROTATION-BASED MOVEMENT // // ======================= // private void HandleMove() // { // if (!canControl || craneA == null) return; // float h = 0f; //#if UNITY_INPUT_SYSTEM // if (Gamepad.current != null) // h = Gamepad.current.leftStick.x.ReadValue(); // if (Keyboard.current != null) // { // if (Keyboard.current.leftArrowKey.isPressed || Keyboard.current.aKey.isPressed) h -= 1f; // if (Keyboard.current.rightArrowKey.isPressed || Keyboard.current.dKey.isPressed) h += 1f; // } //#else // if (Input.GetAxisRaw("Horizontal") > 0|| Input.GetAxisRaw("Horizontal") < 0) h = Input.GetAxisRaw("Horizontal"); // else h = joystick.Horizontal; //#endif // if (invertX) h = -h; // // Shock jam: block one rotation direction // if (jammed) // { // if (jamBlocksClockwise && h > 0f) h = 0f; // if (!jamBlocksClockwise && h < 0f) h = 0f; // } // if (Mathf.Approximately(h, 0f)) return; // // Get current signed local Y (-180..180) // Vector3 e = craneA.localEulerAngles; // float currYaw = Mathf.DeltaAngle(0f, e.y); // // Apply delta and clamp // float yawDelta = h * rotateSpeed * Time.deltaTime; // float newYaw = Mathf.Clamp(currYaw + yawDelta, minYaw, maxYaw); // // Write back as 0..360 // e.y = Mathf.Repeat(newYaw + 360f, 360f); // craneA.localEulerAngles = e; // } // // ============== // // DROP-TO-GRAB IO // // ============== // private void HandleDropInput() // { // if (!canControl) return; // bool pressed = false; //#if UNITY_INPUT_SYSTEM // if (Keyboard.current != null && Keyboard.current.spaceKey.wasPressedThisFrame) pressed = true; // if (Gamepad.current != null && Gamepad.current.buttonSouth.wasPressedThisFrame) pressed = true; //#else // if (Input.GetKeyDown(KeyCode.Space)) pressed = true; //#endif // if (!pressed) return; // if (Time.time - lastDropTime < dropCooldown) return; // lastDropTime = Time.time; // StartCoroutine(DropToGrab()); // << keep the original coroutine name // } // /// // /// Descend (DOTween scale Z), attempt grab at bottom, rise, deliver. // /// Matches your previous DropToGrab-style flow. // /// // private IEnumerator DropToGrab() // { // // ---- Go Down ---- // if (sfx && sfxDropStart) sfx.PlayOneShot(sfxDropStart); // Tween downTween = null; // if (craneE) // { // Vector3 to = craneE.localScale; to.z = downZ; // downTween = craneE.DOScale(to, descendDuration).SetEase(Ease.Linear); // } // if (downTween != null) yield return downTween.WaitForCompletion(); // else yield return new WaitForSeconds(descendDuration); // // ---- Try Grab at Bottom ---- // TryGrabAtBottom(); // // tiny settle delay for visuals if we grabbed something // if (heldObject != null) // yield return new WaitForSeconds(0.05f); // // ---- Rise Up ---- // Tween upTween = null; // if (craneE) // { // Vector3 to = craneE.localScale; to.z = upZ; // upTween = craneE.DOScale(to, 0.5f).SetEase(Ease.Linear); // } // if (upTween != null) yield return upTween.WaitForCompletion(); // else yield return new WaitForSeconds(0.5f); // // ---- Deliver / Score ---- // if (heldObject != null) // yield return DeliverHeld(); // } // private void TryGrabAtBottom() // { // if (heldObject != null) return; // ClawBubble candidate = null; // float bestSqr = float.MaxValue; // // Sphere overlap first (tolerant) // var hits = Physics.OverlapSphere(clawPivot.position, grabRadius, bubbleMask, QueryTriggerInteraction.Ignore); // foreach (var c in hits) // { // var cb = c.GetComponentInParent(); // if (!cb) continue; // float d = (cb.transform.position - clawPivot.position).sqrMagnitude; // if (d < bestSqr) // { // bestSqr = d; // candidate = cb; // } // } // // Fallback: straight ray down // if (candidate == null) // { // if (Physics.Raycast(clawPivot.position, Vector3.down, out var hit, grabRayLength, bubbleMask, QueryTriggerInteraction.Ignore)) // candidate = hit.collider.GetComponentInParent(); // } // if (candidate == null) // { // if (sfx && sfxEmpty) sfx.PlayOneShot(sfxEmpty); // return; // } // // Grab: parent to pivot, freeze physics // heldObject = candidate.gameObject; // heldRb = heldObject.GetComponent(); // if (heldRb) // { // heldRb.velocity = Vector3.zero; // heldRb.angularVelocity = Vector3.zero; // heldRb.isKinematic = true; // } // var t = heldObject.transform; // t.SetParent(clawPivot, worldPositionStays: true); // t.DOLocalMove(Vector3.zero, liftDuration).SetEase(Ease.OutQuad); // if (sfx && sfxGrab) sfx.PlayOneShot(sfxGrab); // } // private IEnumerator DeliverHeld() // { // var cb = heldObject.GetComponent(); // var type = cb ? cb.type : ClawBubbleType.Wolf; // // Optional arc to chute // if (rewardsChute != null) // { // Vector3 start = heldObject.transform.position; // Vector3 end = rewardsChute.position; // Vector3 peak = (start + end) * 0.5f + Vector3.up * deliverArcHeight; // var seq = DOTween.Sequence(); // seq.Append(heldObject.transform.DOMove(peak, deliverMoveDuration * 0.5f).SetEase(Ease.OutQuad)); // seq.Append(heldObject.transform.DOMove(end, deliverMoveDuration * 0.5f).SetEase(Ease.InQuad)); // yield return seq.WaitForCompletion(); // } // // Score & effects // if (ClawScoreManager.Instance != null) // ClawScoreManager.Instance.EvaluateBubble(type); // // Safety notify to spawner (in case you prefer it here) // if (ClawBubbleSpawner.Instance != null) // ClawBubbleSpawner.Instance.NotifyBubbleGrabbed(type); // // Cleanup grabbed object // if (heldObject != null) // { // heldObject.transform.SetParent(null, true); // Destroy(heldObject); // } // heldObject = null; // heldRb = null; // } // // ========= // // FX Hooks // // ========= // /// Bomb: disable all control for 'seconds'. // public void ApplyDisable(float seconds) // { // if (!gameObject.activeInHierarchy) return; // StartCoroutine(_Disable(seconds)); // } // private IEnumerator _Disable(float s) // { // bool prev = canControl; // canControl = false; // yield return new WaitForSeconds(s); // canControl = prev; // } // /// Shock: jam one rotation direction for 'seconds'. // public void ApplyJam(float seconds) // { // if (!gameObject.activeInHierarchy) return; // StartCoroutine(_Jam(seconds)); // } // private IEnumerator _Jam(float s) // { // jammed = true; // yield return new WaitForSeconds(s); // jammed = false; // } // // ===== // // Gizmo // // ===== // void OnDrawGizmosSelected() // { // if (clawPivot) // { // Gizmos.color = Color.yellow; // Gizmos.DrawWireSphere(clawPivot.position, grabRadius); // } // } //} //using UnityEngine; //using System.Collections; //using DG.Tweening; //#if UNITY_INPUT_SYSTEM //using UnityEngine.InputSystem; //#endif //public class ClawController : MonoBehaviour //{ // [Header("Hierarchy")] // public Transform craneA; // Crane_A (rotates as base) // public Transform craneB; // public Transform craneC; // public Transform craneD; // public Transform craneE; // Scale Z to drop // public Transform clawPivot; // Ray origin (tip/pivot of claw) // public Transform grabPoint; // Where held ball sits (keep scale ~1,1,1 ideally) // [Header("Movement/Input")] // public float rotateSpeed = 90f; // public string horizontalAxis = "Horizontal"; // legacy axis (keyboard/gamepad) // public string dropButtonName = "Fire1"; // legacy button // public KeyCode dropKey = KeyCode.Space; // keyboard fallback // public Joystick virtualJoystick; // optional on-screen joystick // [Header("Extend/Retract")] // public float descendTime = 0.6f; // public float dropZ = 50f; // craneE localScale.z when extended // public float upZ = 40f; // craneE localScale.z when idle // public float dropCooldown = 0.5f; // [Header("Raycast & Layers")] // public float grabRayLength = 2f; // public LayerMask ballLayerMask; // set in Inspector (Ball layer) // [Tooltip("Layer index to assign while the ball is held to avoid re-grab; set -1 to leave unchanged.")] // public int grabbedBallLayer = -1; // [Tooltip("Layer index to assign on release; set -1 to leave unchanged.")] // public int releasedBallLayer = -1; // private bool canDrop = true; // private GameObject heldObject; // [Header("Drop Tuning")] // public float descendDuration = 1.5f; // doc requirement // private bool canControl = true; // private bool jammed = false; // private float lastDropTime = -999f; // void Reset() // { // if (craneE) // { // var s = craneE.localScale; // craneE.localScale = new Vector3(s.x, s.y, upZ); // } // } // void Update() // { // // Visualize the ray (Scene view) // Debug.DrawRay(clawPivot.position, Vector3.down * grabRayLength, heldObject ? Color.cyan : Color.yellow); // // Rotation input (use Vertical from joystick per your previous setup; change to Horizontal if you prefer) // float x = 0f; // if (virtualJoystick != null) x = virtualJoystick.Vertical; // else x = Input.GetAxis(horizontalAxis); // // Rotate around Crane_A's local forward axis (your setup) // craneA.Rotate(craneA.forward, x * rotateSpeed * Time.deltaTime, Space.World); // // Drop/Action input // //bool dropPressed = Input.GetButtonDown(dropButtonName) || Input.GetKeyDown(dropKey); //#if UNITY_INPUT_SYSTEM // if (!dropPressed && Gamepad.current != null) // dropPressed = Gamepad.current.buttonSouth.wasPressedThisFrame; // A/Cross //#endif // //if (dropPressed && canDrop) // //{ // // if (heldObject != null) // // { // // // Drop wherever it is // // StartCoroutine(ReleaseHeldHere()); // // } // // else // // { // // // Try to grab a ball under the claw // // if (Physics.Raycast(clawPivot.position, Vector3.down, out RaycastHit ballHit, grabRayLength, ballLayerMask)) // // { // // StartCoroutine(DropToGrab(ballHit.collider.gameObject)); // // } // // // else: nothing under → do nothing (or add a small poke if you want) // // } // //} // } // // UI Button hook for mobile // public void TriggerDrop() // { // if (!canDrop) return; // if (heldObject != null) // { // StartCoroutine(ReleaseHeldHere()); // } // else if (Physics.Raycast(clawPivot.position, Vector3.down, out RaycastHit ballHit, grabRayLength, ballLayerMask)) // { // StartCoroutine(DropToGrab(ballHit.collider.gameObject)); // } // else // { // StartCoroutine(DropToGrab()); // } // } // IEnumerator DropToGrab(GameObject targetBall=null) // { // canDrop = false; // // Extend // var s = craneE.localScale; // var start = new Vector3(s.x, s.y, upZ); // var end = new Vector3(s.x, s.y, dropZ); // craneE.localScale = start; // craneE.DOScale(end, descendTime); // yield return new WaitForSeconds(descendTime); // if (targetBall != null) // { // ClawScoreManager.Instance.EvaluateBubble(targetBall.GetComponent().type); // // Disable physics while held // var rb = targetBall.GetComponent(); // if (rb != null) // { // rb.isKinematic = true; // rb.constraints = RigidbodyConstraints.None; // rb.velocity = Vector3.zero; // rb.angularVelocity = Vector3.zero; // } // // Parent to grabPoint without visual scale change // PreserveWorldScaleParent(targetBall.transform, grabPoint); // targetBall.transform.localPosition = Vector3.zero; // targetBall.transform.localRotation = Quaternion.identity; // // Move to 'grabbed' layer if requested // if (grabbedBallLayer >= 0) // SetLayerRecursively(targetBall, grabbedBallLayer); // heldObject = targetBall; // } // // Retract // craneE.DOScale(start, descendTime); // yield return new WaitForSeconds(descendTime); // yield return new WaitForSeconds(dropCooldown); // canDrop = true; // } // IEnumerator ReleaseHeldHere() // { // canDrop = false; // if (heldObject != null) // { // // Unparent at current position // heldObject.transform.SetParent(null, true); // // Re-enable full physics // var rb = heldObject.GetComponent(); // if (rb != null) // { // rb.isKinematic = false; // rb.constraints = RigidbodyConstraints.None; // no restrictions // rb.velocity = Vector3.zero; // rb.angularVelocity = Vector3.zero; // } // // Optionally restore/change layer on release // if (releasedBallLayer >= 0) // SetLayerRecursively(heldObject, releasedBallLayer); // heldObject = null; // } // yield return new WaitForSeconds(dropCooldown); // canDrop = true; // } // // --- Utilities --- // // Keep visual scale identical after parenting into a scaled hierarchy // private void PreserveWorldScaleParent(Transform child, Transform newParent) // { // Vector3 worldScale = child.lossyScale; // child.SetParent(newParent, true); // keep world pos/rot // Vector3 parentScale = newParent.lossyScale; // child.localScale = new Vector3( // SafeDiv(worldScale.x, parentScale.x), // SafeDiv(worldScale.y, parentScale.y), // SafeDiv(worldScale.z, parentScale.z) // ); // } // private float SafeDiv(float a, float b) => Mathf.Approximately(b, 0f) ? 0f : a / b; // private void SetLayerRecursively(GameObject go, int layerIndex) // { // //go.layer = layerIndex; // //foreach (Transform t in go.transform) // // SetLayerRecursively(t.gameObject, layerIndex); // } //}