482 lines
14 KiB
C#
482 lines
14 KiB
C#
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;
|
|
private int _disableLocks = 0;
|
|
private int _jamLocks = 0;
|
|
|
|
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;
|
|
|
|
// NEW: tell the mode manager a drop attempt is happening now
|
|
|
|
if (heldObject == null)
|
|
{
|
|
ClawGameModeManager.Instance?.NotifyDropAttempt();
|
|
StartCoroutine(Press_GrabFlow());
|
|
}
|
|
else
|
|
StartCoroutine(Press_DropFlow());
|
|
}
|
|
|
|
//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;
|
|
|
|
var hits = Physics.OverlapSphere(clawPivot.position, grabRadius, bubbleMask, QueryTriggerInteraction.Ignore);
|
|
foreach (var c in hits)
|
|
{
|
|
var cb = c.GetComponentInParent<ClawBubble>();
|
|
if (!cb) continue;
|
|
float d = (cb.transform.position - clawPivot.position).sqrMagnitude;
|
|
if (d < bestSqr) { bestSqr = d; candidate = cb; }
|
|
}
|
|
|
|
if (candidate == null)
|
|
{
|
|
if (Physics.Raycast(clawPivot.position, Vector3.down, out var hit, grabRayLength, bubbleMask, QueryTriggerInteraction.Ignore))
|
|
candidate = hit.collider.GetComponentInParent<ClawBubble>();
|
|
}
|
|
|
|
if (candidate == null)
|
|
{
|
|
// NEW: precision miss should penalize
|
|
ClawGameModeManager.Instance?.NotifyMiss();
|
|
if (sfx && sfxEmpty) sfx.PlayOneShot(sfxEmpty);
|
|
return;
|
|
}
|
|
|
|
// NEW: notify what we grabbed (mode may use this info)
|
|
ClawGameModeManager.Instance?.NotifyGrabbed(candidate.type);
|
|
|
|
// (existing grab logic)
|
|
heldObject = candidate.gameObject;
|
|
heldRb = heldObject.GetComponent<Rigidbody>();
|
|
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 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<ClawBubble>();
|
|
// 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<ClawBubble>();
|
|
// }
|
|
|
|
// if (candidate == null)
|
|
// {
|
|
// if (sfx && sfxEmpty) sfx.PlayOneShot(sfxEmpty);
|
|
// return;
|
|
// }
|
|
|
|
// // Grab: parent + freeze physics
|
|
// heldObject = candidate.gameObject;
|
|
// heldRb = heldObject.GetComponent<Rigidbody>();
|
|
// 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;
|
|
}
|
|
public void ApplyDisable(float seconds)
|
|
{
|
|
if (!gameObject.activeInHierarchy) return;
|
|
StartCoroutine(DisableLock(seconds));
|
|
}
|
|
private IEnumerator DisableLock(float s)
|
|
{
|
|
_disableLocks++;
|
|
canControl = false;
|
|
if (s > 0f) yield return new WaitForSeconds(s);
|
|
_disableLocks = Mathf.Max(0, _disableLocks - 1);
|
|
if (_disableLocks == 0) canControl = true;
|
|
}
|
|
|
|
public void ApplyJam(float seconds)
|
|
{
|
|
if (!gameObject.activeInHierarchy) return;
|
|
StartCoroutine(JamLock(seconds));
|
|
}
|
|
private IEnumerator JamLock(float s)
|
|
{
|
|
_jamLocks++;
|
|
jammed = true;
|
|
if (s > 0f) yield return new WaitForSeconds(s);
|
|
_jamLocks = Mathf.Max(0, _jamLocks - 1);
|
|
if (_jamLocks == 0) jammed = false;
|
|
}
|
|
|
|
// === new hard overrides used by the mode manager ===
|
|
public void ForceEnableControl()
|
|
{
|
|
_disableLocks = 0;
|
|
canControl = true;
|
|
}
|
|
public void ForceClearJam()
|
|
{
|
|
_jamLocks = 0;
|
|
jammed = false;
|
|
}
|
|
|
|
// =========
|
|
// FX Hooks
|
|
// =========
|
|
/// <summary>Bomb: disable all control for 'seconds'.</summary>
|
|
|
|
private IEnumerator _Disable(float s)
|
|
{
|
|
bool prev = canControl;
|
|
canControl = false;
|
|
yield return new WaitForSeconds(s);
|
|
canControl = prev;
|
|
}
|
|
|
|
/// <summary>Shock: jam one rotation direction for 'seconds'.</summary>
|
|
|
|
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);
|
|
}
|
|
}
|
|
} |