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}");
+ }
+}