using System; using System.Collections; using UnityEngine; using Random = UnityEngine.Random; namespace YachtDice.Dice { /// /// Красиво подбрасывает и раскручивает кубик, ждёт пока он остановится, /// плавно доворачивает до ровного положения, возвращает на старт и сообщает результат. /// public class DiceRoller : MonoBehaviour { private static DiceRoller _activeReturnRoller; [Header("References")] [SerializeField] private Dice dice; [SerializeField] private Rigidbody rb; [SerializeField] private Collider diceCollider; [Tooltip("Определение типа дайса")] [field: SerializeField] public DiceDefinition Definition { get; private set; } [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; [Tooltip("Максимальная длительность броска до принудительного завершения")] [SerializeField] private float maxRollDuration = 3f; [Header("Snap Alignment")] [Tooltip("Длительность плавного доворота к ровному положению")] [SerializeField] private float snapDuration = 0.35f; [SerializeField] private AnimationCurve snapCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f); [Header("Return To Start")] [Tooltip("Длительность возврата в стартовую позицию")] [SerializeField] private float returnDuration = 0.4f; [Tooltip("Высота дуги при возврате в стартовую позицию")] [SerializeField] private float returnArcHeight = 0.6f; [SerializeField] private AnimationCurve returnCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f); /// /// Вызывается когда кубик полностью остановился. Аргумент — выпавшее значение. /// public event Action OnRollFinished; /// /// Идёт ли сейчас бросок. /// public bool IsRolling { get; private set; } private Coroutine _rollRoutine; private Vector3 _startLocalPosition; private bool _startPoseCaptured; private void Awake() { if (dice == null) dice = GetComponent(); if (rb == null) rb = GetComponent(); if (diceCollider == null) diceCollider = GetComponent(); CaptureStartPose(); } private void Reset() { dice = GetComponent(); rb = GetComponent(); diceCollider = GetComponent(); } private void CaptureStartPose() { if (_startPoseCaptured) return; _startLocalPosition = transform.localPosition; _startPoseCaptured = true; } /// /// Бросить кубик. Повторный вызов во время броска игнорируется. /// public void Roll() { if (IsRolling) return; if (dice == null || rb == null) { Debug.LogError("DiceRoller: отсутствуют обязательные компоненты Dice/Rigidbody.", this); return; } if (_rollRoutine != null) StopCoroutine(_rollRoutine); _rollRoutine = StartCoroutine(RollSequence()); } private IEnumerator RollSequence() { IsRolling = true; CaptureStartPose(); // ── 1. Подготовка ──────────────────────────────────────────── if (diceCollider != null) diceCollider.enabled = true; rb.isKinematic = false; rb.useGravity = true; rb.linearVelocity = Vector3.zero; rb.angularVelocity = Vector3.zero; var rollStartedAt = Time.time; // ── 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. Ждём пока кубик успокоится ─────────────────────────── var stillTimer = 0f; var sqrThreshold = settleSpeed * settleSpeed; var maxDuration = Mathf.Max(0.1f, maxRollDuration); while (stillTimer < settleDelay) { if (Time.time - rollStartedAt >= maxDuration) { break; } 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: не удалось определить верхнюю грань."); topValue = 1; } // ── 6. Плавный доворот до ровного положения ───────────────── Quaternion startRot = transform.rotation; Quaternion targetRot = dice.GetClosestTopAlignedWorldRotation(startRot); var elapsed = 0f; var snapTime = Mathf.Max(0.01f, snapDuration); var snapCurveToUse = snapCurve ?? AnimationCurve.Linear(0f, 0f, 1f, 1f); while (elapsed < snapTime) { elapsed += Time.deltaTime; float t = snapCurveToUse.Evaluate(Mathf.Clamp01(elapsed / snapTime)); transform.rotation = Quaternion.Slerp(startRot, targetRot, t); yield return null; } transform.rotation = targetRot; // ── 7. Возвращаемся в стартовую позицию без коллизий ─────── if (diceCollider != null) diceCollider.enabled = false; yield return ReturnToStartPosition(); if (diceCollider != null) diceCollider.enabled = true; // ── 8. Готово ─────────────────────────────────────────────── IsRolling = false; OnRollFinished?.Invoke(topValue); // Debug.Log($"{gameObject.name} | Выпало: {topValue}"); } private IEnumerator ReturnToStartPosition() { var startPos = transform.localPosition; var duration = Mathf.Max(0.01f, returnDuration); yield return WaitForReturnTurn(); try { var elapsed = 0f; var returnCurveToUse = returnCurve ?? AnimationCurve.Linear(0f, 0f, 1f, 1f); while (elapsed < duration) { elapsed += Time.deltaTime; float normalized = Mathf.Clamp01(elapsed / duration); float curved = returnCurveToUse.Evaluate(normalized); var pos = Vector3.Lerp(startPos, _startLocalPosition, curved); pos.y += Mathf.Sin(normalized * Mathf.PI) * returnArcHeight; transform.localPosition = pos; yield return null; } transform.localPosition = _startLocalPosition; } finally { if (_activeReturnRoller == this) _activeReturnRoller = null; } } private IEnumerator WaitForReturnTurn() { while (_activeReturnRoller != null && _activeReturnRoller != this) yield return null; _activeReturnRoller = this; } } }