Utility helpers for the game. Provides value mapping with multiple easing functions, angle/vector math, random helpers using a deterministic Rng, and an extension method to shuffle lists.
using System;
using System.Collections.Generic;
namespace BlockParty;
public enum EasingType
{
Linear,
ExpoEaseIn, ExpoEaseOut, ExpoEaseInOut,
CubicEaseIn, CubicEaseOut, CubicEaseInOut,
SineEaseIn, SineEaseOut, SineEaseInOut
}
/// <summary>
/// Port of the original <c>Utils</c> manager: value mapping with easing, plus a few
/// vector/angle helpers. Vector helpers use sbox's <see cref="Vector2"/>
/// (lower-case x/y). RNG is routed through <see cref="Rng"/> for determinism.
/// </summary>
public static class Utils
{
public static float Map( float value, float inputMin, float inputMax, float outputMin, float outputMax, bool clamp )
{
return Map( value, inputMin, inputMax, outputMin, outputMax, clamp, EasingType.Linear );
}
public static float Map( float value, float inputMin, float inputMax, float outputMin, float outputMax, bool clamp, EasingType easingType )
{
float ratio = (value - inputMin) / (inputMax - inputMin);
// Clamp BEFORE easing: the sine easings are periodic, so an out-of-range ratio oscillates
// (SineEaseIn hits 0 again at ratio 4 — a 400px/s landing was silent) and the output clamp
// below can't undo that. The monotonic easings are unaffected.
if ( clamp )
ratio = Math.Clamp( ratio, 0.0f, 1.0f );
switch ( easingType )
{
case EasingType.ExpoEaseIn: ratio = ExpoEaseIn( ratio ); break;
case EasingType.ExpoEaseOut: ratio = ExpoEaseOut( ratio ); break;
case EasingType.ExpoEaseInOut: ratio = ExpoEaseInOut( ratio ); break;
case EasingType.CubicEaseIn: ratio = CubicEaseIn( ratio ); break;
case EasingType.CubicEaseOut: ratio = CubicEaseOut( ratio ); break;
case EasingType.CubicEaseInOut: ratio = CubicEaseInOut( ratio ); break;
case EasingType.SineEaseIn: ratio = SineEaseIn( ratio ); break;
case EasingType.SineEaseOut: ratio = SineEaseOut( ratio ); break;
case EasingType.SineEaseInOut: ratio = SineEaseInOut( ratio ); break;
default: break;
}
float outVal = outputMin + (ratio * (outputMax - outputMin));
if ( clamp )
{
if ( outputMax < outputMin )
{
if ( outVal < outputMax ) outVal = outputMax;
else if ( outVal > outputMin ) outVal = outputMin;
}
else
{
if ( outVal > outputMax ) outVal = outputMax;
else if ( outVal < outputMin ) outVal = outputMin;
}
}
return outVal;
}
static float ExpoEaseIn( float t ) { return MathF.Pow( 2.0f, 10.0f * (t - 1.0f) ); }
static float ExpoEaseOut( float t ) { return 1.0f - MathF.Pow( 2.0f, -10.0f * t ); }
static float ExpoEaseInOut( float t ) { return (t < 0.5f) ? ExpoEaseIn( t * 2.0f ) * 0.5f : 1 - ExpoEaseIn( 2.0f - t * 2.0f ) * 0.5f; }
static float CubicEaseIn( float t ) { return t * t * t; }
static float CubicEaseOut( float t ) { return 1.0f - CubicEaseIn( 1.0f - t ); }
static float CubicEaseInOut( float t ) { return (t < 0.5f) ? CubicEaseIn( t * 2.0f ) * 0.5f : 1.0f - CubicEaseIn( 2.0f - t * 2.0f ) * 0.5f; }
static float SineEaseIn( float t ) { return 1.0f - MathF.Cos( t * MathF.PI * 0.5f ); }
static float SineEaseOut( float t ) { return 1.0f - SineEaseIn( 1.0f - t ); }
static float SineEaseInOut( float t ) { return (t < 0.5f) ? SineEaseIn( t * 2.0f ) * 0.5f : 1.0f - SineEaseIn( 2.0f - t * 2.0f ) * 0.5f; }
public static Vector2 GetVectorFromRotation( float degrees )
{
float x = MathF.Sin( -Deg2Rad( degrees ) );
float y = MathF.Cos( Deg2Rad( degrees ) );
return new Vector2( x, y );
}
public static float GetRotationFromVector( Vector2 vector )
{
return Rad2Deg( (MathF.Atan2( vector.y, vector.x ) - MathF.Atan2( 1.0f, 0.0f )) );
}
public static float Deg2Rad( float angle ) { return MathF.PI * angle / 180.0f; }
public static float Rad2Deg( float angle ) { return angle * (180.0f / MathF.PI); }
/// <summary>Rotates a vector CCW by the given degrees, preserving magnitude.</summary>
public static Vector2 RotateVector( Vector2 vec, float degrees )
{
float r = Deg2Rad( degrees );
float c = MathF.Cos( r );
float s = MathF.Sin( r );
return new Vector2( vec.x * c - vec.y * s, vec.x * s + vec.y * c );
}
public static Vector2 RandomVector()
{
float azimuth = Rng.Value() * 2f * MathF.PI;
return new Vector2( MathF.Cos( azimuth ), MathF.Sin( azimuth ) );
}
public static float RandomSign() { return Rng.Int( 0, 2 ) == 0 ? 1f : -1f; }
/// <summary>Safe normalize (returns zero for a zero-length vector).</summary>
public static Vector2 Normalized( Vector2 v )
{
float l = v.Length;
return l > 0.0001f ? v / l : Vector2.Zero;
}
}
public static class Extensions
{
/// <summary>Fisher-Yates shuffle routed through the deterministic <see cref="Rng"/>.</summary>
public static void Shuffle<T>( this IList<T> list )
{
int n = list.Count;
while ( n > 1 )
{
n--;
int k = Rng.Int( 0, n + 1 );
T value = list[k];
list[k] = list[n];
list[n] = value;
}
}
}