UI/ArenaView.cs

UI panel for the arena fight. Draws floating damage and miss popups over each gladiator's head, placing them in stage coordinates from World3D and fading them out by age.

using System;
using Sandbox.UI;
using Sandbox.UI.Construct;

/// <summary>
/// The fight's UI layer: the damage / miss popups over the fighters' heads. The fight itself is drawn in
/// the 3D dungeon or arena (<see cref="World3D"/>).
/// </summary>
public sealed class ArenaView : Panel
{
	/// <summary>Stage pixels per puppet pixel (fight distances and lifts are in stage pixels).</summary>
	public const float BaseScale = 0.78f;

	/// <summary>A fighter position (DisplayX, height above the ground in stage pixels) on the 1920x1080 stage.</summary>
	public static Vector2 ToStage( float displayX, float lift )
	{
		var w = World3D.Instance;
		if ( w.IsValid() ) return w.ToStage( w.FighterWorld( displayX, lift ) );
		return new Vector2( 960f + displayX, 965f - lift );
	}

	readonly Panel popupLayer;
	readonly Dictionary<Gladiator.Popup, Panel> popups = new();

	public ArenaView()
	{
		AddClass( "arena-view" );
		popupLayer = Add.Panel( "popup-layer" );
	}

	public override void Tick()
	{
		base.Tick();

		var fight = Scene?.Get<ArenaFight>();
		var l = fight?.Left;
		var r = fight?.Right;

		CleanPopups( l, r );
		UpdatePopups( l );
		UpdatePopups( r );
	}

	void CleanPopups( Gladiator l, Gladiator r )
	{
		bool Alive( Gladiator.Popup p ) => (l.IsValid() && l.Popups.Contains( p )) || (r.IsValid() && r.Popups.Contains( p ));

		foreach ( var dead in popups.Keys.Where( p => !Alive( p ) ).ToList() )
		{
			popups[dead].Delete();
			popups.Remove( dead );
		}
	}

	void UpdatePopups( Gladiator g )
	{
		if ( !g.IsValid() || float.IsNaN( g.DisplayX ) ) return;

		foreach ( var p in g.Popups )
		{
			if ( !popups.TryGetValue( p, out var panel ) )
			{
				panel = popupLayer.Add.Panel( "popup" );
				panel.AddClass( p.Kind.ToString().ToLower() );
				var label = panel.Add.Label( p.Text, "popup-text" );
				if ( p.Colour is not null ) label.Style.FontColor = Color.Parse( p.Colour ) ?? Color.White;
				popups[p] = panel;
			}

			float age = p.Age;
			var head = ToStage( g.DisplayX, g.Lift + 470f * BaseScale );
			panel.Style.Left = head.x + p.OffsetX;
			panel.Style.Top = head.y - age * 70f;
			panel.Style.Opacity = Math.Clamp( 1.6f - age, 0f, 1f );
		}
	}
}