172 lines
5.6 KiB
C#
172 lines
5.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace YachtDice.Dice
|
|
{
|
|
public class Dice : MonoBehaviour
|
|
{
|
|
[Serializable]
|
|
public struct Entry : IEquatable<Entry>
|
|
{
|
|
[field: SerializeField] public int Value { get; private set; }
|
|
[field: SerializeField] public Transform Point { get; private set; }
|
|
|
|
public bool Equals(Entry other) => Point == other.Point;
|
|
public override bool Equals(object obj) => obj is Entry other && Equals(other);
|
|
public override int GetHashCode() => Point != null ? Point.GetHashCode() : 0;
|
|
}
|
|
|
|
[SerializeField] private List<Entry> entries = new();
|
|
|
|
private HashSet<Entry> _entrySet;
|
|
|
|
private void Awake() => RebuildSet();
|
|
private void OnValidate() => RebuildSet();
|
|
|
|
private void RebuildSet()
|
|
{
|
|
_entrySet = new HashSet<Entry>();
|
|
|
|
if (entries == null) return;
|
|
|
|
foreach (var t in entries)
|
|
_entrySet.Add(t);
|
|
}
|
|
|
|
public bool TryGetTopValue(out int value) => TryGetExtremeValue(isTop: true, out value);
|
|
public bool TryGetBottomValue(out int value) => TryGetExtremeValue(isTop: false, out value);
|
|
|
|
public int AlignToTopByLocalAngles()
|
|
{
|
|
var e = GetExtremeEntryByWorldY(isTop: true);
|
|
AlignByFaceLocalAngles(e.Point);
|
|
return e.Value;
|
|
}
|
|
|
|
public Quaternion GetClosestTopAlignedWorldRotation(Quaternion currentWorldRotation)
|
|
{
|
|
var e = GetExtremeEntryByWorldY(isTop: true);
|
|
return GetClosestAlignedWorldRotation(e.Point, currentWorldRotation);
|
|
}
|
|
|
|
public int AlignToBottomByLocalAngles()
|
|
{
|
|
var e = GetExtremeEntryByWorldY(isTop: false);
|
|
AlignByFaceLocalAngles(e.Point);
|
|
return e.Value;
|
|
}
|
|
|
|
private bool TryGetExtremeValue(bool isTop, out int value)
|
|
{
|
|
value = default;
|
|
|
|
if (_entrySet == null || _entrySet.Count == 0)
|
|
return false;
|
|
|
|
var found = false;
|
|
var bestY = isTop ? float.NegativeInfinity : float.PositiveInfinity;
|
|
int best = default;
|
|
|
|
foreach (var e in _entrySet)
|
|
{
|
|
var p = e.Point;
|
|
if (!p) continue;
|
|
|
|
float y = p.position.y;
|
|
if (!found || (isTop ? y > bestY : y < bestY))
|
|
{
|
|
found = true;
|
|
bestY = y;
|
|
best = e.Value;
|
|
}
|
|
}
|
|
|
|
if (!found) return false;
|
|
|
|
value = best;
|
|
return true;
|
|
}
|
|
|
|
private Quaternion GetClosestAlignedWorldRotation(Transform facePoint, Quaternion currentWorldRotation)
|
|
{
|
|
if (!facePoint) throw new InvalidOperationException("Dice: facePoint == null.");
|
|
|
|
var baseWorldRotation = GetAlignedWorldRotation(facePoint);
|
|
var bestWorldRotation = baseWorldRotation;
|
|
var smallestAngle = Quaternion.Angle(currentWorldRotation, baseWorldRotation);
|
|
|
|
for (var i = 1; i < 4; i++)
|
|
{
|
|
var candidateWorldRotation = Quaternion.AngleAxis(90f * i, Vector3.up) * baseWorldRotation;
|
|
var candidateAngle = Quaternion.Angle(currentWorldRotation, candidateWorldRotation);
|
|
|
|
if (candidateAngle >= smallestAngle) continue;
|
|
|
|
smallestAngle = candidateAngle;
|
|
bestWorldRotation = candidateWorldRotation;
|
|
}
|
|
|
|
return bestWorldRotation;
|
|
}
|
|
|
|
private Entry GetExtremeEntryByWorldY(bool isTop)
|
|
{
|
|
if (_entrySet == null || _entrySet.Count == 0)
|
|
throw new InvalidOperationException("Dice: коллекция пуста.");
|
|
|
|
var found = false;
|
|
var bestY = isTop ? float.NegativeInfinity : float.PositiveInfinity;
|
|
Entry best = default;
|
|
|
|
foreach (var e in _entrySet)
|
|
{
|
|
var p = e.Point;
|
|
|
|
if (!p) continue;
|
|
|
|
var y = p.position.y;
|
|
|
|
if (found && (isTop ? !(y > bestY) : !(y < bestY))) continue;
|
|
|
|
found = true;
|
|
bestY = y;
|
|
best = e;
|
|
}
|
|
|
|
if (!found)
|
|
throw new InvalidOperationException("Dice: все Transform == null.");
|
|
|
|
return best;
|
|
}
|
|
|
|
private static float Norm180(float a)
|
|
{
|
|
a %= 360f;
|
|
return a > 180f ? a - 360f : (a < -180f ? a + 360f : a);
|
|
}
|
|
|
|
private Quaternion GetAlignedWorldRotation(Transform facePoint)
|
|
{
|
|
var parentRotation = transform.parent != null ? transform.parent.rotation : Quaternion.identity;
|
|
return parentRotation * GetAlignedLocalRotation(facePoint);
|
|
}
|
|
|
|
private static Quaternion GetAlignedLocalRotation(Transform facePoint)
|
|
{
|
|
if (!facePoint) throw new InvalidOperationException("Dice: facePoint == null.");
|
|
return Quaternion.Inverse(facePoint.localRotation);
|
|
}
|
|
|
|
private void AlignByFaceLocalAngles(Transform facePoint)
|
|
{
|
|
if (!facePoint) throw new InvalidOperationException("Dice: facePoint == null.");
|
|
|
|
transform.localRotation = GetAlignedLocalRotation(facePoint);
|
|
|
|
var e = transform.localEulerAngles;
|
|
transform.localEulerAngles = new Vector3(Norm180(e.x), Norm180(e.y), Norm180(e.z));
|
|
}
|
|
}
|
|
}
|