Game/Utils.cs

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.

NetworkingFile AccessProcess ExecutionNative InteropObfuscated CodeEncoded DataExternal DownloadCredential AccessSelf Modifying CodeHttp CallsReflection
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;
		}
	}
}