diff --git a/Assets/Scripts/Dice/DiceRoller.cs b/Assets/Scripts/Dice/DiceRoller.cs new file mode 100644 index 0000000..9e38bff --- /dev/null +++ b/Assets/Scripts/Dice/DiceRoller.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections; +using UnityEngine; +using Random = UnityEngine.Random; + +/// +/// Красиво подбрасывает и раскручивает кубик, ждёт пока он остановится, +/// плавно доворачивает до ровного положения и сообщает результат. +/// +public sealed class DiceRoller : MonoBehaviour +{ + // ─────────────────────────── Inspector ─────────────────────────── + + [Header("References")] + [SerializeField] private Dice dice; + [SerializeField] private Rigidbody rb; + + [Header("Throw Settings")] + [Tooltip("Сила подброса вверх")] + [SerializeField] private float throwUpForce = 6f; + + [Tooltip("Горизонтальный разброс")] + [SerializeField] private float throwScatter = 1.5f; + + [Tooltip("Минимальный крутящий момент")] + [SerializeField] private float torqueMin = 15f; + + [Tooltip("Максимальный крутящий момент")] + [SerializeField] private float torqueMax = 30f; + + [Header("Settle Detection")] + [Tooltip("Порог скорости, ниже которого кубик считается остановившимся")] + [SerializeField] private float settleSpeed = 0.05f; + + [Tooltip("Сколько секунд кубик должен быть неподвижен, чтобы засчитать остановку")] + [SerializeField] private float settleDelay = 0.3f; + + [Header("Snap Alignment")] + [Tooltip("Длительность плавного доворота к ровному положению")] + [SerializeField] private float snapDuration = 0.35f; + + [SerializeField] + private AnimationCurve snapCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f); + + // ─────────────────────────── Events ────────────────────────────── + + /// Вызывается когда кубик полностью остановился. Аргумент — выпавшее значение. + public event Action OnRollFinished; + + // ─────────────────────────── State ─────────────────────────────── + + /// Идёт ли сейчас бросок. + public bool IsRolling { get; private set; } + + private Coroutine rollRoutine; + + // ─────────────────────────── Unity ─────────────────────────────── + + private void Reset() + { + dice = GetComponent(); + rb = GetComponent(); + } + + // ─────────────────────────── Public API ────────────────────────── + + /// Бросить кубик. Повторный вызов во время броска игнорируется. + public void Roll() + { + if (IsRolling) return; + + if (rollRoutine != null) + StopCoroutine(rollRoutine); + + rollRoutine = StartCoroutine(RollSequence()); + } + + // ─────────────────────────── Core ──────────────────────────────── + + private IEnumerator RollSequence() + { + IsRolling = true; + + // ── 1. Подготовка ──────────────────────────────────────────── + rb.isKinematic = false; + rb.useGravity = true; + rb.linearVelocity = Vector3.zero; + rb.angularVelocity = Vector3.zero; + + // ── 2. Импульс: подбросить вверх с лёгким разбросом ───────── + Vector3 force = new Vector3( + Random.Range(-throwScatter, throwScatter), + throwUpForce, + Random.Range(-throwScatter, throwScatter) + ); + rb.AddForce(force, ForceMode.Impulse); + + // ── 3. Случайный крутящий момент — красивое вращение ──────── + Vector3 torque = Random.onUnitSphere * Random.Range(torqueMin, torqueMax); + rb.AddTorque(torque, ForceMode.Impulse); + + // Даём кубику взлететь + yield return new WaitForSeconds(0.2f); + + // ── 4. Ждём пока кубик успокоится ─────────────────────────── + float stillTimer = 0f; + float sqrThreshold = settleSpeed * settleSpeed; + + while (stillTimer < settleDelay) + { + yield return new WaitForFixedUpdate(); + + bool isSlow = rb.linearVelocity.sqrMagnitude < sqrThreshold + && rb.angularVelocity.sqrMagnitude < sqrThreshold; + + stillTimer = isSlow ? stillTimer + Time.fixedDeltaTime : 0f; + } + + // ── 5. Замораживаем физику и читаем верхнюю грань ─────────── + rb.linearVelocity = Vector3.zero; + rb.angularVelocity = Vector3.zero; + rb.isKinematic = true; + + if (!dice.TryGetTopValue(out int topValue)) + { + Debug.LogWarning("DiceRoller: не удалось определить верхнюю грань."); + IsRolling = false; + yield break; + } + + // ── 6. Плавный доворот до ровного положения ───────────────── + Quaternion startRot = transform.rotation; + + // Вычисляем целевой поворот через Dice.AlignToTopByLocalAngles + dice.AlignToTopByLocalAngles(); + Quaternion targetRot = transform.rotation; + + // Откатываемся обратно — будем интерполировать + transform.rotation = startRot; + + float elapsed = 0f; + while (elapsed < snapDuration) + { + elapsed += Time.deltaTime; + float t = snapCurve.Evaluate(Mathf.Clamp01(elapsed / snapDuration)); + transform.rotation = Quaternion.Slerp(startRot, targetRot, t); + yield return null; + } + transform.rotation = targetRot; + + // ── 7. Готово ─────────────────────────────────────────────── + IsRolling = false; + OnRollFinished?.Invoke(topValue); + + Debug.Log($"🎲 Выпало: {topValue}"); + } +}