UI/PlayerHud/Components/DamageIndicatorNew.razor

A Razor UI component for the player HUD that draws damage direction indicators as triangle images. It tracks recent hits in a list of FDamageTri structs, computes rotation and screen offsets from hit world positions relative to the local player, and fades indicators over time.

File Access
🐞 DamageTris grows without bound because entries are never removed, so each hit adds a permanent entry and the UI keeps rendering them.
@namespace KOTH.UI
@inherits Panel
@attribute [StyleSheet]

<root>
	@foreach (var DamageTri in DamageTris)
	{
		<img class="damage-tri" src="/ui/koth/damage-triangle.png" style="opacity: @DamageTri.GetOpacity(); transform: translate(@(-50 + DamageTri.TopRightOffset.x)%, @(-55 - DamageTri.TopRightOffset.y)%) rotate(@(DamageTri.Rotation)deg);" />
	}
</root>

@code
{
	public struct FDamageTri
	{
		public FDamageTri()
		{
			TimeSince = 0;
		}

		public PlayerPawn LocalPlayerPawn { get; init; } = null;
		public Vector3 HitLocation { get; init; } = Vector3.Zero;
		public float Rotation { get => GetRotationOffset().Rotation; }
		public Vector2 TopRightOffset { get => GetRotationOffset().TopRightOffset; }
		public TimeSince TimeSince { get; init; } = new();

		public (float Rotation, Vector2 TopRightOffset) GetRotationOffset()
		{
			if (!LocalPlayerPawn.IsValid())
			{
				return (0, Vector2.Zero);
			}

			// ---------------
			// rotation
			var PlayerToHit = HitLocation - LocalPlayerPawn.WorldPosition;
			var PlayerToHit2d = new Vector2(PlayerToHit.x, PlayerToHit.y).Normal;

			var PlayerForward = LocalPlayerPawn.AimRay.Forward.Normal;

			var Angle1 = Math.Atan2(PlayerForward.y, PlayerForward.x);
			var Angle2 = Math.Atan2(PlayerToHit.y, PlayerToHit.x);

			var AngleR = Angle1 - Angle2;
			var Angle = AngleR * (180.0 / Math.PI);
			var Rotation = (float)Angle;

			// ---------------
			// position
			var TopRight = new Vector2((float)Math.Sin(AngleR) * 300f, (float)Math.Cos(AngleR) * 300f);

			return (Rotation, TopRight);
		}

		public float GetOpacity()
		{
			// TODO : scale with damage
			return MathX.Lerp(0.7f, 0f, TimeSince / 1f);
		}
	}

	public float Rotation = 0;
	public Vector2 TopRight = new(1, 0);

	public Vector2 GetScreenPosFromPosition(Vector3 InPosition, out bool IsBehind)
	{
		var Position = LocalPlayerPawn.Camera.PointToScreenNormal(InPosition, out IsBehind);
		return Position;
	}

	public static DamageIndicatorNew Instance { get; set; }

	public DamageIndicatorNew()
	{
		Instance = this;
	}

	public float GetOpacity()
	{
		return MathX.Lerp(0.7f, 0f, TimeSinceLastHit / 2f);
	}

	PlayerPawn LocalPlayerPawn => PlayerState.Local.IsValid() ? PlayerState.Local.PlayerPawn : null;

	public List<FDamageTri> DamageTris = new();

	TimeSince TimeSinceLastHit = new();
	public void OnHit(Vector3 DamageLocation)
	{
		TimeSinceLastHit = 0;

		if (!LocalPlayerPawn.IsValid())
		{
			return;
		}

		// ---------------
		// rotation
		var PlayerToHit = DamageLocation - LocalPlayerPawn.WorldPosition;
		var PlayerToHit2d = new Vector2(PlayerToHit.x, PlayerToHit.y).Normal;

		var PlayerForward = LocalPlayerPawn.AimRay.Forward.Normal;

		var Angle1 = Math.Atan2(PlayerForward.y, PlayerForward.x);
		var Angle2 = Math.Atan2(PlayerToHit.y, PlayerToHit.x);

		var AngleR = Angle1 - Angle2;
		var Angle = AngleR * (180.0 / Math.PI);
		Rotation = (float)Angle;

		// ---------------
		// position
		TopRight = new((float)Math.Sin(AngleR) * 300f, (float)Math.Cos(AngleR) * 300f);

		DamageTris.Add(new() { HitLocation = DamageLocation, LocalPlayerPawn = LocalPlayerPawn });
		// TODO : currently leaking memory, remove em
	}

	protected override int BuildHash()
	{
		return HashCode.Combine(Time.Now);
	}
}