0f9b162061
- Add abstract dice system (IDie interface, DieDefinitionSO, StandardDieSO, DieInstance) to support future custom dice types while keeping backward compat via int[] DiceValues - Replace YachtCategory enum and CategoryScorer switch with CategoryDefinitionSO hierarchy: SumOfValueCategorySO, NOfAKindCategorySO, FullHouseCategorySO, StraightCategorySO, SumAllCategorySO - Add CategoryCatalogSO for ordered category collections and DiceCheckUtility for shared logic - Refactor ScoringSystem, Views, GameManager, GameController to use SO references - Update CategoryCondition modifier to use SO reference instead of enum - Update all editor tests to use SO-based categories and DieInstance Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
160 lines
6.6 KiB
C#
160 lines
6.6 KiB
C#
using System;
|
|
using System.Collections;
|
|
using UnityEngine;
|
|
using Random = UnityEngine.Random;
|
|
|
|
namespace YachtDice.Dice
|
|
{
|
|
/// <summary>
|
|
/// Красиво подбрасывает и раскручивает кубик, ждёт пока он остановится,
|
|
/// плавно доворачивает до ровного положения и сообщает результат.
|
|
/// </summary>
|
|
public class DiceRoller : MonoBehaviour
|
|
{
|
|
[Header("References")]
|
|
[SerializeField] private Dice dice;
|
|
[SerializeField] private Rigidbody rb;
|
|
[SerializeField] private DieDefinitionSO definition;
|
|
|
|
/// <summary>Определение типа дайса (назначается в инспекторе).</summary>
|
|
public DieDefinitionSO Definition => definition;
|
|
|
|
[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);
|
|
|
|
/// <summary>
|
|
/// Вызывается когда кубик полностью остановился. Аргумент — выпавшее значение.
|
|
/// </summary>
|
|
public event Action<int> OnRollFinished;
|
|
|
|
/// <summary>
|
|
/// Идёт ли сейчас бросок.
|
|
/// </summary>
|
|
public bool IsRolling { get; private set; }
|
|
|
|
private Coroutine _rollRoutine;
|
|
|
|
private void Reset()
|
|
{
|
|
dice = GetComponent<Dice>();
|
|
rb = GetComponent<Rigidbody>();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Бросить кубик. Повторный вызов во время броска игнорируется.
|
|
/// </summary>
|
|
public void Roll()
|
|
{
|
|
if (IsRolling) return;
|
|
|
|
if (_rollRoutine != null)
|
|
StopCoroutine(_rollRoutine);
|
|
|
|
_rollRoutine = StartCoroutine(RollSequence());
|
|
}
|
|
|
|
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($"{gameObject.name} | Выпало: <b>{topValue}</b>");
|
|
}
|
|
}
|
|
}
|