Core/GameInput.cs

Static input helper for the game. Maps named input actions (WASD, arrows, numpad) to Direction intents, provides methods to read newly pressed turn intents into a caller-supplied array, and exposes simple boolean checks for Confirm, Pause, Restart, and any-direction presses.

namespace Coilgarden;

/// <summary>
/// Turns key presses into direction intents, and nothing else. The layer boundary the
/// whole project rests on is that the simulation never reads a keyboard - so this class
/// hands out intents and has no idea what is done with them.
/// <para>
/// Every heading has three interchangeable bindings: WASD, the arrow cluster and the
/// numpad. Nothing downstream can tell which one produced an intent, so a player can swap
/// hands mid-run. The bindings live in <c>ProjectSettings/Input.config</c> as named
/// actions rather than as raw scancodes here, which is what will make them rebindable
/// later without touching this file.
/// </para>
/// </summary>
public static class GameInput
{
	private static readonly (Direction Direction, string[] Actions)[] Bindings =
	{
		(Direction.Up, new[] { "Forward", "ArrowUp", "NumpadUp" }),
		(Direction.Right, new[] { "Right", "ArrowRight", "NumpadRight" }),
		(Direction.Down, new[] { "Backward", "ArrowDown", "NumpadDown" }),
		(Direction.Left, new[] { "Left", "ArrowLeft", "NumpadLeft" })
	};

	/// <summary>How many intents a single frame can possibly produce.</summary>
	public const int MaxIntentsPerFrame = 4;

	/// <summary>
	/// Writes the headings newly pressed this frame into <paramref name="into"/> and
	/// returns how many were written.
	/// <para>
	/// Presses are reported rather than held keys, and all of them rather than the first,
	/// because the snake buffers turns: on a frame where a player rounds a corner with two
	/// quick taps, dropping one of them is the difference between a turn that works and one
	/// that mysteriously does not.
	/// </para>
	/// </summary>
	public static int ReadTurns( Direction[] into )
	{
		if ( into is null ) return 0;

		var count = 0;

		foreach ( var binding in Bindings )
		{
			if ( count >= into.Length ) break;
			if ( !AnyPressed( binding.Actions ) ) continue;

			into[count++] = binding.Direction;
		}

		return count;
	}

	/// <summary>Start a waiting run, or restart a finished one.</summary>
	public static bool ConfirmPressed() => Input.Pressed( "Confirm" );

	public static bool PausePressed() => Input.Pressed( "Pause" );

	public static bool RestartPressed() => Input.Pressed( "Restart" );

	/// <summary>
	/// Any direction key at all - so a player who never reads instructions still starts the
	/// game by pressing the key they were always going to press first.
	/// </summary>
	public static bool AnyDirectionPressed()
	{
		foreach ( var binding in Bindings )
		{
			if ( AnyPressed( binding.Actions ) ) return true;
		}

		return false;
	}

	private static bool AnyPressed( string[] actions )
	{
		foreach ( var action in actions )
		{
			if ( Input.Pressed( action ) ) return true;
		}

		return false;
	}
}