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.
@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);
}
}