Player/PlayerDamageIndicators.cs
public sealed class PlayerDamageIndicators : Component, Local.IPlayerEvents
{
	[RequireComponent] Player Player { get; set; }

	float RadialDistanceFromCenter => 128f;
	float RadialIndicatorLifetime => 2f;

	List<(Vector3 WorldPos, TimeSince Lifetime)> radialIndicators = new();

	[Property] public Texture RadialDamageIcon { get; set; }

	protected override void OnPreRender()
	{
		if ( !Player.IsLocalPlayer ) return;
		if ( Scene.Camera is null ) return;

		UpdateRadialIndicators();
	}

	void UpdateRadialIndicators()
	{
		if ( RadialDamageIcon is null )
			return;

		var hud = Scene.Camera.Hud;
		var playerPos = Player.EyeTransform.Position;
		var playerRot = Player.EyeTransform.Rotation;
		var center = Screen.Size / 2f;

		// rough approx of where the crosshair is in worldspace, makes close-up directions more easily parsable/accurate
		var focalPoint = playerPos + playerRot.Forward * 16;

		for ( int i = radialIndicators.Count - 1; i >= 0; i-- )
		{
			var entry = radialIndicators[i];
			if ( entry.Lifetime >= RadialIndicatorLifetime )
			{
				radialIndicators.RemoveAt( i );
				continue;
			}

			var dir = (entry.WorldPos - focalPoint).Normal;
			var angle = -MathF.Atan2( dir.y, dir.x ) + playerRot.Angles().yaw.DegreeToRadian() - (MathF.PI / 2f);

			Matrix matrix = Matrix.CreateRotation( Rotation.From( 0, angle.RadianToDegree(), 0 ) );
			matrix *= Matrix.CreateTranslation( center );
			hud.SetMatrix( matrix );

			var size = new Vector2( 256, 512 ) * Hud.Scale;
			var rect = new Rect( new Vector2( RadialDistanceFromCenter * Hud.Scale, -size.y / 2 ), size );

			// scale alpha based on damage dealt or something?
			hud.DrawTexture( RadialDamageIcon, rect, Color.Red.WithAlpha( 1f - (entry.Lifetime / RadialIndicatorLifetime) ) );
		}

		hud.SetMatrix( Matrix.Identity );
	}

	void Local.IPlayerEvents.OnDamage( PlayerDamageParams args )
	{
		if ( !args.Attacker.IsValid() ) return;
		
		radialIndicators.Add( (args.Attacker.WorldPosition, 0f) );
	}
}