A UI Razor component that renders floating damage/heal numbers on the HUD. It tracks active FScreenNumberElement entries, creates or updates them on hit/heal events, computes screen positions and random offsets, and removes expired entries each Tick.
@namespace KOTH.UI
@inherits Panel
@attribute [StyleSheet]
<root>
@foreach (var DamageNumberElement in DamageNumberElements)
{
<label class="damage-number" style="font-size: @(DamageNumberElement.GetSizePx())px; color: @(DamageNumberElement.GetColour().Hex); left: @(DamageNumberElement.GetScreenPos().x * 100)%; top: @(DamageNumberElement.GetScreenPos().y * 100)%; ">
@(DamageNumberElement)
</label>
}
</root>
@code
{
// TODO : put these in a helper somewhere
public Vector2 GetScreenPosFromPosition(Vector3 Position, out bool IsBehind)
{
return LocalPlayerPawn.Camera.PointToScreenNormal(Position, out IsBehind);
}
public Vector2 GetScreenOffset(float Distance = 0f)
{
float DistanceFactor = 0.01f; //0.007f * (float)Math.Sqrt(Distance);
float XOffset = (float)(Random.Shared.NextDouble() * DistanceFactor * 2) - DistanceFactor;
float YOffset = (float)(Random.Shared.NextDouble() * (0.15f - DistanceFactor)) + DistanceFactor;
return new Vector2(XOffset, -YOffset);
}
////////////////////////////////////////////////////////////////////////////////////////
public static DamageNumbers Instance { get; set; }
public DamageNumbers()
{
Instance = this;
}
List<FScreenNumberElement> DamageNumberElements = new();
PlayerPawn LocalPlayerPawn => PlayerState.Local.IsValid() ? PlayerState.Local.PlayerPawn : null;
public void OnHit(FDamageTaken DamageGiven)
{
if (!LocalPlayerPawn.IsValid() || !LocalPlayerPawn.IsAlive || DamageGiven.AssumedVictimPlayerPawn == LocalPlayerPawn)
{
return;
}
var Distance = DamageGiven.DamageLocation.Distance(LocalPlayerPawn.WorldPosition);
var DamageLocation = DamageGiven.DamageType == EDamageType.Projectile && DamageGiven.VictimGameObject.IsValid() ? DamageGiven.VictimGameObject.WorldPosition : DamageGiven.DamageLocation;
foreach (var Element in DamageNumberElements)
{
if (Element.DisplayedTime < -0.1f)
{
continue;
}
if (Element.TargetPawn == DamageGiven.AssumedVictimPlayerPawn)
{
Element.AddDamage(DamageGiven.Damage);
Element.DisplayedTime = 0;
Element.Location = DamageLocation;
return; // NOTE : early return
}
}
// if we haven't return'd then make a new element
FScreenNumberElement DamageNumberElement = new()
{
PlayerPawn = LocalPlayerPawn,
TargetPawn = DamageGiven.AssumedVictimPlayerPawn,
ScreenOffset = GetScreenOffset(Distance),
Damage = DamageGiven.Damage,
DisplayedTime = 0,
Location = DamageLocation,
};
DamageNumberElements.Add(DamageNumberElement);
}
public void OnHit(FHealingReceived HealingReceived)
{
if (!LocalPlayerPawn.IsValid() || !LocalPlayerPawn.IsAlive
|| HealingReceived.TargetPlayerPawn == LocalPlayerPawn || !HealingReceived.TargetPlayerPawn.IsValid()
|| HealingReceived.HealingType == EHealingType.Continuous || HealingReceived.TargetPlayerPawn.Team != LocalPlayerPawn.Team)
{
return;
}
var Distance = HealingReceived.TargetPlayerPawn.WorldPosition.Distance(LocalPlayerPawn.WorldPosition);
var DamageLocation = HealingReceived.TargetPlayerPawn.WorldPosition;
foreach (var Element in DamageNumberElements)
{
if (Element.DisplayedTime < -0.1f)
{
continue;
}
if (Element.TargetPawn == HealingReceived.TargetPlayerPawn)
{
Element.AddDamage(HealingReceived.Heals);
Element.DisplayedTime = 0;
Element.Location = DamageLocation;
return; // NOTE : early return
}
}
// if we haven't return'd then make a new element
FScreenNumberElement DamageNumberElement = new()
{
PlayerPawn = LocalPlayerPawn,
TargetPawn = HealingReceived.TargetPlayerPawn,
ScreenOffset = GetScreenOffset(Distance),
Damage = HealingReceived.Heals,
DisplayedTime = 0,
Location = DamageLocation,
IsDamage = false,
};
DamageNumberElements.Add(DamageNumberElement);
}
public override void Tick()
{
List<FScreenNumberElement> DamageNumbersToRemove = new();
foreach (var DamageNumber in DamageNumberElements)
{
if (DamageNumber.DisplayedTime < -0.66f)
{
DamageNumbersToRemove.Add(DamageNumber);
}
}
foreach (var Remove in DamageNumbersToRemove)
{
DamageNumberElements.Remove(Remove);
}
}
protected override int BuildHash()
{
return HashCode.Combine(Time.Now);
}
}