Code/HitShapes/Internal/RadialHitShape.cs
using System;
using Sandbox;
namespace HitShapes;
internal sealed class RadialHitShape : IHitShape, IGeometricHitShape
{
readonly int _slots;
readonly float _innerRatio;
readonly float _outerRatio;
readonly float _startRad; // start offset in radians (0 = baseline / slot 0 at 12 o'clock)
readonly float _wedgeSize;
readonly float _halfWedge;
public RadialHitShape(int slots, float innerRatio, float outerRatio, float startAngleDeg = 0f)
{
_slots = slots;
_innerRatio = innerRatio;
_outerRatio = outerRatio;
_startRad = startAngleDeg * MathF.PI / 180f;
_wedgeSize = MathF.Tau / slots;
_halfWedge = _wedgeSize * 0.5f;
}
public int SlotCount => _slots;
public int? Resolve(Vector2 local, Vector2 size)
{
return ResolveWithGeometry(local, size, out _, out _);
}
public int? ResolveWithGeometry(Vector2 local, Vector2 size, out float? angleDeg, out float? distanceNormalized)
{
var dx = local.x - size.x * 0.5f;
var dy = local.y - size.y * 0.5f;
var dist2 = dx * dx + dy * dy;
var radius = MathF.Min(size.x, size.y) * 0.5f;
var outerR = radius * _outerRatio;
var innerR = radius * _innerRatio;
if (dist2 < innerR * innerR || dist2 > outerR * outerR)
{
angleDeg = null;
distanceNormalized = null;
return null;
}
// Angle shifted by _startRad and half-wedge so slot 0 is centered on the start direction.
var angle = MathF.Atan2(dy, dx) + MathF.PI * 0.5f + _halfWedge - _startRad;
while (angle < 0) angle += MathF.Tau;
while (angle >= MathF.Tau) angle -= MathF.Tau;
// rawAngle is in the wheel's absolute frame; only slot math shifts by _startRad.
var rawAngle = MathF.Atan2(dy, dx) + MathF.PI * 0.5f;
if (rawAngle < 0) rawAngle += MathF.Tau;
if (rawAngle >= MathF.Tau) rawAngle -= MathF.Tau;
angleDeg = rawAngle * (180f / MathF.PI);
distanceNormalized = radius > 0 ? MathF.Sqrt(dist2) / radius : 0f;
return (int)(angle / _wedgeSize) % _slots;
}
}