UI/KillFeed.razor
@using System;
@using System.Collections.Generic;
@using System.Globalization;
@using System.Linq;
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace Sandbox

<root class="@(IsWorldPanel ? "full" : "")">
	<div class="feed">
		@foreach ( var entry in VisibleEntries )
		{
			var opacity = EntryOpacity( entry ).ToString( "0.###", CultureInfo.InvariantCulture );
			<div class="entry" style="opacity: @opacity;">
				@if ( !entry.IsSuicide )
				{
					<label class="name @(IsLocal( entry.KillerId ) ? "local" : "")">@entry.KillerName</label>
					@if ( !string.IsNullOrEmpty( entry.WeaponIconPath ) )
					{
						<img class="weapon" src="@entry.WeaponIconPath" />
					}
					else
					{
						<label class="separator">»</label>
					}
				}
				else
				{
					<label class="separator suicide">☠</label>
				}
				<label class="name @(IsLocal( entry.VictimId ) ? "local" : "")">@entry.VictimName</label>
				@if ( entry.Headshot )
				{
					<label class="headshot">HS</label>
				}
			</div>
		}
	</div>
</root>

@code
{
	private const int MaxEntries = 5;
	private const float EntryLifetime = 5f;
	private const float FadeStart = 4f;

	public static KillFeed Instance { get; private set; }

	/// <summary>When true, layout fills a WorldPanel (VR); when false, uses ScreenPanel HUD offsets.</summary>
	[Property] bool IsWorldPanel { get; set; }

	private readonly List<KillFeedEntry> _entries = new();
	private int _version;

	private IEnumerable<KillFeedEntry> VisibleEntries =>
		_entries.OrderByDescending( e => e.CreatedAt ).Take( MaxEntries );

	public static void Add( Guid attackerId, Guid victimId, bool headshot, bool isSuicide, string weaponIconPath )
	{
		Instance?.AddEntry( attackerId, victimId, headshot, isSuicide, weaponIconPath );
	}

	public void AddEntry( Guid attackerId, Guid victimId, bool headshot, bool isSuicide, string weaponIconPath )
	{
		var attacker = NetworkManager.Instance?.GetPlayerSession( attackerId );
		var victim = NetworkManager.Instance?.GetPlayerSession( victimId );

		PushEntry(
			attackerId,
			victimId,
			attacker?.DisplayName ?? "Unknown",
			victim?.DisplayName ?? "Unknown",
			headshot,
			isSuicide,
			weaponIconPath
		);
	}

	/// <summary>Inserts a sample killfeed row (cycles kill / headshot / suicide).</summary>
	[Button( "Add Test Entry" )]
	public void AddTestEntry()
	{
		var localId = Connection.Local?.Id ?? Guid.Empty;
		var variant = _version % 3;

		switch ( variant )
		{
			case 0:
				PushEntry( localId, Guid.NewGuid(), "TestKiller", "TestVictim", false, false, "/materials/m4a1.png" );
				break;
			case 1:
				PushEntry( localId, Guid.NewGuid(), "TestKiller", "TestVictim", true, false, "/materials/usp.webp" );
				break;
			default:
				PushEntry( localId, localId, "TestVictim", "TestVictim", false, true, "" );
				break;
		}
	}

	private void PushEntry( Guid killerId, Guid victimId, string killerName, string victimName, bool headshot, bool isSuicide, string weaponIconPath )
	{
		_entries.Insert( 0, new KillFeedEntry
		{
			KillerId = killerId,
			VictimId = victimId,
			KillerName = killerName,
			VictimName = victimName,
			Headshot = headshot,
			IsSuicide = isSuicide,
			WeaponIconPath = isSuicide ? "" : (weaponIconPath ?? ""),
			CreatedAt = Time.Now
		} );

		while ( _entries.Count > MaxEntries )
			_entries.RemoveAt( _entries.Count - 1 );

		_version++;
		StateHasChanged();
	}

	protected override void OnStart()
	{
		Instance = this;
	}

	protected override void OnDestroy()
	{
		if ( Instance == this )
			Instance = null;
	}

	protected override void OnUpdate()
	{
		var now = Time.Now;
		var removed = _entries.RemoveAll( e => now - e.CreatedAt >= EntryLifetime );
		if ( removed > 0 )
			_version++;
	}

	private static bool IsLocal( Guid connectionId ) =>
		Connection.Local is not null && Connection.Local.Id == connectionId;

	private static float EntryOpacity( KillFeedEntry entry )
	{
		var age = Time.Now - entry.CreatedAt;
		if ( age < FadeStart )
			return 1f;

		return Math.Clamp( 1f - (age - FadeStart) / (EntryLifetime - FadeStart), 0f, 1f );
	}

	protected override int BuildHash()
	{
		var fadeTick = (int)(Time.Now * 10f);
		return HashCode.Combine( _version, fadeTick, _entries.Count, IsWorldPanel );
	}

	private sealed class KillFeedEntry
	{
		public Guid KillerId { get; set; }
		public Guid VictimId { get; set; }
		public string KillerName { get; set; }
		public string VictimName { get; set; }
		public string WeaponIconPath { get; set; }
		public bool Headshot { get; set; }
		public bool IsSuicide { get; set; }
		public float CreatedAt { get; set; }
	}
}