249 results

@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace Sandbox

@*
    The floating name tag above an avatar. StreamPlayer.Setup() points its Viewer at the right viewer,
    and we render their display name in their chat colour.
*@

<root>
    <div class="username" style="@NameStyle">
        @Viewer.DisplayName
    </div>
</root>

@code
{
    /// <summary>
    /// The viewer this tag is labelling. Set by StreamPlayer when the avatar is spawned.
    /// </summary>
    public Streamer.Viewer Viewer { get; set; }

    /// <summary>
    /// Colour the name with the viewer's chat colour, if they have one set.
    /// </summary>
    string NameStyle => Viewer.Color.HasValue ? $"color: {Viewer.Color.Value.Hex}" : null;

    /// <summary>
    /// PanelComponent rebuilds the markup whenever this hash changes - so include anything we display.
    /// </summary>
    protected override int BuildHash() => System.HashCode.Combine( Viewer.DisplayName, Viewer.Color );
}
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox

<root>
    @if (Header != null)
    {
        <div class="header">@Header</div>
    }

    <div class="body menuinner">@Body</div>
</root>

@code
{

    [Parameter] public RenderFragment Header { get; set; }
    [Parameter] public RenderFragment Body { get; set; }

}
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox

<root>
	@Path
</root>


@code
{
	public string Path { get; set;  }

}
@using Sandbox;
@using Sandbox.UI;
@using Sandbox.Mounting;
@inherits Panel
@namespace Sandbox

<root>

<Button class="menu-action primary wide" Icon="➕" Text="#spawnmenu.spawnlist.new_button" Disabled=@( !CanCreate() ) @onclick=@CreatePopup></Button>

</root>

@using Sandbox;
@using Sandbox.UI;
@namespace Sandbox
@inherits Panel

<root>

    <div class="menu-segmented-group">
    @{

        var activeMode = SpawnMenuHost.GetActiveMode();


        foreach ( var mode in Game.TypeLibrary.GetTypesWithAttribute<SpawnMenuHost.SpawnMenuMode>().OrderBy( x => x.Type.Order ) )
        {
            if ( !mode.Attribute.CheckCondition() ) continue;

            var activeClass = mode.Type.TargetType == activeMode?.GetType() ? "active" : "";

            <div class="menu-mode-button @activeClass" @onclick="@( () => SpawnMenuHost.SwitchMode( mode.Type.Name ) )">
                <div class="icon">@mode.Type.Icon</div>
                <div class="title">@mode.Type.Title</div>                
            </div>
        }
    }
    </div>

</root>

@code
{
    protected override int BuildHash() => HashCode.Combine( SpawnMenuHost.GetActiveMode(), Game.TypeLibrary.GetTypesWithAttribute<SpawnMenuHost.SpawnMenuMode>() );
}

@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox

<root class="tt">

    <div class="icon">@Icon</div>
    <div class="title">@Title</div>
    <div class="description">@Description</div>

</root>

@code
{
    [Parameter] public string Title { get; set; }
    [Parameter] public string Icon { get; set; }
    [Parameter] public string Description { get; set; }
}
@using Sandbox;
@using Sandbox.UI;
@using Sandbox.Mounting;
@inherits Panel
@namespace Sandbox

<root>

<Button class="menu-action primary wide" Icon="💾" Text="#spawnmenu.dupes.save_button" Disabled=@( !CanSaveDupe() ) @onclick=@MakeSave></Button>

</root>

@using Sandbox;
@using Sandbox.UI;
@using Sandbox.Mounting;
@inherits Panel
@namespace Sandbox

<root>

	<div class="title">@Item.Title</div>

	<div class="author">

		<div class="avatar" style="background-image: url('@Item.Owner.Avatar');"></div>
		<div class="name">@Item.Owner.Name</div>

	</div>

</root>

@code
{
    public Storage.QueryItem Item { get; set; }

    public override bool WantsDrag => true;

    protected override void OnParametersSet()
    {
        Style.SetBackgroundImage( Item.Preview );
    }

    protected override void OnMouseDown( MousePanelEvent e )
    {
        if ( e.MouseButton == MouseButtons.Right )
        {
            var menu = MenuPanel.Open( this );

            SpawnlistData.PopulateContextMenu( menu, new SpawnlistItem
            {
                Ident = SpawnlistItem.MakeIdent( "dupe", Item.Id.ToString(), "workshop" ),
                Title = Item.Title,
                Icon = Item.Preview,
            } );

            return;
        }

        base.OnMouseDown( e );
    }

    protected override async void OnDragStart( DragEvent e )
    {
        var data = new DragData
        {
            Type = "dupe",
            Icon = Item.Preview,
            Title = Item.Title,
            Source = this
        };

        DragHandler.StartDragging( data );

		var installed = await Item.Install();
		if ( installed is null ) return;

		data.Data = installed.Files.ReadAllText( "/dupe.json" );
	}

    protected override void OnDragEnd(DragEvent e)
    {
        if ( DragHandler.IsDragging )
        {
            DragHandler.StopDragging();
        }
    }
}
@using Sandbox;
@using Sandbox.UI;

@inherits Panel
@namespace Sandbox

@if ( LastResult is null )
{
    // loading
    return;
}

<SpawnMenuContent>

    <Header>
        <SpawnMenuToolbar>
            <Left>
                <TextEntry Placeholder="#spawnmenu.common.search" class="filter menu-input" Value:bind=@Filter />
            </Left>

            <Right>
                <DropDown Value:bind=@SortOrder />
            </Right>
        </SpawnMenuToolbar>
    </Header>

    <Body>
<VirtualGrid Items=@( LastResult.Packages ) ItemSize=@(120)>
    <Item Context="item">
        @if (item is Package entry)
        {
            <SpawnMenuIcon Ident=@($"prop:{entry.FullIdent}") Title="@entry.Title"></SpawnMenuIcon>
        }
    </Item>
</VirtualGrid>
    </Body>

</SpawnMenuContent>

@code
{
    public string Category { get; set; } = "";

    private string Filter
    {
        get;
        set { field = value; Rebuild(); }
    }

    public PackageSortMode SortOrder
    {
        get;
        set { field = value; Rebuild(); }
    }

    protected override void OnParametersSet()
    {
        SortOrder = PackageSortMode.Popular;
        Rebuild();
    }

    Package.FindResult LastResult;

    async void Rebuild()
    {
        var query = $"sort:{SortOrder.ToIdentifier()} type:model";
        if ( !string.IsNullOrEmpty( Filter ) ) query += $" {Filter} ";
        if ( !string.IsNullOrEmpty( Category ) ) query += $" category:{Category}";

        LastResult = await Package.FindAsync( query );
        StateHasChanged();
    }
}
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox

@{
    var data = GetData();
}

<SpawnMenuContent>

    <Header>
        <SpawnMenuToolbar>
            <Left>
                <h2>@data.Name</h2>
            </Left>
            <Right>
                @{
                    var workshopId = Entry.GetMeta( "_workshopId", 0u );
                    var isPublished = workshopId > 0;
                }
                <div class="menu-icon-toggle-group">
                    @if ( isPublished )
                    {
                        <IconPanel Tooltip="#spawnmenu.spawnlist.copy_url" Text="link" @onclick=@( () => Clipboard.SetText( $"https://steamcommunity.com/sharedfiles/filedetails/?id={workshopId}" ) ) />
                        <IconPanel Tooltip="#spawnmenu.spawnlist.sync" Text="sync" @onclick=@( () => OnPublish() ) />
                    }
                    else
                    {
                        <IconPanel Tooltip="#spawnmenu.spawnlist.publish" Text="cloud_upload" @onclick=@( () => OnPublish() ) />
                    }
                    @if ( !Entry.Files.IsReadOnly )
                    {
                        <IconPanel Tooltip="#spawnmenu.spawnlist.delete" Text="delete" @onclick=@( () => OnDelete() ) />
                    }
                    else
                    {
                        <IconPanel Tooltip="#spawnmenu.spawnlist.remove" Text="delete" @onclick=@( () => OnUninstall() ) />
                    }
                </div>
            </Right>
        </SpawnMenuToolbar>
    </Header>

    <Body>
        @if ( data.Items.Count == 0 )
        {
            <div class="empty-state">
                <p>#spawnmenu.spawnlist.empty_title</p>
                <p>#spawnmenu.spawnlist.empty_instructions</p>
            </div>
        }
        else
        {
            <VirtualGrid [email protected] ItemSize=@(120)>
                <Item Context="item">
                    @if ( item is SpawnlistItem spawnItem )
                    {
                        <SpawnMenuIcon Ident="@spawnItem.Ident" Title="@spawnItem.Title" Icon="@spawnItem.Icon" />
                    }
                </Item>
            </VirtualGrid>
        }
    </Body>

</SpawnMenuContent>

@code
{
    public Storage.Entry Entry { get; set; }

    SpawnlistData _cachedData;
    RealTimeSince _lastRefresh;

    SpawnlistData GetData()
    {
        if ( _cachedData == null )
            RefreshCache();

        return _cachedData;
    }

    public void RefreshCache()
    {
        _cachedData = SpawnlistData.Load( Entry );
        _lastRefresh = 0;
    }

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

        // Periodically check for changes from other tabs
        if ( _lastRefresh > 1f )
        {
            var fresh = SpawnlistData.Load( Entry );
            if ( fresh.Items.Count != _cachedData?.Items?.Count )
            {
                _cachedData = fresh;
                StateHasChanged();
            }
            _lastRefresh = 0;
        }
    }

    protected override int BuildHash() => HashCode.Combine( _cachedData?.Items?.Count );

    void OnPublish()
    {
        if ( Entry is null ) return;
        SpawnlistData.Publish( Entry );
    }

    void OnDelete()
    {
        if ( Entry is null ) return;
        var page = Ancestors.OfType<SpawnlistsPage>().FirstOrDefault();
        page?.Collection.Delete( Entry );
    }

    void OnUninstall()
    {
        if ( Entry is null ) return;
        var workshopId = Entry.GetMeta( "_workshopId", 0ul );
        var page = Ancestors.OfType<SpawnlistsPage>().FirstOrDefault();
        page?.Collection.Uninstall( workshopId );
    }
}

@using Sandbox;
@using Sandbox.UI;
@namespace Sandbox
@inherits Panel

<root>

    <div class="menu-segmented-group">
    @{

        var activeMode = SpawnMenuHost.GetActiveMode();


        foreach ( var mode in Game.TypeLibrary.GetTypesWithAttribute<SpawnMenuHost.SpawnMenuMode>().OrderBy( x => x.Type.Order ) )
        {
            if ( !mode.Attribute.CheckCondition() ) continue;

            var activeClass = mode.Type.TargetType == activeMode?.GetType() ? "active" : "";

            <div class="menu-mode-button @activeClass" @onclick="@( () => SpawnMenuHost.SwitchMode( mode.Type.Name ) )">
                <div class="icon">@mode.Type.Icon</div>
                <div class="title">@mode.Type.Title</div>                
            </div>
        }
    }
    </div>

</root>

@code
{
    protected override int BuildHash() => HashCode.Combine( SpawnMenuHost.GetActiveMode(), Game.TypeLibrary.GetTypesWithAttribute<SpawnMenuHost.SpawnMenuMode>() );
}

@using Sandbox;
@using Sandbox.UI;
@using Sandbox.UI.Navigation;
@using Machines.Components;
@using Machines.Resources;

@namespace Machines.UI
@inherits Panel

@{
    var flow = LobbyFlow.Current;
    var map = flow?.SelectedMap;

    var total = Connection.All.Count;
    var ready = ReadyCount();
}

<root class="lobby-status @(Visible ? "" : "hidden") @(Input.UsingController ? "controller" : "mouse")" onclick=@GoToPlay>
    @if ( Visible )
    {
        <div class="ls-map">
            <div class="ls-thumb-wrap">
                <image @ref="ThumbImage" class="ls-thumb" />
                @if ( map?.Thumbnail == null )
                {
                    <div class="ls-thumb-empty">NO IMAGE</div>
                }
            </div>
            <div class="ls-map-info">
                <span class="ls-eyebrow">IN LOBBY</span>
                <span class="ls-title">@(map?.Title ?? "No map")</span>
            </div>
        </div>

        <PlayerSlots class="ls-slots" />

        <div class="ls-status">
            <span class="ls-state">@StateLabel( flow )</span>
            <span class="ls-ready">@ready / @System.Math.Max( total, 1 ) READY</span>
        </div>
    }
</root>

@code
{
    public Sandbox.UI.Image ThumbImage { get; set; }

    // Show when a lobby exists, except on /play which shows the full picker.
    private bool Visible =>
        LobbyFlow.Current is not null
        && MainMenuPanel.Instance?.CurrentUrl != "/play";

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

        if ( ThumbImage != null )
            ThumbImage.Texture = LobbyFlow.Current?.SelectedMap?.Thumbnail;

        StackAbovePatchNotes();
    }

    // Sit above the patch-notes card; explicit px so the CSS transition works both ways.
    private void StackAbovePatchNotes()
    {
        var revision = FindRootPanel()?.Descendants.OfType<RevisionCard>().FirstOrDefault();
        var cardHeight = revision is not null && revision.IsVisible
            ? revision.Box.Rect.Height * ScaleFromScreen + 14f
            : 0f;

        // Nudge up on the main menu home page so it clears the home layout.
        var menuOffset = MainMenuPanel.Instance?.CurrentUrl == "/home" ? 32f : 0f;

        Style.Bottom = Length.Pixels( 56f + cardHeight + menuOffset ); // $deadzone-y (+ card + gap)
    }

    // Click navigates back to the map selector.
    private void GoToPlay()
    {
        this.Navigate( "/play" );
    }

    private static string StateLabel( LobbyFlow flow )
    {
        if ( flow == null )
            return "WAITING";

        return flow.State switch
        {
            LobbyState.Countdown => $"STARTING IN {System.Math.Max( 0, (int)System.Math.Ceiling( (float)flow.CountdownEnds ) )}…",
            LobbyState.Launching => "STARTING…",
            _ => "WAITING"
        };
    }

    private static int ReadyCount()
    {
        var flow = LobbyFlow.Current;
        if ( flow == null )
            return 0;

        return Connection.All.Count( c => flow.Ready.TryGetValue( c.Id, out var r ) && r );
    }

    protected override int BuildHash()
    {
        var flow = LobbyFlow.Current;
        var countdownTick = flow?.State == LobbyState.Countdown ? (int)((float)flow.CountdownEnds * 4) : 0;

        return System.HashCode.Combine(
            Visible,
            flow?.SelectedMapIdent,
            flow?.State,
            Connection.All.Count,
            ReadyCount(),
            countdownTick,
            Input.UsingController );
    }
}
@using Sandbox;
@using Sandbox.UI;
@using Machines.Components;
@using Machines.Race;
@using System;
@using System.Collections.Generic;

@namespace Machines.UI
@inherits Panel

@{
    EnsureBaked();

    var hasMap = _texture != null && _maxExtent > 0.001f && _trackRadius > 0.001f;
    var center = new Vector2( 0.5f, 0.5f );           // whole track, centred on the map
    var fitRadius = PanelSize * 0.5f - RimMargin;     // fit the whole track inside the round clip
    var disp = _maxExtent * (fitRadius / _trackRadius); // displayed track-image size in logical px
}

<root class="minimap @(hasMap ? "" : "empty")">
    <div class="clip">
        @if ( hasMap )
        {
            var imgLeft = PanelSize * 0.5f - center.x * disp;
            var imgTop = PanelSize * 0.5f - center.y * disp;

            <image @ref="MapImage" class="map-img"
                   style="width: @(disp)px; height: @(disp)px; left: @(imgLeft)px; top: @(imgTop)px;" />

            @foreach ( var m in GetCheckpointMarkers( center, disp ) )
            {
                <div class="cp-bar @(m.IsFinish ? "finish" : "")"
                     style="left: @(m.X)px; top: @(m.Y)px; width: @(m.LengthPx)px; transform: translate(-50%, -50%) rotate(@(m.AngleDeg)deg);"></div>
            }

            @foreach ( var dot in GetBlipDots( center, disp ) )
            {
                var c = dot.Color;
                var rgb = $"rgb({(int)(c.r * 255)}, {(int)(c.g * 255)}, {(int)(c.b * 255)})";
                <div class="dot @dot.Class"
                     style="left: @(dot.X)px; top: @(dot.Y)px; background-color: @rgb;"></div>
            }
        }
    </div>
</root>

@code
{
    /// <summary>
    /// Diameter of the minimap in logical px.
    /// </summary>
    public float PanelSize { get; set; } = 320;

    /// <summary>
    /// Inset (logical px) from the minimap edge that the track is fit within.
    /// </summary>
    private const float RimMargin = 32f;

    /// <summary>
    /// Resolution of the baked track texture (square).
    /// </summary>
    public int TextureResolution { get; set; } = 1024;

    /// <summary>
    /// Half-width of the drawn road in world units; tune to match the track mesh.
    /// </summary>
    public float RoadHalfWidth { get; set; } = 80f;

    /// <summary>
    /// Outline thickness around the road, in world units.
    /// </summary>
    public float OutlineWorld { get; set; } = 4f;

    private Image MapImage { get; set; }

    private Texture _texture;
    private RacingLine _bakedFor;        // rebake when this changes
    private Vector2 _center;             // world-XY centre of the baked region
    private float _maxExtent;            // world units covered by the baked region
    private float _trackRadius;          // world units from centre to outermost road edge

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

        if ( MapImage != null && _texture != null )
            MapImage.Texture = _texture;
    }

    /// <summary>
    /// Bakes the track texture once the racing line is available, or rebakes on change.
    /// </summary>
    private void EnsureBaked()
    {
        var line = RacingPath.Current?.Optimal;
        if ( line is null || !line.IsValid )
            return;

        if ( _texture != null && _bakedFor == line )
            return;

        Bake( line, RoadHalfWidth );
        _bakedFor = line;
    }

    private void Bake( RacingLine line, float halfWidth )
    {
        var res = TextureResolution;

        // AA feather sized to ~1 displayed pixel for a smooth outline.
        var (_, _, trackRadius) = TrackMapBaker.ComputeBounds( line, halfWidth, OutlineWorld );
        var pxPerWorld = (PanelSize * 0.5f - RimMargin) / trackRadius;
        var aaW = pxPerWorld > 0.0001f ? 1f / pxPerWorld : 0f;

        var bake = TrackMapBaker.Bake( line, halfWidth, OutlineWorld, res, aaW );
        if ( bake is null )
            return;

        _center = bake.Center;
        _maxExtent = bake.MaxExtent;
        _trackRadius = bake.TrackRadius;

        _texture = Texture.Create( res, res )
            .WithFormat( ImageFormat.RGBA8888 )
            .WithData( bake.Rgba )
            .Finish();
    }

    private struct CpMarker
    {
        public float X;
        public float Y;
        public float AngleDeg;
        public float LengthPx;
        public bool IsFinish;
    }

    /// <summary>
    /// Bar across the track at each checkpoint, oriented to the line tangent.
    /// </summary>
    private List<CpMarker> GetCheckpointMarkers( Vector2 center, float disp )
    {
        var list = new List<CpMarker>();
        if ( _maxExtent < 0.001f )
            return list;

        var line = RacingPath.Current?.Optimal;
        var half = PanelSize * 0.5f;
        var pxPerWorld = disp / _maxExtent;
        var roadWidthWorld = RoadHalfWidth * 2f;
        var lengthPx = MathF.Max( roadWidthWorld * pxPerWorld * 1.4f, 14f );

        foreach ( var cp in Scene.GetAll<Checkpoint>() )
        {
            if ( !cp.IsValid() )
                continue;

            var n = WorldToNorm( cp.WorldPosition );
            var sx = half + (n.x - center.x) * disp;
            var sy = half + (n.y - center.y) * disp;

            // Bar is perpendicular to the line tangent.
            float angle = 0f;
            if ( line is not null && line.IsValid )
            {
                var d = line.GetDistanceAtPosition( cp.WorldPosition );
                var tan = line.GetTangentAtDistance( d );
                // North-up map: screenX ~ -worldY, screenY ~ -worldX.
                var tsx = -tan.y;
                var tsy = -tan.x;
                // Perpendicular = bar's long axis.
                angle = MathF.Atan2( tsx, -tsy ) * (180f / MathF.PI);
            }

            list.Add( new CpMarker
            {
                X = sx,
                Y = sy,
                AngleDeg = angle,
                LengthPx = lengthPx,
                IsFinish = cp.IsFinishLine
            } );
        }

        return list;
    }

    private struct BlipDot
    {
        public float X;
        public float Y;
        public Color Color;
        public string Class;
        public int Priority;
    }

    private List<BlipDot> GetBlipDots( Vector2 center, float disp )
    {
        var dots = new List<BlipDot>();
        var half = PanelSize * 0.5f;
        var maxR = half - 8f; // keep dots inside the round clip

        // Off-screen blips clamp to the rim.
        Vector2 Project( Vector3 world )
        {
            var n = WorldToNorm( world );
            var off = new Vector2( (n.x - center.x) * disp, (n.y - center.y) * disp );
            if ( off.Length > maxR )
                off = off.Normal * maxR;
            return new Vector2( half + off.x, half + off.y );
        }

        // GetAllComponents returns only enabled components.
        foreach ( var blip in Scene.GetAllComponents<IMinimapBlip>() )
        {
            if ( blip is not Component c || !c.IsValid() || !blip.ShowOnMinimap )
                continue;

            var p = Project( c.WorldPosition );
            dots.Add( new BlipDot
            {
                X = p.x,
                Y = p.y,
                Color = blip.BlipColor,
                Class = blip.BlipClass,
                Priority = blip.BlipPriority
            } );
        }

        // Draw order: ghost < items < racers < local player.
        dots.Sort( ( x, y ) => x.Priority.CompareTo( y.Priority ) );
        return dots;
    }

    /// <summary>
    /// Normalized [0,1] texel coords for a world position (north-up).
    /// </summary>
    private Vector2 WorldToNorm( Vector3 w )
    {
        // North-up: world +X = screen up, world +Y = screen left.
        var nu = 0.5f - (w.y - _center.y) / _maxExtent;
        var nv = 0.5f - (w.x - _center.x) / _maxExtent;
        return new Vector2( nu, nv );
    }

    protected override int BuildHash()
    {
        return HashCode.Combine( _texture != null, Time.Now );
    }
}
@using Sandbox;
@using Sandbox.UI;
@using Machines.Player;
@using Machines.Components;
@using Machines.GameModes;

@namespace Machines.UI
@inherits Panel

<root class="score-hud">
    @{
        var car = VisibleCar();
        if ( car is not null )
        {
            var score = car.Score;

            <div class="score-stack" style="opacity: @ComboAlpha( score ).ToString( "0.###", System.Globalization.CultureInfo.InvariantCulture );">
                <div class="combo-value">@score.ComboScore</div>

                @foreach ( var entry in score.Entries.OrderByDescending( e => e.LastTime ) )
                {
                    <div class="combo-entry">
                        <span class="entry-source">@entry.Source</span>
                        <span class="entry-amount">+@entry.Amount</span>
                    </div>
                }
            </div>
        }
    }
</root>

@code
{
    // Tail of the combo timeout over which the whole stack fades out (seconds).
    private const float FadeTail = 0.4f;

    /// <summary>
    /// Local car to show, only while a combo is running; null hides the overlay (so it never
    /// shows on start, and disappears once the chain lapses). Mirrors CarBoostHud's gating.
    /// </summary>
    private Car VisibleCar()
    {
        if ( !BaseGameMode.Current.IsValid() || BaseGameMode.Current.State != GameModeState.Playing )
            return null;

        if ( Game.ActiveScene?.Get<SpectatorCamera>()?.IsActive ?? false )
            return null;

        var car = Car.Local;
        if ( !car.IsValid() || car.Autopilot )
            return null;

        if ( !car.Score.IsValid() || !car.Score.IsComboActive )
            return null;

        return car;
    }

    // Full opacity until the last FadeTail seconds of the combo, then ramp to zero.
    private static float ComboAlpha( CarScore score )
    {
        var remaining = CarScore.ComboTimeout - score.SinceLastGain;
        return MathX.Clamp( remaining / FadeTail, 0f, 1f );
    }

    protected override int BuildHash() => HashCode.Combine( Time.Now );
}
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@inherits Panel
@attribute [StyleSheet("DifficultyPanel.razor.scss")]

@{
	var isClient = Networking.IsClient;
	int diff = DontChangeGameDifficulty ? DifficultyToDisplay : Manager.Instance.Difficulty;
	// var canDecreaseDifficulty = diff > Manager.MinDifficulty;
	var isAtMinDifficulty = diff <= Manager.MinDifficulty - (DontChangeGameDifficulty ? 1 : 0);
	var canIncreaseDifficulty = DontChangeGameDifficulty || ( diff < Manager.MaxDifficulty && Manager.Instance.HasBeatenDifficulty(diff) );
	var isAtMaxDifficulty = diff >= Manager.MaxDifficulty;
	var difficultyDesc = Manager.GetDescriptionForDifficulty( diff );
}

<root>
	<div class="top-row">
		@if( isAtMinDifficulty)
		{
			<div class="difficulty_decrease disabled_button" style="opacity:0;"> </div>
		}
		else
		{
			<div class="difficulty_decrease @(isClient && !DontChangeGameDifficulty ? "disabled_button" : "")" onclick="@(() => ButtonLeft())">
				@if(Input.UsingController && (!isClient || DontChangeGameDifficulty)) { <InputHint class="inputbutton" Button="Slot1" /> }
			</div>
		}

		<div class="middle">
			<div class="difficulty_label" style="color:@(Manager.GetDifficultyLabelColor(diff).Rgba);">@($"{Manager.GetNameForDifficulty(diff)}")</div>
			@if ( !string.IsNullOrEmpty( difficultyDesc ) )
			{
				<label class="description">@difficultyDesc</label>
			}
		</div>

		@if(isAtMaxDifficulty)
		{
			<div class="difficulty_increase disabled_button" style="opacity:0;"></div>
		}
		else
		{
			if(canIncreaseDifficulty)
			{
				<div class="difficulty_increase @(isClient && !DontChangeGameDifficulty ? "disabled_button" : "")" onclick="@(() => ButtonRight())">
					@if(Input.UsingController && (!isClient || DontChangeGameDifficulty)) { <InputHint class="inputbutton" Button="Slot3" /> }
				</div>
			}
			else
			{
				<div class="difficulty_locked" Tooltip=@($"Beat {Manager.GetNameForDifficulty(diff)} first")></div>
			}
		}
	</div>

</root>

@code
{
	public bool DontChangeGameDifficulty { get; set; }
	public static int DifficultyToDisplay { get; set; } = -1; // -1 equals all difficulties combined

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

		if(!Input.UsingController)
			return;

		if(Networking.IsClient && !DontChangeGameDifficulty)
			return;

		int diff = DontChangeGameDifficulty ? DifficultyToDisplay : Manager.Instance.Difficulty;
		var isAtMinDifficulty = diff <= Manager.MinDifficulty - (DontChangeGameDifficulty ? 1 : 0);
		var canIncreaseDifficulty = DontChangeGameDifficulty || ( diff < Manager.MaxDifficulty && Manager.Instance.HasBeatenDifficulty(diff) );
		var isAtMaxDifficulty = diff >= Manager.MaxDifficulty;

		if(Input.Pressed("Slot1") && !isAtMinDifficulty) { Manager.Instance.PlaySfxUI("click", pitch: 1.15f, volume: 0.75f); ButtonLeft(); }
		else if(Input.Pressed("Slot3") && !isAtMaxDifficulty) { Manager.Instance.PlaySfxUI("click", pitch: 1.15f, volume: 0.75f); ButtonRight(); }
	}

	protected override int BuildHash()
	{
		return HashCode.Combine(
			Manager.Instance.Difficulty,
			DifficultyToDisplay,
			Input.UsingController
		);
	}

	void ButtonLeft()
	{
		GameSettingsSystem.Save();

		ButtonLeftAsync();
	}

	async void ButtonLeftAsync()
	{
		if(DontChangeGameDifficulty)
		{
			if(DifficultyToDisplay == Manager.MinDifficulty)
				DifficultyToDisplay = -1;
			else
				DifficultyToDisplay = Math.Max(DifficultyToDisplay - 1, Manager.MinDifficulty);
		}
		else
		{
			Manager.Instance.FadeRpc(fadeIn: false);

			await Task.Frame();

			var difficulty = Math.Max(Manager.Instance.Difficulty - 1, Manager.MinDifficulty);
			Manager.Instance.SetDifficulty(difficulty);
		}
	}

	void ButtonRight()
	{
		ButtonRightAsync();

		GameSettingsSystem.Save();
	}

	async void ButtonRightAsync()
	{
		if(DontChangeGameDifficulty)
		{
			DifficultyToDisplay = Math.Min(DifficultyToDisplay + 1, Manager.MaxDifficulty);
		}
		else
		{
			Manager.Instance.FadeRpc(fadeIn: false);

			await Task.Frame();

			var difficulty = Math.Min(Manager.Instance.Difficulty + 1, Manager.MaxDifficulty);
			Manager.Instance.SetDifficulty(difficulty);
		}
	}
}
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@inherits Panel
@attribute [StyleSheet("InfoPanel.razor.scss")]

<root>
	@{
		// todo: split each ui line into its own component

		var xp_percent = Player.ExperienceCurrent / (float)Player.ExperienceRequired;
		var xp_transition_enabled = Player.GetUiStat( PlayerStat.DisableXpBarTransition ) == 0f;
		var hp_percent = Math.Clamp(Math.Max(Player.Health, 0f) / Player.GetSyncStat(PlayerStat.MaxHp), 0f, 1f);
		// var hp_regen = Player.Stats[PlayerStat.HealthRegen] + (Player.IsMoving ? 0f : Player.Stats[PlayerStat.HealthRegenStill]);
		var hp_regen = Player.GetHpRegenAmount(forDisplay: true);
		var hpBarHidden = Player.GetUiStat( PlayerStat.HpBarHidden ) > 0f;
	}

	<div class="info_bar">
		<div class="bar_container" style=@("mask-image: url(\"/textures/ui/panel/info_panel_xp_mask.png\");")>
			<div class="info_bar_overlay @(xp_transition_enabled ? "xp_transition_on" : "")" style="width:@(xp_percent * 100f)%; background-color: #8888ff77;"></div>

			@if(Manager.Instance.Difficulty >= Manager.Instance.FirstDifficultyWithCurses && Player.AvailableCurseCount > 0)
			{
				var cursePercent = Player.IsBeingShownCurseChoices ? 1f : Utils.Map(Player.CurrLevelsUntilCurseChoice, Player.NumLevelsBetweenCurseChoices, 0, 0f, 1f);
				var colorSpeed = Player.IsBeingShownCurseChoices ? 15f : Utils.Map(Player.CurrLevelsUntilCurseChoice, Player.NumLevelsBetweenCurseChoices, 0, 3f, 15f, EasingType.SineIn);

				var curseColor = Color.Lerp(new Color(0.2f, 0.1f, 0.4f, 0.5f), new Color(0.8f, 0.1f, 0.9f, 0.25f), Utils.Map(Utils.FastSin(RealTime.Now * colorSpeed), -1f, 1f, 0f, 1f));

				<div class="info_bar_overlay" style="width:@(cursePercent * 100f)%; background-color: @curseColor.Rgba;"></div>
			}
		</div>
		
		<div class="info_bar_label">XP</div>

		<div class="data_container">
			<div class="data" style="opacity: 0.3;">@($"LVL {Player.Level}")</div>
		</div>
	</div>

	@{
		var regularMaxDashes = (int)MathF.Round( Player.GetUiStat( PlayerStat.NumDashes ) );
		var maxDashes = regularMaxDashes + Player.NumTempDashesAvailable;
		var totalAvailable = Player.NumDashesAvailable + Player.NumTempDashesAvailable;
 	}

	@if( maxDashes < 9 )
	{
		<div class="info_dash_container" style="gap: @(Utils.Map(maxDashes, 1, 8, 8, 4, EasingType.QuadIn))px;">
			<div class="dash_bar_container")>
				@for(int i = 0; i < maxDashes; i++)
				{
					// Bars i < regularMaxDashes are regular dashes; i >= regularMaxDashes are temporary (from PerkDashTemporary).
					// Temp bars are always shown to the right and colored purple.
					var isTemp = i >= regularMaxDashes;
					float progress;
					bool isRecharging;
					if ( isTemp )
					{
						// Temp dashes are either fully available or gone — no partial recharge animation.
						int tempIndex = i - regularMaxDashes;
						progress = tempIndex < Player.NumTempDashesAvailable ? 1f : 0f;
						isRecharging = false;
					}
					else
					{
						// The bar at NumDashesAvailable is the one currently recharging (partial fill).
						// Bars below it are full; bars above it are empty.
						progress = i == Player.NumDashesAvailable ? Player.DashRechargeProgress : (i < Player.NumDashesAvailable ? 1f : 0f);
						isRecharging = i >= Player.NumDashesAvailable;
					}
					var filledColor   = isTemp ? "#b34dff88" : "#00ff0088";
					var rechargingColor = isTemp ? "#b34dff44" : "#00ff4444";

					<div class="info_bar" style="background-color: #00000077;">
						<div class="info_bar_overlay" style="width:@(progress * 100f)%; background-color: @(isRecharging ? rechargingColor : filledColor);"></div>
					</div>
				}
			</div>

			@if(maxDashes <= 0) 
			{
				<div class="info_bar">
				</div>
			}

			<div class="info_bar_label" style="left: 8px;">
				@("DASH")
			</div>
		</div>
	}
	else
	{
		var dashPercent = (float)totalAvailable / (float)maxDashes;

		<div class="info_bar">
			<div class="info_bar_overlay" style="width:@(dashPercent * 100f)%; background-color: #00ff0055; transition: all 0.2s ease-in-out;"></div>
			<div class="info_bar_label">DASH</div>

			<div class="data_container">
				<div class="data">@maxDashes</div>
				<div class="data">/</div>
				<div class="data">@totalAvailable</div>
			</div>
		</div>
	}

	<div class="info_bar">
		<div class="bar_container" style=@("mask-image: url(\"/textures/ui/panel/info_panel_hp_mask.png\");")>
			@if ( hpBarHidden )
			{
				<div class="info_bar_overlay" style="width: 100%; background-color: #7a0040ff;"></div>
			}
			else
			{
				<div class="info_bar_overlay" style="width:@(hp_percent * 100f)%; background-color: #ffffffff; transition: all 0.4s ease-in-out "></div>
				<div class="info_bar_overlay" style="width:@(hp_percent * 100f)%; background-color: #ff0000ff; transition: all 0.2s ease-in-out;"></div>
			}
		</div>

		<div class="info_bar_label">HP</div>

		<div class="data_container">
			<div class="data">@($"{(int)MathF.Round(Player.GetSyncStat(PlayerStat.MaxHp))}")</div>
			<div class="data">/</div>
			@if ( hpBarHidden )
			{
				<div class="data">?</div>
			}
			else
			{
				<div class="data">@($"{(Math.Max(Player.Health, 0f) > 0f && Math.Max(Player.Health, 0f) < 1f ? (int)Math.Ceiling(Math.Max(Player.Health, 0f)) : (int)Math.Round(Math.Max(Player.Health, 0f)))}")</div>

				@if (Math.Abs(hp_regen) > 0f)
				{
					<div class="data" style="color:@((hp_regen > 0f ? new Color(0f, 1f, 0f) : new Color(1f, 0f, 0f)).Rgba); letter-spacing: 2px; padding-right: 10px;">@($"{(hp_regen > 0f ? "+" : "")}{hp_regen.ToString("0.##")}")</div>
				}
			}
		</div>
	</div>

	@if (Player.Armor > 0)
	{
		<div class="armor_icon" style="transform: scale(@(Utils.Map(Player.RealTimeSinceArmorChanged, 0f, 0.5f, 1.35f, 1f, EasingType.QuadOut)));">
			<label class="armor_text">@($"{MathX.CeilToInt(Player.Armor)}")</label>
		</div>
	}
</root>

@code
{
	public Player Player { get; set; }

	protected override int BuildHash()
	{
		if( Manager.Instance.Difficulty >= Manager.Instance.FirstDifficultyWithCurses )
		{
			return HashCode.Combine(RealTime.Now);
		}

		var hpBarHidden = Player.GetUiStat( PlayerStat.HpBarHidden ) > 0f;

		var hpHash = HashCode.Combine(
			Player.Health,
			Player.GetSyncStat(PlayerStat.MaxHp),
			Player.GetSyncStat( PlayerStat.HealthRegen ),
			Player.GetSyncStat( PlayerStat.HealthRegenStill ),
			Player.IsMoving,
			Player.GetHpRegenAmount( forDisplay: true ),
			hpBarHidden
		);

		var armorHash = HashCode.Combine(
			Player.RealTimeSinceArmorChanged > 0.5f ? 0f : Player.RealTimeSinceArmorChanged.Relative,
			Player.Armor
		);

		var xpHash = HashCode.Combine(
			Player.Level,
			Player.ExperienceCurrent,
			Player.ExperienceRequired
		);
		
		return HashCode.Combine(
			Player.DashRechargeProgress,
			Player.NumDashesAvailable,
			Player.NumTempDashesAvailable,
			Player.GetUiStat( PlayerStat.NumDashes ),
			hpHash,
			armorHash,
			xpHash
		);
	}
}
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@using System.Linq;
@using System.Collections.Generic;
@inherits Panel
@attribute [StyleSheet("LoadoutPanel.razor.scss")]

<root>
	<div class="hide_button" onclick=@(() => Close())></div>
	<div class="title_label">Loadout</div>

	<div class="tabs">
		@for(int t = 0; t < TabNames.Length; t++)
		{
			var tabIndex = t;
			<div class="tab @(_selectedTab == tabIndex ? "active" : "")" onclick=@(() => SelectTab(tabIndex))>
				@TabNames[tabIndex]
			</div>
		}
	</div>

	<div class="panel_body">
		@if(_selectedTab == 2)
		{
			var equippedGems = ProgressManager.GetEquippedGems();
			var ownedGems = ProgressManager.GetOwnedItemsByCategory(ShopItemCategory.Gem);
			var maxGemSlots = ProgressManager.GetSelectedGunSocketCount();
			var gemsFull = equippedGems.Count >= maxGemSlots;

			@if(ownedGems.Count > 0)
			{
				<div class="items_grid">
					@foreach(var gem in ownedGems)
					{
						var isEquipped = equippedGems.Contains(gem.Id);
						var gemCapture = gem;
						<LoadoutItemPanel Item=@gem IsSelected=@isEquipped IsSlotFull=@(!isEquipped && gemsFull) OnClick=@(() => OnSelectGem(gemCapture)) />
					}
				</div>
			}
			else
			{
				<div class="coming_soon">Buy gems in the Shop to equip them here.</div>
			}
		}
		else
		{
			var items = GetItemsForTab(_selectedTab);

			@if(_selectedTab == 1)
			{
				var selectedCharmIds = ProgressManager.GetSelectedCharmIds();
				var maxCharmSlots = ProgressManager.GetSelectedGunCharmSlotCount();
				var charmsFull = selectedCharmIds.Count >= maxCharmSlots;

				<div class="items_grid">
					@foreach(var item in items)
					{
						var isSelected = selectedCharmIds.Contains(item.Id);
						var itemCapture = item;
						<LoadoutItemPanel Item=@item IsSelected=@isSelected IsSlotFull=@(!isSelected && charmsFull && maxCharmSlots > 1) OnClick=@(() => OnSelectItem(itemCapture)) />
					}
				</div>
			}
			else
			{
				var selectedGunId = ProgressManager.GetSelectedGunId();

				<div class="items_grid">
					@foreach(var item in items)
					{
						var isSelected = item.Id == selectedGunId;
						var itemCapture = item;
						<LoadoutItemPanel Item=@item IsSelected=@isSelected OnClick=@(() => OnSelectItem(itemCapture)) />
					}
				</div>
			}
		}
	</div>
</root>

@code
{
	static readonly string[] TabNames = { "Guns", "Charms", "Gems" };
	static readonly ShopItemCategory[] TabCategories = { ShopItemCategory.Gun, ShopItemCategory.Charm, ShopItemCategory.Gem };

	static int _selectedTab = 0;
	static bool _initialTabSet = false;

	protected override void OnAfterTreeRender(bool firstTime)
	{
		base.OnAfterTreeRender(firstTime);
		if(firstTime)
		{
			GunPreviewController.Show();
			if(!_initialTabSet)
			{
				_selectedTab = BestStartingTab();
				_initialTabSet = true;
			}
		}
	}

	int BestStartingTab()
	{
		for(int i = 0; i < TabCategories.Length; i++)
			if(GetItemsForTab(i).Count > 0)
				return i;
		return 0;
	}

	List<ShopItemDef> GetItemsForTab(int tab)
	{
		if(tab < 0 || tab >= TabCategories.Length)
			return new List<ShopItemDef>();
		return ProgressManager.GetOwnedItemsByCategory(TabCategories[tab]);
	}

	void OnSelectItem(ShopItemDef item)
	{
		if(_selectedTab == 0)
		{
			var currentGunId = ProgressManager.GetSelectedGunId();
			var deselecting = currentGunId == item.Id;
			var targetDef = deselecting ? ProgressManager.DefaultGun : item;

			// Unequip gems that exceed the target gun's socket count
			var newSocketCount = targetDef.GemSocketCount > 0 ? targetDef.GemSocketCount : 3;
			var equippedGems = ProgressManager.GetEquippedGems();
			while(equippedGems.Count > newSocketCount)
			{
				ProgressManager.UnequipGem(equippedGems[equippedGems.Count - 1]);
				equippedGems = ProgressManager.GetEquippedGems();
			}

			// Trim selected charms to the target gun's charm slot count
			var newCharmSlots = targetDef.CharmSlotCount > 0 ? targetDef.CharmSlotCount : 1;
			var selectedCharms = ProgressManager.GetSelectedCharmIds();
			if(selectedCharms.Count > newCharmSlots)
				ProgressManager.SetSelectedCharmIds(selectedCharms.Take(newCharmSlots).ToList());

			ProgressManager.SetSelectedGunId(deselecting ? ProgressManager.DefaultGun.Id : item.Id);

			// Broadcast gun swap so all clients see the new model
			var player = Manager.Instance.LocalPlayer;
			if(player.IsValid())
			{
				var gunPrefab = ProgressManager.GetPrefabPath(targetDef.Id) ?? "prefabs/guns/gun_default.prefab";
				var charmIds = ProgressManager.GetSelectedCharmIds();
				var charm0 = charmIds.Count > 0 ? ProgressManager.GetPrefabPath(charmIds[0]) ?? "" : "";
				var charm1 = charmIds.Count > 1 ? ProgressManager.GetPrefabPath(charmIds[1]) ?? "" : "";
				var (g0, g1, g2, g3) = GetEquippedGemPrefabs();
				player.SwapGunRpc(gunPrefab, charm0, g0, g1, g2, g3, charm1);
			}
			GunPreviewController.Refresh();
		}
		else if(_selectedTab == 1)
		{
			var selectedIds = ProgressManager.GetSelectedCharmIds();
			var maxSlots = ProgressManager.GetSelectedGunCharmSlotCount();

			if(selectedIds.Contains(item.Id))
			{
				// Deselect
				ProgressManager.SetSelectedCharmIds(selectedIds.Where(id => id != item.Id).ToList());
			}
			else if(maxSlots == 1)
			{
				// Single-slot gun: replace
				ProgressManager.SetSelectedCharmIds(new List<string> { item.Id });
			}
			else if(selectedIds.Count < maxSlots)
			{
				// Multi-slot gun with a free slot: add
				ProgressManager.SetSelectedCharmIds(selectedIds.Concat(new[] { item.Id }).ToList());
			}
			else
			{
				// All slots full: do nothing, player must deselect first
				return;
			}

			// Broadcast charm swap so all clients see the new model
			var player = Manager.Instance.LocalPlayer;
			if(player.IsValid())
			{
				var ids = ProgressManager.GetSelectedCharmIds();
				var charm0 = ids.Count > 0 ? ProgressManager.GetPrefabPath(ids[0]) ?? "" : "";
				var charm1 = ids.Count > 1 ? ProgressManager.GetPrefabPath(ids[1]) ?? "" : "";
				player.SwapCharmRpc(charm0, charm1);
			}
			GunPreviewController.Refresh();
		}
		StateHasChanged();
	}

	void OnSelectGem(ShopItemDef gem)
	{
		var equipped = ProgressManager.GetEquippedGems();
		var maxSlots = ProgressManager.GetSelectedGunSocketCount();
		if(equipped.Contains(gem.Id))
			ProgressManager.UnequipGem(gem.Id);
		else if(equipped.Count < maxSlots)
			ProgressManager.EquipGem(gem.Id, maxSlots);

		BroadcastGemSwap();
		GunPreviewController.Refresh();
		StateHasChanged();
	}


	void BroadcastGemSwap()
	{
		var player = Manager.Instance.LocalPlayer;
		if(!player.IsValid()) return;
		var (g0, g1, g2, g3) = GetEquippedGemPrefabs();
		player.SwapGemsRpc(g0, g1, g2, g3);
	}

	static (string, string, string, string) GetEquippedGemPrefabs()
	{
		var equipped = ProgressManager.GetEquippedGems();
		string Get(int i) => i < equipped.Count ? (ProgressManager.GetPrefabPath(equipped[i]) ?? "") : "";
		return (Get(0), Get(1), Get(2), Get(3));
	}

	void SelectTab(int tabIndex)
	{
		if(_selectedTab == tabIndex) return;
		_selectedTab = tabIndex;
		StateHasChanged();
	}

	void Close()
	{
		GunPreviewController.Hide();
		Manager.Instance.ShowLoadoutPanel = false;
	}

	protected override int BuildHash()
	{
		return System.HashCode.Combine(
			Manager.Instance.ShowLoadoutPanel,
			_selectedTab,
			ProgressManager.StateVersion
		);
	}
}
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@using System.Text.Json;
@inherits Panel
@attribute [StyleSheet("LobbyNametagPanel.razor.scss")]

<root>
	<LeaderboardPlayerIcon style="width: 42px; height: 42px;" PlayerInfo=@PlayerInfo />
	<div class="name">@PlayerInfo.displayName</div>
	@if(Networking.IsHost || PlayerInfo.steamId == Game.SteamId)
	{
		<i class="remove-btn" onclick=@KickPlayer>disabled_by_default</i>
	}
</root>

@code
{
	const int MAX_NAME_LENGTH = 13;

	public PlayerInfo PlayerInfo { get; set; }

	void KickPlayer()
	{
		Manager.Instance?.RequestKickPlayer(PlayerInfo.steamId);
	}
}
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@inherits Panel
@attribute [StyleSheet("PlayerPerks.razor.scss")]

@{
	var player = Manager.Instance.SelectedPlayer.IsValid()
		? Manager.Instance.SelectedPlayer
		: Manager.Instance.LocalPlayer;

	if (!player.IsValid())
		return;

	var height = Manager.Instance.Players.Count > 1 ? 880f : 950f;
}

<root style="height: @(height)px; max-height: @(height)px;">
	@if( player.IsProxy )
	{
		if (player.SyncPerks.Count == 0)
			return;

		int index = 0;
		<div class="itemlist" style="opacity:@(1f);">
			@foreach (var pair in player.SyncPerks)
			{
				var rot = Utils.Map((player.PerkRandomRotationSeed + index * 3f) % 10, 0, 9, -5f, 5f);
				<PerkIconStatic style="width: 50px; height: 50px; transform: rotate(@(rot)deg);" PerkType=@(PerkManager.IdentityToType(pair.Key)) Level=@(pair.Value) ProgressLevel=@(pair.Value) Banished=@false />

				index++;
			}
		</div>
	}
	else
	{
		if (player.Perks.Count == 0)
			return;

		int index = 0;
		<div class="itemlist @(Manager.Instance.IsGameOver ? "" : "itemlist_hover")">
			@foreach (var (_, perk) in player.Perks)
			{
				var rot = Utils.Map((player.PerkRandomRotationSeed + index * 3f) % 10, 0, 9, -5f, 5f);
				<PerkIcon style="width: 50px; height: 50px;" Perk=@perk Angle=@rot Banished=@false />

				index++;
			}
		</div>
	}
</root>

@code
{
	protected override int BuildHash()
	{
		var player = Manager.Instance.SelectedPlayer.IsValid()
			? Manager.Instance.SelectedPlayer
			: Manager.Instance.LocalPlayer;

		var perkHash = player.IsValid()
			? player.PerkHash
			: 0;

		var perkCount = player.IsValid()
			? player.SyncPerks.Count
			: 0;

		return HashCode.Combine(
			player, 
			// player.Id // todo: this instead?
			perkCount,
			perkHash,
			Manager.Instance.IsGameOver
			// Time.Now
		);
	}
}
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox

<root>
    <div class="menu-icon-toggle-group">
        <IconPanel Tooltip="Compact" class=@( Size == 60 ? "active" : "" ) Text="view_compact" @onclick=@( () => ChangeSize( 60 ) )></IconPanel>
        <IconPanel Tooltip="Medium" class=@( Size == 120 ? "active" : "" ) Text="view_module" @onclick=@( () => ChangeSize( 120 ) )></IconPanel>
        <IconPanel Tooltip="Large" class=@( Size == 200 ? "active" : "" ) Text="grid_view" @onclick=@( () => ChangeSize( 200 ) )></IconPanel>
    </div>
</root>


@code
{
	public int Size { get; set; } = 120;

	protected override int BuildHash() => HashCode.Combine( Size );

	void ChangeSize( int size )
	{
		Size = size;
	}
}

@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace Sandbox

<root>

	@if ( Current is not null )
	{
		var mouse = Mouse.Position;
		var left = mouse.x * Panel.ScaleFromScreen - 32;
		var top = mouse.y * Panel.ScaleFromScreen - 32;
		var iconStyle = string.IsNullOrEmpty( Current.Icon ) ? "" : $"background-image: url({Current.Icon}); ";
		<div class="dragging" style="@(iconStyle)left: @(left)px; top: @(top)px;" @ref="DragVisual">
			@if ( string.IsNullOrEmpty( Current.Icon ) )
			{
				<div class="drag-title">@Current.Title</div>
			}
		</div>
	}

</root>

@code
{
    public static DragHandler Instance { get; private set; }

    /// <summary>
    /// The data currently being dragged, or null.
    /// </summary>
    public static DragData Current { get; private set; }

    /// <summary>
    /// True if something is actively being dragged.
    /// </summary>
    public static bool IsDragging => Current is not null;

    Panel DragVisual { get; set; }
    private RootPanel _rootPanel;

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

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

    /// <summary>
    /// Start dragging with the given data.
    /// </summary>
    public static void StartDragging( DragData data )
    {
        Current = data;
        data.Source.UserData = data;

        Instance?.StateHasChanged();
    }

    /// <summary>
    /// Stop dragging and clear all state.
    /// </summary>
    public static void StopDragging()
    {
        if (!IsDragging) return;

        Current = null;
        Instance?.StateHasChanged();
    }

    protected override void OnUpdate()
    {
        if ( !IsDragging ) return;

        if ( !_rootPanel.WantsMouseInput() )
        {
            StopDragging();
            return;
        }

        if ( DragVisual is not null )
        {
            var mouse = Mouse.Position;
            DragVisual.Style.Left = Length.Pixels( mouse.x * Panel.ScaleFromScreen - 32 );
            DragVisual.Style.Top = Length.Pixels( mouse.y * Panel.ScaleFromScreen - 32 );
        }
    }

    protected override void OnTreeBuilt()
    {
        _rootPanel = Panel.FindRootPanel();
    }

	protected override int BuildHash() => HashCode.Combine( Current );
}
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox
@attribute [SpawnMenuHost.SpawnMenuMode]
@attribute [Icon( "🎨" )]
@attribute [Title( "#spawnmenu.mode.effects" )]
@attribute [Order( -75 )]

<root>
    <EffectsList />
    <EffectsProperties />
</root>
@using Sandbox;
@using Sandbox.UI;
@namespace Sandbox
@inherits Panel

<root>

    <div class="presets-toggle" @onclick=@OnToggleClick>
        <i>expand_less</i>
    </div>

</root>

@code
{
    public PlayerInventory Inventory { get; set; }

    void OnToggleClick()
    {
        var menu = MenuPanel.Open( this );

        var presets = PlayerLoadout.GetLoadoutPresets();
        foreach ( var preset in presets )
        {
            var name = preset.Name;
            var json = preset.LoadoutJson;
            menu.AddSubmenu( "bookmark", name, sub =>
            {
                sub.AddOption( "play_arrow", "Load", () => OnLoadPreset( json ) );
                sub.AddOption( "save", "Overwrite with current", () => OnOverwritePreset( name ) );
                sub.AddOption( "close", "Delete", () => OnDeletePreset( name ) );
            } );
        }

        if ( presets.Any() )
        {
            menu.AddSpacer();
        }

        menu.AddOption( "refresh", "Reset to Default", ResetToDefault );
        menu.AddOption( "add", "New Preset", OnSaveNew );
        menu.StateHasChanged();
    }

    void OnLoadPreset( string json )
    {
        var loadout = GetLoadout();
        if ( !loadout.IsValid() ) return;
        loadout.SwitchToPreset( json );
    }

    void OnOverwritePreset( string name )
    {
        var json = LocalData.Get<string>( "hotbar" );
        if ( string.IsNullOrEmpty( json ) ) return;

        PlayerLoadout.SaveLoadoutPreset( name, json );
    }

    void OnDeletePreset( string name )
    {
        PlayerLoadout.DeleteLoadoutPreset( name );
    }

    void ResetToDefault()
    {
        var loadout = GetLoadout();
        if ( !loadout.IsValid() ) return;
        loadout.ResetToDefault();
    }

    void OnSaveNew()
    {
        var popup = new StringQueryPopup
        {
            Title = "New Inventoy Preset",
            Placeholder = "Enter a name...",
            ConfirmLabel = "Save",
            OnConfirm = OnSaveConfirmed,
            Parent = FindRootPanel()
        };
    }

    void OnSaveConfirmed( string name )
    {
        var json = LocalData.Get<string>( "hotbar" );
        if ( string.IsNullOrEmpty( json ) ) return;

        PlayerLoadout.SaveLoadoutPreset( name, json );
    }

    PlayerLoadout GetLoadout()
    {
        return Inventory?.GetComponent<PlayerLoadout>();
    }
}
@using Sandbox;
@using Sandbox.UI;
@using Sandbox.Mounting;
@inherits Panel
@namespace Sandbox

<root>

	<div class="title">@Item.Title</div>

	<div class="author">

		<div class="avatar" style="background-image: url('@Item.Owner.Avatar');"></div>
		<div class="name">@Item.Owner.Name</div>

	</div>

</root>

@code
{
    public Storage.QueryItem Item { get; set; }

    public override bool WantsDrag => true;

    protected override void OnParametersSet()
    {
        Style.SetBackgroundImage( Item.Preview );
    }

    protected override void OnMouseDown( MousePanelEvent e )
    {
        if ( e.MouseButton == MouseButtons.Right )
        {
            var menu = MenuPanel.Open( this );

            SpawnlistData.PopulateContextMenu( menu, new SpawnlistItem
            {
                Ident = SpawnlistItem.MakeIdent( "dupe", Item.Id.ToString(), "workshop" ),
                Title = Item.Title,
                Icon = Item.Preview,
            } );

            return;
        }

        base.OnMouseDown( e );
    }

    protected override async void OnDragStart( DragEvent e )
    {
        var data = new DragData
        {
            Type = "dupe",
            Icon = Item.Preview,
            Title = Item.Title,
            Source = this
        };

        DragHandler.StartDragging( data );

		var installed = await Item.Install();
		if ( installed is null ) return;

		data.Data = installed.Files.ReadAllText( "/dupe.json" );
	}

    protected override void OnDragEnd(DragEvent e)
    {
        if ( DragHandler.IsDragging )
        {
            DragHandler.StopDragging();
        }
    }
}
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox
@implements IUtilityTab
@attribute [Icon("🌍")]
@attribute [Title("#spawnmenu.tab.utilities")]
@attribute [Order(0)]

<root class="tab">
    <div class="left">
        <VerticalMenu class="menuinner">
            <Options>
                @foreach ( var group in GetVisiblePages().GroupBy( x => x.Group ).OrderBy( x => x.Min( t => t.Order ) ) )
                {
                    @if ( !string.IsNullOrWhiteSpace( group.Key ) )
                    {
                        <h2>@group.Key</h2>
                    }

                    @foreach ( var type in group.OrderBy( x => x.Order ).ThenBy( x => x.Title ) )
                    {
                        <MenuOption Text="@type.Title" Icon="@type.Icon"
                            class=@( SelectedPageType == type ? "active" : "" )
                            @onclick="@(() => OnSelect( type ))">
                        </MenuOption>
                    }
                }
            </Options>
        </VerticalMenu>
    </div>

    <div class="body menuinner" @ref="PageContainer"></div>
</root>

@code
{
    TypeDescription SelectedPageType { get; set; }
    Panel PageContainer;
    UtilityPage ActivePage;

    IEnumerable<TypeDescription> GetVisiblePages()
    {
        foreach ( var type in Game.TypeLibrary.GetTypes<UtilityPage>() )
        {
            if ( type.IsAbstract ) continue;
            var instance = type.Create<UtilityPage>();
            if ( instance is null || !instance.IsPageVisible() ) continue;
            instance.Delete();
            yield return type;
        }
    }

    void OnSelect( TypeDescription type )
    {
        SelectedPageType = type;

        ActivePage?.Delete();
        ActivePage = type.Create<UtilityPage>();
        PageContainer.AddChild( ActivePage );

        StateHasChanged();
    }
}
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace Sandbox

@if ( !Player.IsValid() || Player.WantsHideHud ) 
    return;

<root>
    <div class="vitals">
        @if ( Player.Armour > 0 )
        {
            <div class="stat armour hud-panel">
                <Image class="icon" Texture=@ArmourIcon />
                <label class="value">@(DisplayArmour)</label>
            </div>
        }
        <div class="stat health hud-panel">
            <Image class="icon" Texture=@HealthIcon />
            <label class="value">@(DisplayHealth)</label>
        </div>
    </div>

    @if ( Weapon.IsValid() && Weapon.UsesAmmo )
    {
        <div class="ammo hud-panel">
            @if ( WeaponConVars.UnlimitedAmmo )
            {
                <label class="value">∞</label>
            }
            else
            {
                <label class="value">@(Weapon.UsesClips ? Weapon.ClipContents.ToString() : (WeaponConVars.InfiniteReserves ? "∞" : Weapon.ReserveAmmo.ToString()))</label>
                @if ( Weapon.UsesClips )
                {
                    <label class="alternate">@(WeaponConVars.InfiniteReserves ? "∞" : Weapon.ReserveAmmo.ToString())</label>
                }
            }
            <Image class="icon" Texture=@AmmoIcon />
        </div>
    }
</root>

@code
{
    [Property] public Texture HealthIcon { get; set; }
    [Property] public Texture ArmourIcon { get; set; }
    [Property] public Texture AmmoIcon { get; set; }

    Player Player => Player.FindLocalPlayer();
    BaseWeapon Weapon => Player?.GetComponent<PlayerInventory>()?.ActiveWeapon as BaseWeapon;

    int DisplayHealth => (int)Player.Health;
    int DisplayArmour => (int)Player.Armour;

    bool IsSpawnMenuOpen()
    {
        var host = Game.ActiveScene.Get<SpawnMenuHost>();
        return host?.Panel?.HasClass( "open" ) ?? false;
    }

    protected override void OnUpdate()
    {
        Panel.SetClass( "spawnmenu-open", IsSpawnMenuOpen() );
    }

    protected override int BuildHash()
    {
        if ( !Player.IsValid() || Player.WantsHideHud ) return 0;

        return System.HashCode.Combine( Player.Health, Player.Armour, Weapon?.ClipContents, Weapon?.ReserveAmmo );
    }
}
@namespace Facepunch.UI
@inherits PanelComponent

<root>

    @foreach (var voice in VoiceList.Where( x => CanDisplay( x ) ) )
    {
        <div class="item-row">
            <div class="avatar-wrap">
                <div class="speaking-glow" style="opacity: @GetGlowOpacity( voice ); transform: scale( @GetGlowScale( voice ) )"></div>
                <img class="avatar" src="avatar:@voice.Network.Owner.SteamId" />
            </div>
            <label class="name">@voice.Network.Owner.DisplayName</label>
        </div>
    }

</root>

@code
{
    public IEnumerable<Voice> VoiceList => Scene.GetAllComponents<Voice>();

    private Dictionary<ulong, float> _smoothed = new();

    private float GetSmoothed( Voice voice )
    {
        var id = voice.Network.Owner.SteamId;
        _smoothed[id] = _smoothed.GetValueOrDefault( id, 0f ).LerpTo( voice.Amplitude, Time.Delta * 10f );
        return _smoothed[id];
    }

    private bool CanDisplay( Voice voice )
    {
        if ( voice.Network.Owner is null ) return false;
        return voice.LastPlayed < 0.25f;
    }

    private string GetGlowOpacity( Voice voice )
    {
        var s = GetSmoothed( voice );
        return Math.Clamp( s * 4f, 0.2f, 0.9f ).ToString( "0.##" );
    }

    private string GetGlowScale( Voice voice )
    {
        var s = GetSmoothed( voice );
        return Math.Clamp( 1f + s * 0.6f, 1f, 1.6f ).ToString( "0.##" );
    }

    protected override int BuildHash()
    {
        return HashCode.Combine( Time.Now );
    }
}
@inherits PanelComponent
<root> <div class="ynal2p"> @t </div> </root>

@code
{
[Property] public game g {get;set;}
string t = "You need at least 2 players.";

protected override void OnUpdate()
{
if (g.IsActive && g.Players.Count >= 2)
d(this);
}

[Rpc.Broadcast]
public void d(PanelComponent pc)
{pc.Destroy();}
}
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox

<root>
	@Path
</root>


@code
{
	public string Path { get; set;  }

}
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox
@attribute [SpawnMenuHost.SpawnMenuMode]
@attribute [Icon( "🎨" )]
@attribute [Title( "#spawnmenu.mode.effects" )]
@attribute [Order( -75 )]

<root>
    <EffectsList />
    <EffectsProperties />
</root>
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox

<SpawnMenuContent>

    <Header>
        <SpawnMenuToolbar></SpawnMenuToolbar>
    </Header>

    <Body>
        <VirtualGrid Items=@( Entries ) ItemSize=@(120)>
            <Item Context="item">
                @if (item is Entry entry)
                {
                    <SpawnMenuIcon Ident=@($"prop:{entry.Ident}")></SpawnMenuIcon>
                }
            </Item>
        </VirtualGrid>
    </Body>

</SpawnMenuContent>

@code
{
    record class Entry( string Icon, string Ident );

    private readonly List<Entry> Entries = new()
    {
        new("https://cdn.sbox.game/asset/facepunch.oildrumexplosive/thumb.png.fff8e3787c17283", "facepunch.oildrumexplosive"),
        new("https://cdn.sbox.game/asset/facepunch.watermelon/thumb.png.683db0caeab55816", "facepunch.watermelon"),
        new("https://cdn.sbox.game/asset/facepunch.toolchest/thumb.png.e22d621990091e0d", "facepunch.toolchest"),
        new("https://cdn.sbox.game/asset/facepunch.cabinet_a3/thumb.png.1bf467b6816cecf8", "facepunch.cabinet_a3"),
        new("https://cdn.sbox.game/asset/facepunch.wooden_chair_a/thumb.png.e37e2f0a1d36a772", "facepunch.wooden_chair_a"),
        new("https://cdn.sbox.game/asset/facepunch.water_drum_01/thumb.png.34b58345ab151a28", "facepunch.water_drum_01"),
        new("https://cdn.sbox.game/asset/facepunch.metalwheelbarrow/thumb.png.25663142bc944891", "facepunch.metalwheelbarrow"),
        new("https://cdn.sbox.game/asset/facepunch.hoovera/thumb.png.740d47f682e9ebb9", "facepunch.hoovera"),
        new("https://cdn.sbox.game/asset/facepunch.washingmachine/thumb.png.7aca1114cc535187", "facepunch.washingmachine"),
        new("https://cdn.sbox.game/asset/fpopium.crackhead_02/thumb.png.195fcd31c282be9f", "fpopium.crackhead_02"),
        new("https://cdn.sbox.game/asset/fish.moose/thumb.png.56b890737161bf47", "fish.moose"),
        new("https://cdn.sbox.game/asset/starblue.forklifttruck/thumb.png.d684be52bb80e4d8", "starblue.forklifttruck"),
    };

    void Spawn( string ident )
    {
        GameManager.Spawn( $"prop:{ident}" );
    }
}
@namespace Facepunch.UI
@inherits PanelComponent

<root>

    @foreach (var voice in VoiceList.Where( x => CanDisplay( x ) ) )
    {
        <div class="item-row">
            <div class="avatar-wrap">
                <div class="speaking-glow" style="opacity: @GetGlowOpacity( voice ); transform: scale( @GetGlowScale( voice ) )"></div>
                <img class="avatar" src="avatar:@voice.Network.Owner.SteamId" />
            </div>
            <label class="name">@voice.Network.Owner.DisplayName</label>
        </div>
    }

</root>

@code
{
    public IEnumerable<Voice> VoiceList => Scene.GetAllComponents<Voice>();

    private Dictionary<ulong, float> _smoothed = new();

    private float GetSmoothed( Voice voice )
    {
        var id = voice.Network.Owner.SteamId;
        _smoothed[id] = _smoothed.GetValueOrDefault( id, 0f ).LerpTo( voice.Amplitude, Time.Delta * 10f );
        return _smoothed[id];
    }

    private bool CanDisplay( Voice voice )
    {
        if ( voice.Network.Owner is null ) return false;
        return voice.LastPlayed < 0.25f;
    }

    private string GetGlowOpacity( Voice voice )
    {
        var s = GetSmoothed( voice );
        return Math.Clamp( s * 4f, 0.2f, 0.9f ).ToString( "0.##" );
    }

    private string GetGlowScale( Voice voice )
    {
        var s = GetSmoothed( voice );
        return Math.Clamp( 1f + s * 0.6f, 1f, 1.6f ).ToString( "0.##" );
    }

    protected override int BuildHash()
    {
        return HashCode.Combine( Time.Now );
    }
}
@using Skateboard.Player
@using Skateboard.Tricks
@inherits PanelComponent

<root>
	<div class="@GetMultiplierClass()">@_multiplierText</div>
	<div class="@GetTrickClass()">@_trickText</div>
</root>

@code
{
	private float _displayTime;
	private const float MaxDisplayTime = 5f;
	private bool _lastFinished;
	private int _pulseTick;

	private string _multiplierText = "";
	private string _trickText = "";
	private bool _done;
	private bool _failed;
	private bool _fadeout;
	private bool _visuallyEmpty;

	protected override void OnStart()
	{
		TrickScoreHolder.OnLocalTrickScoreUpdate += OnTrickScoreUpdate;
	}

	protected override void OnDestroy()
	{
		TrickScoreHolder.OnLocalTrickScoreUpdate -= OnTrickScoreUpdate;
	}

	private void OnTrickScoreUpdate()
	{
		_displayTime = 0f;
		_pulseTick++;
		StateHasChanged();
	}

	protected override void OnUpdate()
	{
		var holder = SkatePawn.Local?.TrickScores;
		if ( holder is null )
			return;

		if ( _displayTime < MaxDisplayTime && holder.Finished )
			_displayTime += Time.Delta;

		if ( _lastFinished != holder.Finished )
		{
			_lastFinished = holder.Finished;
			_displayTime = 0f;
		}

		var visuallyEmpty = holder.VisuallyEmpty;
		var multiplierText = visuallyEmpty ? "" : $"{holder.Score} x {holder.Multiplier}";
		var trickText = visuallyEmpty ? "" : holder.String;
		var done = holder.Finished;
		var failed = holder.Failed;
		var fadeout = _displayTime >= MaxDisplayTime;

		if ( _visuallyEmpty == visuallyEmpty &&
			 _multiplierText == multiplierText &&
			 _trickText == trickText &&
			 _done == done &&
			 _failed == failed &&
			 _fadeout == fadeout )
			return;

		_visuallyEmpty = visuallyEmpty;
		_multiplierText = multiplierText;
		_trickText = trickText;
		_done = done;
		_failed = failed;
		_fadeout = fadeout;

		StateHasChanged();
	}

	private string GetMultiplierClass()
	{
		return BuildClass( $"multiplier skate-trick-text {GetPulseClass()}" );
	}

	private string GetTrickClass()
	{
		return BuildClass( $"tricks skate-trick-text {GetPulseClass()}" );
	}

	private string BuildClass( string baseClass )
	{
		if ( _visuallyEmpty )
			return $"{baseClass} hidden";

		if ( _fadeout )
			baseClass += " fadeout";

		if ( _failed )
			baseClass += " failed";
		else if ( _done )
			baseClass += " done";

		return baseClass;
	}

	private string GetPulseClass()
	{
		return _pulseTick % 2 == 0 ? "pulse pulse-a" : "pulse pulse-b";
	}
}
@using Sandbox;
@using Sandbox.UI;

@namespace Machines.UI
@inherits PanelComponent

<root>
    <MainMenuPanel />
</root>

@code
{
    protected override int BuildHash() => 0;
}
@inherits PanelComponent
<root> <div class="eyelids" style="opacity: @o;"> </div> </root>

@code
{
public float o {get;set;} = 0;
protected override int BuildHash() => System.HashCode.Combine(o);
}
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@inherits Panel
@attribute [StyleSheet("PerkChoicePanel.razor.scss")]

@{
	var player = GetViewedPlayer();
	var isReadOnly = IsReadOnly( player );
	var showViewingLabel = isReadOnly && !Manager.Instance.IsSpectator;

	if ( !player.IsValid() || (!player.IsChoosingLevelUpReward && !showViewingLabel) )
	{
		Manager.Instance.IsHoveringPerkChoicePanel = false;
		return;
	}
}

@{
	var showBanish = player.HasGottenBanish;
	var showSkip = GetStat( player, PlayerStat.SkipPerkNumRerolls ) > 0f && !player.IsBeingShownCurseChoices;
	var showRandom = GetStat( player, PlayerStat.CanChooseRandomPerk ) > 0f;
	int numChoices = player.GetDisplayedPerkChoiceCount();
}

<root style="bottom: @(Manager.Instance.ShowBossHealthbar ? 50 : 10)px;">
	@if ( showViewingLabel )
	{
		<div class="viewing_label">
			<label>VIEWING CHOICES OF</label>
			<div class="viewing_avatar" style="background-image: url( avatar:@player.Network.Owner.SteamId );"></div>
		</div>
	}
	@if ( player.IsChoosingLevelUpReward )
	{
	@if ( player.NumPerkPointsAvailable > (player.IsBeingShownCurseChoices ? 0 : 1) )
	{
		@{
			var left = 33 + (showBanish ? -8 : 0) + (showSkip ? -6 : 0) + (showRandom ? -5 : 0);
		}

		<div class="num_left" style="left:@(left)%;">
			@{ var extraPerks = player.NumPerkPointsAvailable - 1; }
			<label style="color: #ffffffbb; padding-right: 2px;">@($"{extraPerks}")</label>
			<label style="color: #ffffff88; padding-left: 2px;">@($"MORE {(extraPerks > 1 ? "PERKS" : "PERK")}")</label>
		</div>
	}

	<div class="itemselection @(numChoices > 17 ? "scrollable" : "")" style="gap: @(numChoices == 5 ? 6 : 12)px;">
		@{
			bool onlyShowIcons = numChoices > 5;
		}

		@for ( int i = 0; i < player.GetDisplayedPerkChoiceCount(); i++ )
		{
			var perkType = player.GetDisplayedPerkChoice( i );
			var isUnknown = player.GetDisplayedPerkChoiceUnknown( i );
			var index = i;
			<PerkChoice ViewedPlayer=@player PerkType=@perkType IsChoice=@true OnlyShowIcon=@onlyShowIcons Slot=@i IsUnknown=@isUnknown onclick="@(e => PerkChosen( e, perkType, index ))" />
		}

		@if ( GetStat( player, PlayerStat.ChooseTimeLimit ) > 0f )
		{
			<div class="timelimitbar">
				@{
					var timeLimitProgress = Utils.Map( player.RealTimeSinceOfferedChoices, 0f, GetStat( player, PlayerStat.ChooseTimeLimit ), 0f, 1f );
				}
				<div style="width:@((1f - timeLimitProgress) * 100f)%; background-color:@(Color.Lerp(new Color(1f, 0.8f, 0.8f), new Color(1f, 0f, 0f), timeLimitProgress).Rgba);"></div>
			</div>
		}
	</div>
	<div class="lower_row">
		<div class="button_outer">
			@{
				bool canUseArmor = GetStat( player, PlayerStat.ArmorRerollCost ) > 0f && player.Armor >= (int)GetStat( player, PlayerStat.ArmorRerollCost );
				bool isFree = GetStat( player, PlayerStat.FreeRerollTime ) > 0f && player.RealTimeSinceLvlUp < GetStat( player, PlayerStat.FreeRerollTime );
				bool canReroll = player.NumRerollAvailable > 0 || isFree || canUseArmor;
				var autoRerollTimerProgress = player.GetDisplayedAutoRerollTimerProgress();
			}
			<panel class="button_container @((canReroll && !isReadOnly) ? "" : "disabled")" style="background-image: url(@("/textures/ui/panel/reroll_panel.png"));" onclick="@(e => RerollClicked( e ))">
				@{
					string amountText = isFree ? "∞" : (canUseArmor ? $"{(int)GetStat(player, PlayerStat.ArmorRerollCost)}⛊" : $"{player.NumRerollAvailable}");
					int amountFontSize = canUseArmor ? 16 : 20;
				}

				<InputHint class="ctrl" Button="R" style="opacity: @((canReroll && !isReadOnly) ? 1f : 0.08f);"></InputHint>
				<label class="button" style="color:@((canReroll && !isReadOnly) ? "#E8FFF4CC" : "#ffffff99");"></label>
				<label class="amount" style="color:@((canReroll && !isReadOnly) ? "#88B19E" : "#ffffff99"); font-size:@($"{amountFontSize}px");">@amountText</label>

				@if ( isFree )
				{
					@{
						var freeRerollProgress = Utils.Map( player.RealTimeSinceLvlUp, 0f, GetStat( player, PlayerStat.FreeRerollTime ), 0f, 1f );
					}
					<div class="free_reroll_bar">
						<div style="width:@((1f - freeRerollProgress) * 100f)%;"></div>
					</div>
				}
				@if ( autoRerollTimerProgress > 0f && canReroll )
				{
					<div class="autoreroll_bar">
						<div style="width:@((1f - autoRerollTimerProgress) * 100f)%;"></div>
					</div>
				}
			</panel>
		</div>

		@if ( showBanish )
		{
			<div class="button_outer">
				<panel class="button_container @((player.NumBanishAvailable > 0 && !isReadOnly) ? "" : "disabled")" style="background-image: url(@("/textures/ui/panel/banish_panel.png"));" onclick="@(e => BanishClicked( e ))">
					<InputHint class="ctrl" Button="banish" style="opacity: @((player.NumBanishAvailable > 0 && !isReadOnly) ? 1f : 0.08f);"></InputHint>
					<label class="button" style="color:@((player.NumBanishAvailable > 0 && !isReadOnly) ? "#E8FFF4CC" : "#ffffff99");"></label>
					<label class="amount" style="color:@((player.NumBanishAvailable > 0 && !isReadOnly) ? "#88B19E" : "#ffffff99");">@player.NumBanishAvailable</label>
				</panel>
			</div>
		}

		@if ( showSkip )
		{
			<div class="button_outer">
				<panel class="button_container @(isReadOnly ? "disabled" : "")" style="background-image: url(@("/textures/ui/panel/skip_panel.png"));" onclick="@(e => SkipClicked( e ))">
					<label class="button" style="color:@(isReadOnly ? "#ffffff99" : "#E8FFF4CC"); width: 90px;"></label>
					<label class="amount" style="font-size: 16px; color:@(!isReadOnly && (int)GetStat(player, PlayerStat.SkipPerkNumRerolls) > 0 ? "#88B19E" : "#ffffff55");">@($"+{(int)GetStat(player, PlayerStat.SkipPerkNumRerolls)}")</label>
				</panel>
			</div>
		}

		@if ( showRandom )
		{
			<div class="button_outer">
				<panel class="button_container @(isReadOnly ? "disabled" : "")" style="background-image: url(@("/textures/ui/panel/random_panel.png"));" onclick="@(e => RandomClicked( e ))">
					<label class="button" style="color:@(isReadOnly ? "#ffffff99" : "#E8FFF4CC"); width: 120px;"></label>
				</panel>
			</div>
		}
	</div>
	}

</root>

@code {
	private Player GetViewedPlayer()
	{
		var manager = Manager.Instance;
		var player = manager.SelectedPlayer.IsValid()
			? manager.SelectedPlayer
			: manager.LocalPlayer;

		if ( player.IsValid() && player.IsDead )
			return null;

		return player;
	}

	private bool IsReadOnly( Player player )
	{
		return player.IsValid() && player != Manager.Instance.LocalPlayer;
	}

	private Player GetInteractivePlayer()
	{
		var player = GetViewedPlayer();
		return IsReadOnly( player ) ? null : player;
	}

	private float GetStat( Player player, PlayerStat stat )
	{
		return player.IsValid() ? player.GetUiStat( stat ) : 0f;
	}

	protected override void OnMouseOver( MousePanelEvent e )
	{
		Manager.Instance.IsHoveringPerkChoicePanel = true;
	}

	protected override void OnMouseOut( MousePanelEvent e )
	{
		Manager.Instance.IsHoveringPerkChoicePanel = false;
	}

	protected void PerkChosen( PanelEvent e, TypeDescription type, int index )
	{
		e.StopPropagation();

		if ( Manager.Instance.IsGameOver || Manager.Instance.IsPaused )
			return;

		var player = GetInteractivePlayer();
		if ( !player.IsValid() )
			return;

		if ( Manager.Instance.HoveredPerkType != null && Manager.Instance.IsHoveredPerkAChoice )
		{
			Manager.Instance.HoveredPerkType = null;
			Manager.Instance.HoveredPerkViewedPlayer = null;
			Manager.Instance.HoveredPerkChoiceSlot = -1;
		}

		if ( !player.CanInteractWithChoices )
			return;

		if ( player.IsBanishMode )
		{
			player.CanInteractWithChoices = false;
			player.BanishPerkUIChoice( type );
			Manager.Instance.PlaySfxUI( "banish_perk", pitch: Utils.Map( player.NumBanishAvailable, 0, 5, 1f, 1.3f, EasingType.QuadIn ), volume: 0.95f );
		}
		else
		{
			player.CanInteractWithChoices = false;
			player.AddPerkUIChoice( type );
			Manager.Instance.PlaySfxUI( "click", pitch: Utils.Map( player.NumPerkPointsAvailable, 0, 10, 1f, 0.8f, EasingType.QuadIn ), volume: 0.75f );
		}
	}

	protected void RerollClicked( PanelEvent e )
	{
		e.StopPropagation();

		var player = GetInteractivePlayer();
		if ( !player.IsValid() )
			return;

		player.UseReroll();
	}

	protected void BanishClicked( PanelEvent e )
	{
		e.StopPropagation();

		var player = GetInteractivePlayer();
		if ( !player.IsValid() )
			return;

		player.ToggleBanish();
	}

	protected void SkipClicked( PanelEvent e )
	{
		e.StopPropagation();

		var player = GetInteractivePlayer();
		if ( !player.IsValid() )
			return;

		player.RefreshAfterChoosingPerk();

		var perkType = TypeLibrary.GetType( typeof( PerkSkipChoices ) );
		if ( player.HasPerk( perkType ) )
		{
			var skipPerk = player.GetPerk( perkType ) as PerkSkipChoices;
			if ( skipPerk != null )
				skipPerk.OnSkip();
		}
	}

	protected void RandomClicked( PanelEvent e )
	{
		e.StopPropagation();

		var player = GetInteractivePlayer();
		if ( !player.IsValid() )
			return;

		var perkType = TypeLibrary.GetType( typeof( PerkRandomChoice ) );
		if ( player.HasPerk( perkType ) )
		{
			var randomPerk = player.GetPerk( perkType ) as PerkRandomChoice;
			if ( randomPerk != null )
				randomPerk.OnRandom();
		}
	}

	protected override int BuildHash()
	{
		var player = GetViewedPlayer();
		if ( player is null || !player.IsValid() )
			return 0;

		bool canUseArmor = GetStat( player, PlayerStat.ArmorRerollCost ) > 0f && player.Armor >= (int)GetStat( player, PlayerStat.ArmorRerollCost );
		bool isFree = GetStat( player, PlayerStat.FreeRerollTime ) > 0f && player.RealTimeSinceLvlUp < GetStat( player, PlayerStat.FreeRerollTime );

		var timeLimitHash = GetStat( player, PlayerStat.ChooseTimeLimit ) > 0f ? player.RealTimeSinceOfferedChoices.Relative : 0f;
		var freeRerollHash = isFree ? player.RealTimeSinceLvlUp.Relative : 0f;
		var autoRerollTimerHash = player.GetDisplayedAutoRerollTimerProgress();

		var lockBanishHash = HashCode.Combine(
			player.NumBanishAvailable,
			player.HasGottenBanish
		);

		int buttonsHash = HashCode.Combine(
			player.NumRerollAvailable,
			canUseArmor,
			player.Armor,
			isFree,
			(int)GetStat( player, PlayerStat.SkipPerkNumRerolls ),
			(int)GetStat( player, PlayerStat.CanChooseRandomPerk )
		);

		var panelStateHash = HashCode.Combine(
			player.IsChoosingLevelUpReward,
			player.NumPerkPointsAvailable,
			player.GetDisplayedPerkChoiceCount(),
			player.PerkChoiceHash,
			player.IsBeingShownCurseChoices,
			IsReadOnly( player )
		);

		return HashCode.Combine(
			timeLimitHash,
			freeRerollHash,
			lockBanishHash,
			buttonsHash,
			panelStateHash,
			HashCode.Combine(
				autoRerollTimerHash,
				Manager.Instance.ShowBossHealthbar,
				Input.UsingController
			)
		);
	}
}
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@inherits Panel
@attribute [StyleSheet("PerkIcon.razor.scss")]


@{
	var player = Manager.Instance.LocalPlayer;
	var rot = Angle + Perk.IconAngleOffset;
	var scale = Perk.IconScale;
	const float levelUpAnimDuration = 0.5f;
	var levelUpScale = Perk.RealTimeSinceLevelUp < levelUpAnimDuration
		? Utils.Map( Perk.RealTimeSinceLevelUp, 0f, levelUpAnimDuration, 1.75f, 1.0f, EasingType.QuadOut )
		: 1.0f;
}

<root style="transform: rotate(@(rot)deg) scale(@(scale * levelUpScale));">
	@{
		// float opacity = (SS2Game.Current.IsGameOver || SS2Game.Current.SelectedPlayer != null) ? 4f : Utils.Map(Item.TimeSinceLevelUp, 0f, 3f, 4f, 1f);
		float opacity = Utils.Map( Math.Min( Perk.RealTimeSinceLevelUp, Perk.RealTimeSinceLevelDown ), 0f, 3f, 4f, 1f);
		<!-- todo: cache these -->
		var attrib = TypeLibrary.GetType(Perk.GetType()).GetAttribute<PerkAttribute>();
		var rarity = attrib.Rarity;
		var curse = attrib.Curse;
		var highlightColor = PerkManager.GetCardRarityColor(rarity, curse, alpha: 0.7f);
		var rarityBgTint = PerkManager.GetCardRarityColor(rarity, curse);
		var width = Math.Clamp(Perk.Level / (float)Perk.MaxLevel, 0f, 1f) * 100f;
		var maxLevel = PerkManager.GetMaxLevelForRarity(rarity);
	}
	<panel class="rarity" style="opacity: @opacity; background-color: @highlightColor.Rgba; background-image-tint:@rarityBgTint.Rgba;"></panel>

	@if (!curse && maxLevel > 1)
	{
		// @if (Perk.Level > (IsChoice ? 1 : 0))
		// {
			<panel class="progress" style="width:100%; background-color:@(new Color(0f, 0f, 0f, IsChoice ? 0.3f : 0.6f).Rgba); z-index: 1;"></panel>
		// }
	
		<panel class="progress" style="width:@(width)%; background-color:@highlightColor.Rgba; z-index: 2;"></panel>
	}
	
	<panel class="icon" style="background-image: url(@(Perk.GetImagePath( Perk.GetType() ))); background-color:@rarityBgTint.Rgba; opacity: @opacity;" />

	@if (Perk.Level > Perk.MaxLevel)
	{
		<div class="maxed">X</div>
	}

	@if ( !IsChoice && Perk.DisplayCooldown > 0f) // && !Manager.Instance.IsGameOver
	{
		<div class="cooldown" style="width:@(Math.Clamp(Perk.DisplayCooldown, 0f, 1f) * 100f)%; opacity:@(Utils.Map(Parent.Opacity, 0.3f, 1f, 1.3f, 0.75f)); background-color:@(Perk.DisplayCooldownColor.Rgba);"></div>
	}

	@if ( !string.IsNullOrEmpty(Perk.DisplayText)) //&& !Manager.Instance.IsGameOver && SS2Game.Current.SelectedPlayer == null)
	{
		<label class="display_text" style="color:@(Perk.DisplayTextColor.Rgba); opacity:@Perk.DisplayTextOpacity">@Perk.DisplayText</label>
	}

	@if (Banished)
	{
		<div class="banish">X</div>
	}

	@if (Perk.RealTimeSinceHighlight < Perk.HighlightDuration)
	{
		<div class="highlight" style="opacity:@(Utils.Map( Perk.RealTimeSinceHighlight, 0f, Perk.HighlightDuration, Perk.HighlightOpacity, 0f, EasingType.SineOut)); background-color:@(Perk.HighlightColor.Rgba);"></div>
	}

	@if(Perk.RealTimeSinceLevelDown < 3f) 
	{
		<div class="deleveled" style="opacity:@(Utils.Map( Perk.RealTimeSinceLevelDown, 0f, 3f, 4f, 0f, EasingType.QuadOut));"></div>
	}
</root>

@code
{
	public Perk Perk { get; set; }
	public bool NoTips { get; set; } = false;
	public bool Banished;

	public float Angle { get; set; }
	public float Scale { get; set; }

	public bool IsChoice { get; set; }

	protected override void OnMouseOver(MousePanelEvent e)
	{
		if (e.Target != this || NoTips || Perk == null)
			return;

		Manager.Instance.HoveredPerkType = TypeLibrary.GetType( Perk.GetType() );
		Manager.Instance.HoveredPerkPanel = this;
		Manager.Instance.HoveredPerkLevel = Perk.Level;
		Manager.Instance.IsHoveredPerkBanished = Banished;
		Manager.Instance.IsHoveredPerkAChoice = IsChoice;
		Manager.Instance.IsHoveredPerkHidden = false;

		Manager.Instance.HoveredPlayer = null;
		Manager.Instance.HoveredPlayerIcon = null;
	}

	protected override void OnMouseOut(MousePanelEvent e)
	{
		if ( Manager.Instance.HoveredPerkPanel != this ) return;
		Manager.Instance.HoveredPerkType = null;
		Manager.Instance.HoveredPerkPanel = null;
		Manager.Instance.IsHoveredPerkBanished = false;
	}

	protected override void OnAfterTreeRender ( bool firstTime )
	{
		base.OnAfterTreeRender( firstTime );

		if ( !IsVisible && Manager.Instance.HoveredPerkPanel == this )
		{
			Manager.Instance.HoveredPerkType = null;
			Manager.Instance.HoveredPerkPanel = null;
			Manager.Instance.IsHoveredPerkBanished = false;
		}
	}

	protected override int BuildHash()
	{
		var fadeHash = Math.Min(Perk.RealTimeSinceLevelUp, Perk.RealTimeSinceLevelDown) < 3f ? RealTime.Now : 0f;
		var highlightHash = Perk.RealTimeSinceHighlight.Relative < Perk.HighlightDuration ? Perk.RealTimeSinceHighlight.Relative : 0f;
		var cooldownHash = Perk.DisplayCooldown > 0f ? HashCode.Combine(Perk.DisplayCooldown, Parent.Opacity, Perk.DisplayCooldownColor) : 0f;
		var textHash = !string.IsNullOrEmpty(Perk.DisplayText) ? HashCode.Combine(Perk.DisplayText, Perk.DisplayTextColor, Perk.DisplayTextOpacity) : 0f;
		var transformHash = HashCode.Combine(Angle, Perk.IconAngleOffset, Perk.IconScale);
		return HashCode.Combine(
			Perk.Level, 
			fadeHash, 
			textHash,
			Manager.Instance.IsGameOver, 
			cooldownHash, 
			highlightHash,
			transformHash
		);
	}

	protected override void OnClick(MousePanelEvent e)
	{
		// if non-local player is selected, do nothing
		if (Manager.Instance.SelectedPlayer.IsValid())
			return;

		var player = Manager.Instance.LocalPlayer;
		if ( !player.IsValid() )
			return;

		if( player.Stats[PlayerStat.ClickAddPerk] > 0f )
		{
			var perkType = TypeLibrary.GetType( typeof( PerkClickAdd ) );
			if ( player.HasPerk( perkType ) )
			{
				var perk = player.GetPerk( perkType ) as PerkClickAdd;
				if ( perk != null ) 
				{
					perk.Activate(TypeLibrary.GetType(Perk.GetType()));

					Manager.Instance.HoveredPerkType = null;
					Manager.Instance.IsHoveredPerkBanished = false;

					e.StopPropagation();
				}
			}
		}
	}

	protected override void OnRightClick(MousePanelEvent e)
	{
		// if non-local player is selected, do nothing
		if (Manager.Instance.SelectedPlayer.IsValid())
			return;

		var player = Manager.Instance.LocalPlayer;
		if ( !player.IsValid() )
			return;

		if( player.Stats[PlayerStat.ClickRemovePerk] > 0f )
		{
			var perkType = TypeLibrary.GetType( typeof( PerkClickRemove ) );
			if ( player.HasPerk( perkType ) )
			{
				var perk = player.GetPerk( perkType ) as PerkClickRemove;
				if ( perk != null ) 
				{
					perk.Activate(TypeLibrary.GetType(Perk.GetType()));

					Manager.Instance.HoveredPerkType = null;
					Manager.Instance.IsHoveredPerkBanished = false;

					e.StopPropagation();
				}
			}
		}

		// player.LevelDownPerk(TypeLibrary.GetType(Perk.GetType()));
	}
}
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@inherits PanelComponent
@attribute [StyleSheet("PerkWorldPanel.razor.scss")]

@if(PerkItem.ShouldHidePanel)
{
	return;
}

<root>
	@{
		var bgColor = PerkManager.GetCardRarityColor(PerkItem.PerkRarity, PerkItem.Curse);
	}
	<panel class="rarity" style="background-color:@(bgColor.Rgba);">
		<panel class="icon" style="background-image: url(@(PerkItem.IconPath)); " />
	</panel>
</root>

@code
{
	[Property] public PerkItem PerkItem { get; set; }

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

@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@inherits Panel
@attribute [StyleSheet("PlayerIcon.razor.scss")]

@{
	var borderColor = Manager.Instance.SelectedPlayer.IsValid()
				? Color.Lerp(Color.White, new Color(0.5f, 0.5f, 1f), Utils.FastSin(Time.Now * 24f))
				: Color.White;
}

<root style="border: @(Player == Manager.Instance.SelectedPlayer ? 1 : 0)px solid @(borderColor.WithAlpha(0.75f).Rgba);">
	<div class="icon" style="opacity:@((Player == Manager.Instance.SelectedPlayer ? 1f : 0.5f) * (Player.IsDead ? 0.1f : 1f) * (IsHovering ? 1f : 0.8f));">
		<img src="avatar:@Player.Network.Owner.SteamId" />
		<div class="hp_bg"></div>
		<div class="hp_delta" style="width:@((Player.Health / Player.GetSyncStat(PlayerStat.MaxHp)) * 100f)%;"></div>
		<div class="hp_fill" style="width:@((Player.Health / Player.GetSyncStat(PlayerStat.MaxHp)) * 100f)%;"></div>
	</div>
	<div class="player_level">@(Player.Level)</div>
</root>

@code
{
	public Player Player { get; set; }
	public bool IsHovering;

	protected override void OnClick(MousePanelEvent e)
	{
		base.OnClick(e);

		if (Manager.Instance.SelectedPlayer == Player)
			Manager.Instance.SelectedPlayer = null;
		else
			Manager.Instance.SelectedPlayer = Player;

		e.StopPropagation();
	}

	protected override void OnMouseOver(MousePanelEvent e)
	{
		if(e.Target != this)
			return;

		Manager.Instance.HoveredPlayerIcon = Player;
		IsHovering = true;
	}

	protected override void OnMouseOut(MousePanelEvent e)
	{
		Manager.Instance.HoveredPlayerIcon = null;
		IsHovering = false;
	}

	protected override int BuildHash()
	{
		return HashCode.Combine(RealTime.Now);
	}
}
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@using System.Text.Json;
@inherits Panel
@attribute [StyleSheet("PlayerProfilePanel.razor.scss")]

<root>
	<div class="hide_button" onclick=@(() => HideProfile())></div>

	@if(PlayerStats is null)
	{
		@* <div class="loading">
			<label>@($"Loading...")</label>
		</div> *@

		return;
	}

	<div class="name_container">
		<div class="avatar" style="background-image: url( avatar:@Manager.Instance.PlayerProfileToShow.steamId )"></div>

		@{
			var displayName = Manager.Instance.PlayerProfileToShow.displayName;
		}

		<div class="displayName">@displayName</div>
	</div>

	<div class="main-content">
		@* @if( _isLoadingPerks)
		{
			<div class="loading_perks">@($"Loading perks...")</div>
		}
		else
		{
			if(_perkPickPercents.Count > 0)
			{
				<div class="perks_container">
					<div class="perks_title">@($"Favorite Perks:")</div>

					<div class="perks">
						@{
							foreach(var pair in _perkPickPercents.OrderByDescending(x => x.Value).Take(10))
							{
								var perkType = pair.Key;
								<PerkIconStatic style="height: 100%;" PerkType=@perkType Level=@(1) HideLevel=@true [email protected] />
							}
						}
					</div>
				</div>
			}
		} *@

		@* <div class="stats_title">
			Stats
		</div> *@

		<div class="stats">
			<DifficultyPanel DontChangeGameDifficulty=@true />

			@{
				var numRuns = GetStatSum(StatType.NumRuns);
				var numWins = GetStatSum(StatType.NumVictory);
				// var numLosses = GetStatSum(StatType.NumDefeat);
				// var numResets = Math.Max(numRuns - (numWins + numLosses), 0);

				var winPercent = numRuns > 0 ? (numWins / (float)numRuns) * 100f : 0f;
				
				string winRateString;
				if(winPercent >= 1f) winRateString = $"{winPercent:0.#}%";
				else if(winPercent >= 0.1f) winRateString = $"{winPercent:0.##}%";
				else if(winPercent >= 0.01f) winRateString = $"{winPercent:0.###}%";
				else if(winPercent > 0f) winRateString = $"{winPercent:0.####}%";
				else winRateString = "0%";

				var numKills = GetStatSum(StatType.NumKills);
				var numMinibossKills = GetStatSum(StatType.NumMinibossKills);
				var fastestWinScore = GetStatMax(StatType.LeaderboardRun);
				var hasFastestWin = fastestWinScore > Manager.VICTORY_OFFSET / 2f;
				var fastestWinTime = hasFastestWin ? Manager.VICTORY_OFFSET - fastestWinScore : 0f;
				var fastestWinString = hasFastestWin ? Utils.FormatTime(fastestWinTime) : "...";

				var i = 0;

				PlayerStatsToShow.Clear();
				PlayerStatsToShow.Add(new PlayerProfileStatData("Victories", numWins.ToString("N0"), new Color(1f, 1f, 0.5f), "win", GetFontSizeForNumber(numWins)));
				PlayerStatsToShow.Add(new PlayerProfileStatData("Fastest Victory", fastestWinString, new Color(0.5f, 1f, 0.5f), "clock"));
				PlayerStatsToShow.Add(new PlayerProfileStatData("Attempts", numRuns.ToString("N0"), new Color(0.8f), "num_runs", GetFontSizeForNumber(numRuns)));
				PlayerStatsToShow.Add(new PlayerProfileStatData("Winrate", winRateString, new Color(0.6f, 0.6f, 0.9f, 0.7f), "win_rate"));

				PlayerStatsToShow.Add(new PlayerProfileStatData("Enemy Kills", numKills.ToString("N0"), new Color(0.8f, 0.2f, 0.2f), "kill", GetFontSizeForNumber(numKills)));
				PlayerStatsToShow.Add(new PlayerProfileStatData("Miniboss Kills", numMinibossKills.ToString("N0"), new Color(0.9f, 0.4f, 0.1f), "kill_miniboss", GetFontSizeForNumber(numMinibossKills)));
			}

			<div class="list_container">
				@foreach(var data in PlayerStatsToShow)
				{
					<div class="flat list" style="background-color: @((i % 2 == 0 ? new Color(0, 0, 0, 0.4f) : new Color(0, 0, 0, 0.7f)).Rgba);">
						<div>
							<div class="icon_container">
								<label class="icon" style="background-color:@(data.color.Rgba); mask-image:@($"url(/textures/ui/stats/{data.icon}.png)")"></label>
							</div>

							<label class="bold stat_name" style="font-size:@(data.title.Length > 15 ? Math.Round(Utils.Map(data.title.Length, 15, 36, 14f, 10f, EasingType.SineIn)) : 14)px; color:@(Color.Lerp(data.color, Color.White, (i % 2 == 0 ? 0.5f : 0.4f)).Rgba);">@(data.title)</label>
						</div>

						<div class="values">
							@{
								var color = (data.text == "0" || data.text == "0%" || data.text == "...")
									? Color.Lerp(data.color, new Color(0.5f), 0.75f).WithAlpha(0.5f).Rgba
									: data.color.Rgba;
							}

							<label class="bold stat_value" style="color:@(color); font-size:@(data.fontSize)px;">@(data.text)</label>
						</div>
					</div>

					@{
						i++;
					}
				}
			</div>
		</div>
	</div>
</root>

@code
{
	public struct PlayerProfileStatData
	{
		public string title;
		public string text;
		public Color color;
		public string icon;
		public float fontSize;

		public PlayerProfileStatData(string _title, string _text, Color _color, string _icon, float _fontSize = 16f)
		{
			title = _title;
			text = _text;
			color = _color;
			icon = _icon;
			fontSize = _fontSize;
		}
	}

	public List<PlayerProfileStatData> PlayerStatsToShow { get; set; } = new();

	private bool _isLoadingPerks;
	private long _loadedSteamId;

	public Sandbox.Services.Stats.PlayerStats PlayerStats { get; set; }

	private Dictionary<TypeDescription, float> _perkPickPercents = new();

	protected override void OnAfterTreeRender(bool firstTime)
	{
		base.OnAfterTreeRender(firstTime);

		var currentSteamId = Manager.Instance.PlayerProfileToShow?.steamId ?? 0;
		if (firstTime || currentSteamId != _loadedSteamId)
		{
			_loadedSteamId = currentSteamId;
			Refresh();
		}
	}

	async void Refresh()
	{
		PlayerStats = null;
		StateHasChanged();

		_isLoadingPerks = true;

		PlayerStats = Sandbox.Services.Stats.GetPlayerStats("facepunch.ss2", Manager.Instance.PlayerProfileToShow.steamId);
		await PlayerStats.Refresh();

		_isLoadingPerks = false;

		_perkPickPercents.Clear();

		/*
		foreach(var type in TypeLibrary.GetTypes<Perk>())
		{
			var attrib = type.GetAttribute<PerkAttribute>();
			if(attrib == null)
				continue;

			if(attrib.Disabled)
				continue;

			if(attrib.Curse)
				continue;

			var timesChosen = (int)PlayerStats.Get(Manager.GetPerkStatString(StatType.PerkChosen, type)).Sum;
			var timesIgnored = (int)PlayerStats.Get(Manager.GetPerkStatString(StatType.PerkIgnored, type)).Sum;

			// if( timesChosen + timesIgnored > 0 && (timesChosen > 2 || timesIgnored > 3) )
			if( timesChosen >= 2 )
			{
				var percent = MathX.Clamp(timesChosen / (float)(timesChosen + timesIgnored), 0f, 1f);
				// Log.Info($"{type}: chosen {timesChosen}, ignored {timesIgnored} - percent: {percent}");

				_perkPickPercents[type] = percent;
			}
		}
		*/

		// Log.Info($"Loaded {_perkPickPercents.Count} perks for player profile.");

		// var totalWinTime = 0f;
		// for(int difficulty = 1; difficulty <= Manager.MaxDifficulty; difficulty++)
		// {

		// }

		StateHasChanged();

		// var zombies = stats.Get("zombies_killed");

		// Log.Info($"Garry has killed {zombies.Sum} zombies!");

		// Log.Info($"Refreshed leaderboard with {Leaderboard.Entries.Count()} entries.");
	}

	protected override int BuildHash()
	{
		return HashCode.Combine(
			DifficultyPanel.DifficultyToDisplay,
			Manager.Instance.PlayerProfileToShow
		);
	}

	public void HideProfile()
	{
		Manager.Instance.PlayerProfileToShow = null;
	}

	public int GetStatSum( StatType statType )
	{
		if( DifficultyPanel.DifficultyToDisplay == -1 )
		{
			int total = 0;
			for(int diff = Manager.MinDifficulty; diff <= Manager.MaxDifficulty; diff++)
			{
				total += (int)PlayerStats.Get(Manager.GetStatString(statType, numPlayers: 1, diff)).Sum;
			}
			return total;
		}
		else
		{
			return (int)PlayerStats.Get(Manager.GetStatString(statType, numPlayers: 1, DifficultyPanel.DifficultyToDisplay)).Sum;
		}
	}

	public float GetStatMax( StatType statType )
	{
		if( DifficultyPanel.DifficultyToDisplay == -1 )
		{
			float max = 0f;
			for(int diff = Manager.MinDifficulty; diff <= Manager.MaxDifficulty; diff++)
			{
				var val = (float)PlayerStats.Get(Manager.GetStatString(statType, numPlayers: 1, diff)).Max;
				if(val > max)
				max = val;
			}

			return max;
		}
		else
		{
			return (float)PlayerStats.Get(Manager.GetStatString(statType, numPlayers: 1, DifficultyPanel.DifficultyToDisplay)).Max;
		}
	}

	public static float GetFontSizeForNumber( float number )
	{
		if(number >= 1000000000f) return 10f;
		if(number >= 100000000f) return 11f;
		if(number >= 10000000f) return 12f;
		if(number >= 1000000f) return 14f;
		return 16f;
	}
}
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@inherits Panel
@attribute [StyleSheet("PlayerTooltip.razor.scss")]

<root>
	@{
		string name = "";
	}

	@if(Player != null )
	{
		@if(ShowIcon) 
		{
			<div class="icon">
				<img src="avatar:@Player.Network.Owner.SteamId" />
				<div class="level">@Player.Level</div>
			</div>
		}

		@{
			name = Player?.Network.Owner?.DisplayName ?? "";
		}
	}
	else
	{
		name = Name ?? "";
	}

	<div class="name">@name</div>
	@if(Player != null && Player.Network.Owner != null)
	{
		<div class="ping">@($"{Player.Network.Owner.Ping:0}ms")</div>
	}
</root>

@code {
	public Player Player { get; set; }
	public bool ShowIcon { get; set; }

	public string Name { get; set; }

	public override void Tick()
	{
		SetClass( "hidden", Input.UsingController );

		var mousePos = Mouse.Position;

		if (mousePos.x > Screen.Width * 0.9f)
		{
			mousePos += new Vector2(-20f, 20f);
			var mousePosRelative = mousePos / Screen.Size;
			Style.Right = Length.Fraction(1f - mousePosRelative.x);
			Style.Top = Length.Fraction(mousePosRelative.y);
		}
		else
		{
			mousePos += new Vector2(20f, 20f);
			var mousePosRelative = mousePos / Screen.Size;
			Style.Left = Length.Fraction(mousePosRelative.x);
			Style.Top = Length.Fraction(mousePosRelative.y);
		}
	}

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

@namespace Sandbox
@inherits Panel
@using Sandbox.UI
@implements IRichTextPanel
@attribute [RichTextPanel( @"([+-]?\d+(?:\.\d+)?[%ms]?)[ \t]*(?:→|->)[ \t]*([+-]?\d+(?:\.\d+)?[%ms]?)", UseCaptureGroup = false )]


<root class="richtext-arrow-result">
	<label class="val val1" style="color: @(Color.Hex);">@Text1</label>
	<label>→</label>
	<label class="val val2" style="color: @(Color.Hex);">@Text2</label>
</root>

<style>
	.val1
	{
		opacity: 0.2;
	}
</style>


@code
{
	public virtual Color Color => Color.Green;
	public float? ImageSize { get; set; }

	string Text1;
	string Text2;

	public void ParseRichText(string text)
	{
		// Log.Info( "Parsing arrow result: " + text );
		var split = text.Split( "->");
		if(split.Length == 1)
		{
			split = text.Split( "→" );
		}

		if(split.Length == 2)
		{
			Text1 = split[0];
			Text2 = split[1];

			if ( Text1.StartsWith( "[-]" ) || Text1.StartsWith( "[+]" ) )
			{
				Text1 = Text1[3..];
			}

			if ( Text2.EndsWith( "[/-]" ) || Text2.EndsWith( "[/+]" ) )
			{
				Text2 = Text2[..^4];
			}

			if(Text1.StartsWith("+") && !Text2.StartsWith("-") && !Text2.StartsWith("+"))
			{
				Text2 = "+" + Text2;
			}
		}
		else
		{
			Text1 = text;
			Text2 = "";
		}
	}

	protected override int BuildHash()
	{
		return System.HashCode.Combine( Text1, Text2, Color );
	}
}
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace Sandbox

@if (so == null)
    return;

<root>
	
    <div class="left-panel">

        <h1>Free Cam</h1>

        <div class="row">
            <h2>Timescale</h2>
            <SliderControl Property="@so.GetProperty( nameof(Timescale) )"></SliderControl>
        </div>

        <div class="row">
            <h2>Camera Smoothing</h2>
            <SliderControl Property="@so.GetProperty(nameof(CameraSmoothing))"></SliderControl>
        </div>

        <h1>Depth Of Field</h1>

        <div class="row">
            <h2>Blur Amount</h2>
            <SliderControl Property="@so.GetProperty(nameof(DofBlur))"></SliderControl>
        </div>

        <div class="row">
            <h2>Focal Distance</h2>
            <SliderControl Property="@so.GetProperty(nameof(DofFocalDistance))"></SliderControl>
        </div>

        <div class="row">
            <h2>Focal Range</h2>
            <SliderControl Property="@so.GetProperty(nameof(DofFocalRange))"></SliderControl>
        </div>

    </div>

    

</root>

@code
{
    SerializedObject so;

    [Range(0.0f, 2.0f), Step( 0.1f )]
    public float Timescale
    {
        get => field;
        set 
        {
            field = value;

            if (!Networking.IsHost)
                return;

            Scene.TimeScale = value;
        }
    }

    [Range(0.0f, 1.0f), Step(0.1f)]
    public float CameraSmoothing { get; set; } = 0.7f;


    [Range(0.0f, 1.0f), Step(0.01f)]
    public float DofBlur { get; set; } = 0.0f;

    [Range(0.0f, 1000.0f), Step(1f)]
    public float DofFocalDistance { get; set; } = 300.0f;

    [Range(0.0f, 1000.0f), Step(1f)]
    public float DofFocalRange { get; set; } = 500.0f;

    protected override void OnStart()
    {
        so = TypeLibrary.GetSerializedObject(this);

        base.OnStart();
    }

    protected override void OnEnabled()
    {
        base.OnEnabled();

        Timescale = Timescale;
    }

    public void Update( bool isActive )
    {
        SetClass("active", isActive);

        var volume = GetOrAddComponent<PostProcessVolume>();
        volume.SceneVolume = new Volumes.SceneVolume { Type = Volumes.SceneVolume.VolumeTypes.Infinite };
        volume.Priority = 1000;

        UpdateDepthOfField();
    }

    void UpdateDepthOfField()
    {
        var dof = GetOrAddComponent<DepthOfField>();
        dof.Enabled = DofBlur > 0.01f;
        dof.BlurSize = DofBlur * 50.0f;
        dof.FocalDistance = DofFocalDistance;
        dof.FocusRange = DofFocalRange;
    }
}
@using Sandbox;
@using Sandbox.UI;
@attribute [InspectorEditor(null)]
@attribute [Order(100)]
@inherits Panel
@namespace Sandbox
@implements IInspectorEditor

<root>
    <div class="body">
            @if (Target == null || Target.Count == 0)
            {
            <div class="empty-state">#spawnmenu.inspect.click_to_inspect</div>
            }
            else
            {
                var totalMass = Target.SelectMany( go => go.GetComponentsInChildren<Rigidbody>() ).Sum( ResolveMass );
                var health = Target.Select( go => go.GetComponent<Prop>() ).FirstOrDefault( p => p.IsValid() );

                <div class="object-info">
                    @if ( totalMass > 0 )
                    {
                        <span>⚖️ @($"{totalMass:0.#} kg")</span>
                    }
                    @if ( health.IsValid() && health.Health != 0 )
                    {
                        <span>❤️ @($"{health.Health:0.#} HP")</span>
                    }
                </div>
                <ControlSheet Target="@Properties"></ControlSheet>

                @if (Renderers.Count > 0)
                {
                    @if ( MaterialGroups.Count > 1 )
                    {
                        var currentGroup = Renderers[0].MaterialGroup ?? MaterialGroups[0];
                        <div class="material-row">
                        <label>#spawnmenu.inspect.skin</label>
                            <div class="material-button" @onclick="@PickMaterialGroup">
                                <label>@currentGroup</label>
                                <label class="material-group-arrow">▾</label>
                            </div>
                        </div>
                    }

                    var accessor = Renderers[0].Materials;
                    @for (int i = 0; i < accessor.Count; i++)
                    {
                        var index = i;
                        var hasOverride = accessor.HasOverride(index);
                        var mat = hasOverride ? accessor.GetOverride(index) : accessor.GetOriginal(index);
                        var name = mat?.ResourceName ?? "Default";

                        <div class="material-row @(hasOverride ? "overridden" : "")">
                        <label>#spawnmenu.inspect.material</label><label> @(index + 1)</label>
                            <div class="material-button" @onclick=@(() => PickMaterial(index))>
                                <div class="material-preview" style="background-image: url( thumb:@(mat?.ResourcePath) )"></div>
                                <label>@name</label>
                            </div>
                            @if (hasOverride)
                            {
                                <div class="material-revert" @onclick=@(() => RevertMaterial(index))>x</div>
                            }
                        </div>
                    }
                }
            }
    </div>
</root>

@code
{
    public string Title => Target?.Count switch
    {
        null or < 2 => "📦 " + Game.Language.GetPhrase( "spawnmenu.inspect.object" ),
        _ => "📦 " + Game.Language.GetPhrase( "spawnmenu.inspect.object" ) + $" (+{Target.Count - 1})"
    };

    public List<GameObject> Target { get; private set; }

    public bool TrySetTarget(List<GameObject> selection)
    {
        var ids = selection.Select(x => x.Id);
        if (!ids.SequenceEqual(Target?.Select(x => x.Id) ?? []))
        {
            Target = selection.Any() ? selection.ToList() : null;
            RebuildFromTarget();
            StateHasChanged();
        }

        // Hide the tab when something is selected but there's nothing to show
        return Target == null || InspectorHasContent();
    }

    // Frozen rigidbodies report Mass = 0 from the live physics body, so fall back
    // to PhysicalProperties.Mass and MassOverride before giving up.
    static float ResolveMass( Rigidbody rb )
    {
        if ( !rb.IsValid() ) return 0f;
        var mo = rb.GetComponent<PhysicalProperties>();
        if ( mo.IsValid() && mo.Mass > 0f ) return mo.Mass;
        if ( rb.MassOverride > 0f ) return rb.MassOverride;
        return rb.Mass;
    }

    bool InspectorHasContent()
    {
        if ( Target == null ) return false;
        if ( Properties.Count > 0 || Renderers.Count > 0 ) return true;

        var totalMass = Target.SelectMany( go => go.GetComponentsInChildren<Rigidbody>() ).Sum( ResolveMass );
        if ( totalMass > 0 ) return true;

        var health = Target.Select( go => go.GetComponent<Prop>() ).FirstOrDefault( p => p.IsValid() );
        if ( health.IsValid() && health.Health != 0 ) return true;

        return false;
    }

    List<SerializedProperty> Properties = new();
    List<ModelRenderer> Renderers = new();
    List<string> MaterialGroups = new();

    protected override int BuildHash()
    {
        var hc = new HashCode();
        foreach ( var go in Target ?? [] )
        {
            hc.Add( go.Id );
            hc.Add( ResolveMass( go.GetComponent<Rigidbody>() ) );
            hc.Add( go.GetComponent<Prop>()?.Health ?? -1f );
            hc.Add( go.GetComponent<ModelRenderer>()?.MaterialGroup );
        }
        return hc.ToHashCode();
    }

    protected override void OnParametersSet()
    {
        base.OnParametersSet();
        RebuildFromTarget();
    }

    void RebuildFromTarget()
    {
        Properties = new();
        Renderers = new();
        MaterialGroups = new();

        if (Target == null) return;

        foreach (var c in Target.SelectMany(x => x.Components.GetAll()).Distinct().GroupBy(x => x is Collider ? typeof(Collider) : x.GetType()))
        {
            CollectProperties(c.ToArray());
        }
    }

    bool HasEditableProperties(Type type, PropertyDescription[] properties)
    {
        if (type.IsAssignableTo(typeof(ModelRenderer))) return true;
        if (type.IsAssignableTo(typeof(Collider))) return true;

        foreach (var prop in properties)
        {
            if (prop.HasAttribute<ClientEditableAttribute>())
                return true;
        }

        return false;
    }

    void CollectProperties(Component[] components)
    {
        var firstComponent = components.First();

        var tl = TypeLibrary.GetType(firstComponent.GetType());
        if (tl is null) return;

        if (!HasEditableProperties(firstComponent.GetType(), tl.Properties)) return;

        var so = new MultiSerializedObject();
        so.OnPropertyChanged = PropertyChanged;

        foreach (var component in components)
            so.Add(TypeLibrary.GetSerializedObject(component));

        so.Rebuild();

        foreach (var prop in tl.Properties)
        {
            if (!prop.HasAttribute<ClientEditableAttribute>()) continue;
            Properties.Add(so.GetProperty(prop.Name));
        }

        if (firstComponent is ModelRenderer mr)
        {
            Renderers.AddRange(components.OfType<ModelRenderer>());

            var model = mr.Model;
            if ( model is not null )
            {
                for ( int i = 0; i < model.MaterialGroupCount; i++ )
                    MaterialGroups.Add( model.GetMaterialGroupName( i ) );
            }

            var prop = mr.GetComponent<Prop>();
            if (prop is not null)
            {
                var propso = TypeLibrary.GetSerializedObject(prop);
                propso.OnPropertyChanged = PropertyChanged;
                Properties.Add(propso.GetProperty(nameof(ModelRenderer.Tint)));
            }
            else
            {
                Properties.Add(so.GetProperty(nameof(ModelRenderer.Tint)));
            }

            Properties.Add(so.GetProperty(nameof(ModelRenderer.RenderType)));
        }

        if (firstComponent is Collider)
            Properties.Add(so.GetProperty(nameof(Collider.Surface)));
    }

    void PropertyChanged(SerializedProperty prop)
    {
        foreach (var c in prop.Parent.Targets)
        {
            if (c is Component component)
                GameManager.ChangeProperty(component, prop.Name, prop.GetValue<object>());
        }
    }

    void PickMaterialGroup()
    {
        var menu = MenuPanel.Open( this );
        var current = Renderers[0].MaterialGroup ?? MaterialGroups.FirstOrDefault();
        foreach ( var group in MaterialGroups )
        {
            var g = group;
            menu.AddOption( current == g ? "check" : "", g, () => SetMaterialGroup( g ) );
        }
    }

    void SetMaterialGroup( string group )
    {
        foreach ( var renderer in Renderers )
            GameManager.ChangeProperty( renderer, nameof( ModelRenderer.MaterialGroup ), group );
    }

    void PickMaterial(int index)
    {
        var accessor = Renderers[0].Materials;
        var mat = accessor.HasOverride(index) ? accessor.GetOverride(index) : accessor.GetOriginal(index);

        var popup = new ResourceSelectPopup();
        popup.Extension = "material";
        popup.CurrentValue = mat?.ResourcePath;
        popup.AllowPackages = true;
        popup.Parent = FindPopupPanel();
        popup.OnSelectedFile = (path) => SetMaterialOverride(index, path);
    }

    void SetMaterialOverride(int index, string path)
    {
        foreach (var renderer in Renderers)
            GameManager.ChangeMaterialOverride(renderer, index, path);
    }

    void RevertMaterial(int index)
    {
        foreach (var renderer in Renderers)
            GameManager.ChangeMaterialOverride(renderer, index, null);
    }
}
@using Sandbox;
@using Sandbox.UI;
@namespace Sandbox
@inherits Panel

<root>

	<div class="canvas" @ref=_canvas></div>

	<div class="inspector-panel @(_editors?.Any( e => e.WasVisible ) == true ? "" : "hidden")" @onmousedown="@WindowPress">
		<div class="tab-bar">
			@foreach ( var entry in _editors?.Where( e => e.WasVisible ) ?? [] )
			{
				var e = entry;
				<div class="tab @( _active == e ? "active" : "" )" @onclick="@(() => SetActive( e ))">
					@e.Editor.Title
				</div>
			}
		</div>
		<div class="window-stack" @ref=_windowStack></div>
	</div>

</root>

@code
{
    public GameObject Hovered => hovered;

    Panel _canvas = default;
    Panel _windowStack = default;

    record EditorEntry( IInspectorEditor Editor )
    {
        public bool WasVisible { get; set; }
    }
    List<EditorEntry> _editors;
    EditorEntry _active;

    void InitEditors()
    {
        if ( _editors != null || _windowStack == null ) return;
        _editors = new();

        var types = TypeLibrary.GetTypesWithAttribute<InspectorEditorAttribute>()
            .OrderByDescending( x => x.Type.GetAttribute<OrderAttribute>()?.Value ?? 0 )
            .ThenBy( t => t.Attribute.Type is null ? 1 : 0 );

        foreach ( var (typeDesc, attr) in types )
        {
            var editor = typeDesc.Create<IInspectorEditor>();
            if ( editor is not Panel editorPanel ) continue;

            editorPanel.AddClass( "window" );
            editorPanel.SetClass( "hidden", true );
            editorPanel.Parent = _windowStack;

            _editors.Add( new EditorEntry( editor ) );
        }
    }

    void SetActive( EditorEntry entry )
    {
        _active = entry;
        ApplyVisibility();
    }

    void ApplyVisibility()
    {
        foreach ( var e in _editors )
            (e.Editor as Panel)?.SetClass( "hidden", !( e.WasVisible && e == _active ) );
    }

    HashSet<GameObject> _lastSelected = new();

    void UpdateEditors()
    {
        if ( _editors == null ) return;

        bool changed = false;

        foreach ( var entry in _editors )
        {
            bool visible = entry.Editor.TrySetTarget( _selected );
            if ( visible != entry.WasVisible ) changed = true;
            entry.WasVisible = visible;
        }

        if ( !_lastSelected.SetEquals( _selected ) )
        {
            changed = true;
            _lastSelected = _selected.ToHashSet();
        }

        if ( changed )
        {
            var visible = _editors.Where( e => e.WasVisible ).ToList();
            if ( _active == null || !_active.WasVisible )
                _active = visible.LastOrDefault();

            ApplyVisibility();
        }
    }

    protected override int BuildHash() => HashCode.Combine( hovered, _selected.Count, _active );

    public override void Tick()
    {
        _selected.RemoveAll( x => !x.IsValid() );
        InitEditors();
        UpdateEditors();
        UpdateHighlights();
        UpdateCursor();
    }

    protected override void OnVisibilityChanged()
    {
        UpdateHighlights();
    }

    void UpdateHighlights()
    {
        var host = Ancestors.OfType<ContextMenuHost>().FirstOrDefault();
        if (host is null) return;

        if (host.SelectedOutline is null || host.HoveredOutline is null)
            return;

        host.SelectedOutline.Targets ??= new();
        host.SelectedOutline.Targets.Clear();
        host.SelectedOutline.Color = new Color(4.7f, 10.1f, 30.6f, 1);
        host.SelectedOutline.ObscuredColor = new Color(2.2f, 2.3f, 2.9f, 0.1f);
        host.SelectedOutline.Width = 0.2f;

        host.HoveredOutline.Targets ??= new();
        host.HoveredOutline.Targets.Clear();
        host.HoveredOutline.Color = new Color(2.6f, 2.0f, 0.2f, 1);
        host.HoveredOutline.ObscuredColor = new Color(2.6f, 2.0f, 0.2f, 0.1f);
        host.HoveredOutline.Width = 0.2f;

        if (!IsVisible)
            return;

        host.SelectedOutline.Targets.AddRange(_selected.SelectMany(x => GetRenderers( x ) ) ?? []);

        if ( !_selected.Contains( hovered  ) )
        {
            host.HoveredOutline.Targets = GetRenderers( Hovered ).ToList();
        }
        else
        {
            host.HoveredOutline.Targets = default;
        }
    }

    IEnumerable<Renderer> GetRenderers( GameObject o )
    {
        if ( o == null ) yield break;

        foreach ( var r in o.GetComponents<Renderer>() )
        {
            yield return r;
        }

        foreach( var c in o.Children )
        {
            if (c.NetworkMode == NetworkMode.Object) continue;

            foreach (var rr in GetRenderers( c ) )
            {
                yield return rr;
            }
        }
    }

    GameObject hovered;

    List<GameObject> _selected = new();

    void UpdateCursor()
    {
        var cursorPos = Mouse.Position;
        var screenRay = Scene.Camera.ScreenPixelToRay( cursorPos );
        var tr = Scene.Trace.Ray(screenRay, 4096 )
                            .IgnoreGameObjectHierarchy( Player.FindLocalPlayer()?.GameObject )
                            .Run();

        var go = tr.Collider?.GameObject ?? tr.GameObject;
        go = go.FindNetworkRoot();

        if (!_canvas.HasHovered) go = default;
        if (!CanSelect(go)) go = null;

        UpdateHovered(go);
    }

    bool CanSelect( GameObject o )
    {
        if (o == null) return false;
        if (o.Tags.Has("world")) return false;
        if (o.NetworkMode == NetworkMode.Never) return false;

        o = o?.FindNetworkRoot();

        return true;
    }

    void UpdateHovered( GameObject o )
    {
        o = o?.FindNetworkRoot();

        if (hovered == o) return;

        hovered = o;
        PlaySound("ui.button.over");
    }


    public void WorldMouseDown(MousePanelEvent e)
    {
        SelectObject(hovered);
    }

    public void WorldMouseRightDown(MousePanelEvent e)
    {
        if ( !hovered.IsValid() ) return;

        SelectObject( hovered );

        var target = hovered;
        var isPlayer = target.Tags.Has( "player" );
        var prop = target.GetComponent<Prop>();
        var isGibbable = prop.IsValid() && prop.Health > 0;

        var menu = MenuPanel.Open( this );
        if ( !isPlayer )
        {
            menu.AddOption( "🗑️", Game.Language.GetPhrase( "spawnmenu.inspect.delete" ), () => GameManager.DeleteInspectedObject( target ) );
        }

        if ( isGibbable )
        {
            menu.AddOption( "💥", Game.Language.GetPhrase( "spawnmenu.inspect.break" ), () => GameManager.BreakInspectedProp( prop ) );
        }
    }

    public void WorldMouseUp(MousePanelEvent e)
    {
        // nothing.
    }

    public void SelectObject( GameObject o )
    {
        if ( !o.IsValid() )
        {
            _selected.Clear();
        }
        else
        {
            if (!Input.Down("run"))
                _selected.Clear();

            _selected.Remove(o);
            _selected.Add(o);
        }

        _selected = _selected.Distinct().ToList();
    }

	void WindowPress( PanelEvent panelEvent )
	{
		panelEvent.StopPropagation();
	}
}


@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox

<root>
    @if ( _selected.IsValid() )
    {
        <div class="header">
            <h2>
                <span>🎨</span>
                <span>@_selected.Title</span>
            </h2>
        </div>

        @if ( _properties.Count > 0 )
        {
            <ControlSheet Target="@_properties" />
        }
        else
        {
            <div class="empty-state">#spawnmenu.effects.no_properties</div>
        }
    }
    else
    {
        <div class="empty-state">#spawnmenu.effects.select_effect</div>
    }
</root>

@code
{
    PostProcessManager _manager;
    PostProcessResource _selected;
    List<SerializedProperty> _properties = new();

    public override void Tick()
    {
        if ( _manager is null )
            _manager = Game.ActiveScene.GetSystem<PostProcessManager>();

        var selectedPath = _manager?.SelectedPath;
        var resource = selectedPath != null
            ? ResourceLibrary.Get<PostProcessResource>( selectedPath )
            : null;

        if ( resource != _selected )
        {
            _selected = resource;
            RebuildProperties();
        }

        if ( _selected is not null )
            StateHasChanged();
    }

    void RebuildProperties()
    {
        var path = _selected?.ResourcePath;
        if ( path is null )
        {
            _properties = [];
            return;
        }

        var props = new List<SerializedProperty>();
        props.Add( TypeLibrary.CreateProperty( "Enabled",
            () => _manager?.IsEnabled( path ) ?? false,
            v => _manager?.Set( path, v ) ) );

        foreach ( var component in _manager?.GetSelectedComponents() ?? [] )
        {
            var so = TypeLibrary.GetSerializedObject( component );
            so.OnPropertyChanged = OnPropertyChanged;
            props.AddRange( so.Where( FilterProperties ) );
        }

        _properties = props;
    }

    static bool FilterProperties( SerializedProperty o )
    {
        if ( o.PropertyType is null ) return false;
        if ( o.PropertyType.IsAssignableTo( typeof(Delegate) ) ) return false;

        if ( o.IsMethod ) return true;
        if ( !o.HasAttribute<PropertyAttribute>() ) return false;

        return true;
    }

    void OnPropertyChanged( SerializedProperty prop )
    {
        foreach ( var target in prop.Parent.Targets )
        {
            if ( target is Component component )
                GameManager.ChangeProperty( component, prop.Name, prop.GetValue<object>() );
        }
    }

    protected override int BuildHash() => HashCode.Combine( _manager?.SelectedPath );
}
@using Sandbox;
@using Sandbox.UI;
@inherits Panel

<root class="row player">

	@if ( Connection is not null )
	{
		var steamId = Connection.SteamId;
		var data = PlayerData.For( Connection );
		bool isMuted = SandboxVoice.IsMuted( steamId );

		<img class="avatar" src="avatar:@steamId" />
		<div class="name">@Connection.DisplayName</div>
		<div class="stat">@(data?.Kills ?? 0)</div>
		<div class="stat">@(data?.Deaths ?? 0)</div>
		<div class="stat">@(Connection.Ping.CeilToInt())</div>
		@if ( Connection != Connection.Local )
		{
			<div class="mute-btn @(isMuted ? "muted" : "")" onclick="@( () => SandboxVoice.Mute( steamId ) )">
				<i>@(isMuted ? "volume_off" : "volume_up")</i>
			</div>
		}
		else
		{
			<div class="mute-spacer"></div>
		}
	}

</root>

@code
{
    public Connection Connection { get; set; }

    public override void Tick()
    {
        if ( Connection is null ) return;
        SetClass( "me", Connection == Connection.Local );
        SetClass( "friend", new Friend( Connection.SteamId ).IsFriend );
    }

    protected override int BuildHash()
    {
        if ( Connection is null ) return 0;
        var data = PlayerData.For( Connection );
        return System.HashCode.Combine( Connection.DisplayName, data?.Kills ?? 0, data?.Deaths ?? 0, Connection.Ping );
    }

    protected override void OnRightClick( MousePanelEvent e )
    {
        if ( Connection is null ) return;

        var steamId = Connection.SteamId;
        var menu = Sandbox.MenuPanel.Open( this );

        menu.AddOption( "content_copy", "Copy Steam ID", () =>
        {
            Clipboard.SetText( steamId.ToString() );
            Notices.AddNotice( "copy_all", Color.Cyan, $"Copied {Connection.DisplayName}'s SteamID to your clipboard", 5 );
        } );

		if ( Connection != Connection.Local )
		{
			bool isMuted = SandboxVoice.IsMuted( steamId );
			menu.AddOption( isMuted ? "volume_up" : "volume_off", isMuted ? "Unmute" : "Mute", () => SandboxVoice.Mute( steamId ) );

			if ( Connection.Local?.HasPermission( "admin" ) == true )
			{
				menu.AddSpacer();
				menu.AddOption( "person_remove", "Kick", () => OpenKickConfirm( Connection ) );
				menu.AddOption( "gavel", "Ban", () => OpenBanConfirm( Connection ) );
			}
		}

		e.StopPropagation();
	}

	void OpenKickConfirm( Connection connection )
	{
		var popup = new StringQueryPopup
		{
			Title = "Kick Player",
			Prompt = $"Why do you want to kick {connection.DisplayName}?",
			ConfirmLabel = "Kick",
			OnConfirm = x =>
			{
				GameManager.RpcKickPlayer( connection, x );
			}
		};
		popup.Parent = FindPopupPanel();
	}

	void OpenBanConfirm( Connection connection )
	{
		var popup = new StringQueryPopup
		{
			Title = "Ban Player",
			Prompt = $"Why do you want to ban {connection.DisplayName}?",
			ConfirmLabel = "Ban",
			OnConfirm = x => BanSystem.RpcBanPlayer( connection, x )
		};
		popup.Parent = FindPopupPanel();
	}
}
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox

@{
    var data = GetData();
}

<SpawnMenuContent>

    <Header>
        <SpawnMenuToolbar>
            <Left>
                <h2>@data.Name</h2>
            </Left>
            <Right>
                @{
                    var workshopId = Entry.GetMeta( "_workshopId", 0u );
                    var isPublished = workshopId > 0;
                }
                <div class="menu-icon-toggle-group">
                    @if ( isPublished )
                    {
                        <IconPanel Tooltip="#spawnmenu.spawnlist.copy_url" Text="link" @onclick=@( () => Clipboard.SetText( $"https://steamcommunity.com/sharedfiles/filedetails/?id={workshopId}" ) ) />
                        <IconPanel Tooltip="#spawnmenu.spawnlist.sync" Text="sync" @onclick=@( () => OnPublish() ) />
                    }
                    else
                    {
                        <IconPanel Tooltip="#spawnmenu.spawnlist.publish" Text="cloud_upload" @onclick=@( () => OnPublish() ) />
                    }
                    @if ( !Entry.Files.IsReadOnly )
                    {
                        <IconPanel Tooltip="#spawnmenu.spawnlist.delete" Text="delete" @onclick=@( () => OnDelete() ) />
                    }
                    else
                    {
                        <IconPanel Tooltip="#spawnmenu.spawnlist.remove" Text="delete" @onclick=@( () => OnUninstall() ) />
                    }
                </div>
            </Right>
        </SpawnMenuToolbar>
    </Header>

    <Body>
        @if ( data.Items.Count == 0 )
        {
            <div class="empty-state">
                <p>#spawnmenu.spawnlist.empty_title</p>
                <p>#spawnmenu.spawnlist.empty_instructions</p>
            </div>
        }
        else
        {
            <VirtualGrid [email protected] ItemSize=@(120)>
                <Item Context="item">
                    @if ( item is SpawnlistItem spawnItem )
                    {
                        <SpawnMenuIcon Ident="@spawnItem.Ident" Title="@spawnItem.Title" Icon="@spawnItem.Icon" />
                    }
                </Item>
            </VirtualGrid>
        }
    </Body>

</SpawnMenuContent>

@code
{
    public Storage.Entry Entry { get; set; }

    SpawnlistData _cachedData;
    RealTimeSince _lastRefresh;

    SpawnlistData GetData()
    {
        if ( _cachedData == null )
            RefreshCache();

        return _cachedData;
    }

    public void RefreshCache()
    {
        _cachedData = SpawnlistData.Load( Entry );
        _lastRefresh = 0;
    }

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

        // Periodically check for changes from other tabs
        if ( _lastRefresh > 1f )
        {
            var fresh = SpawnlistData.Load( Entry );
            if ( fresh.Items.Count != _cachedData?.Items?.Count )
            {
                _cachedData = fresh;
                StateHasChanged();
            }
            _lastRefresh = 0;
        }
    }

    protected override int BuildHash() => HashCode.Combine( _cachedData?.Items?.Count );

    void OnPublish()
    {
        if ( Entry is null ) return;
        SpawnlistData.Publish( Entry );
    }

    void OnDelete()
    {
        if ( Entry is null ) return;
        var page = Ancestors.OfType<SpawnlistsPage>().FirstOrDefault();
        page?.Collection.Delete( Entry );
    }

    void OnUninstall()
    {
        if ( Entry is null ) return;
        var workshopId = Entry.GetMeta( "_workshopId", 0ul );
        var page = Ancestors.OfType<SpawnlistsPage>().FirstOrDefault();
        page?.Collection.Uninstall( workshopId );
    }
}

@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox

<SpawnMenuContent>

    <Header>
        <SpawnMenuToolbar>
            <Left>
                <TextEntry Placeholder="#spawnmenu.common.search" class="filter menu-input" Value:bind=@Filter />
            </Left>
            <Right>
                <DropDown Value:bind=@SortOrder />
            </Right>
        </SpawnMenuToolbar>
    </Header>

    <Body>
        <VirtualList [email protected]( x => GetItemCount( x ) > 0 ) ItemHeight=@(48) OnLastCell="@(() => { _ = QueryNext(); })">
            <Item Context="context">
                @if (context is Storage.QueryItem item)
                {
                    <div class="spawnlist-row" onclick=@( () => _ = OnInstall( item ) )>
                        <div class="avatar" style="background-image: url('@item.Owner.Avatar');"></div>
                        <div class="info">
                            <div class="name">@item.Title</div>
                            <div class="author"><label>#spawnmenu.spawnlist.by</label> @item.Owner.Name</div>
                        </div>

                        <div class="item-count">@GetItemCount( item ) <label>#spawnmenu.common.items</label></div>
                    </div>
                }
            </Item>
        </VirtualList>
    </Body>

</SpawnMenuContent>

@code
{
    string _filter;
    public string Filter
    {
        get => _filter;
        set
        {
            if ( _filter == value ) return;
            _filter = value;
            Rebuild();
        }
    }

    WorkshopSortMode _sortOrder = WorkshopSortMode.Popular;
    public WorkshopSortMode SortOrder
    {
        get => _sortOrder;
        set
        {
            if ( _sortOrder == value ) return;
            _sortOrder = value;
            Rebuild();
        }
    }

    protected override async Task OnParametersSetAsync()
    {
        Items.Clear();
        LastResult = null;
        await QueryNext();
    }

    List<Storage.QueryItem> Items = new();
    Storage.QueryResult LastResult;

    async Task QueryNext()
    {
        if ( LastResult != null )
        {
            if ( !LastResult.HasMoreResults() )
                return;

            LastResult = await LastResult.GetNextResults();
            if ( LastResult.Items == null ) return;

            Items.AddRange( LastResult.Items );
            StateHasChanged();
            return;
        }

        var query = new Storage.Query();
        query.KeyValues["package"] = "facepunch.sandbox";
        query.KeyValues["type"] = "spawnlist";
        query.SortOrder = SortOrder.ToSortOrder();

        if ( !string.IsNullOrWhiteSpace( Filter ) )
            query.SearchText = Filter;

        LastResult = await query.Run();
        if ( LastResult.Items == null ) return;

        Items.AddRange( LastResult.Items );
        StateHasChanged();
    }

    async void Rebuild()
    {
        Items.Clear();
        LastResult = null;
        await QueryNext();
    }

    async Task OnInstall( Storage.QueryItem item )
    {
        var page = Ancestors.OfType<SpawnlistsPage>().FirstOrDefault();
        if ( page is null ) return;

        await page.Collection.InstallAsync( item );
    }

    int GetItemCount( Storage.QueryItem item )
    {
        try
        {
            var doc = Json.ParseToJsonObject( item.Metadata );
            return int.Parse( doc["Meta"]["item_count"]?.ToString()?.Trim( '"' ) ?? "0" );
        }
        catch { }

        return 0;
    }
}
@using Sandbox;
@using Sandbox.UI;
@attribute [InspectorEditor(null)]
@attribute [Order(100)]
@inherits Panel
@namespace Sandbox
@implements IInspectorEditor

<root>
    <div class="body">
            @if (Target == null || Target.Count == 0)
            {
            <div class="empty-state">#spawnmenu.inspect.click_to_inspect</div>
            }
            else
            {
                var totalMass = Target.SelectMany( go => go.GetComponentsInChildren<Rigidbody>() ).Sum( ResolveMass );
                var health = Target.Select( go => go.GetComponent<Prop>() ).FirstOrDefault( p => p.IsValid() );

                <div class="object-info">
                    @if ( totalMass > 0 )
                    {
                        <span>⚖️ @($"{totalMass:0.#} kg")</span>
                    }
                    @if ( health.IsValid() && health.Health != 0 )
                    {
                        <span>❤️ @($"{health.Health:0.#} HP")</span>
                    }
                </div>
                <ControlSheet Target="@Properties"></ControlSheet>

                @if (Renderers.Count > 0)
                {
                    @if ( MaterialGroups.Count > 1 )
                    {
                        var currentGroup = Renderers[0].MaterialGroup ?? MaterialGroups[0];
                        <div class="material-row">
                        <label>#spawnmenu.inspect.skin</label>
                            <div class="material-button" @onclick="@PickMaterialGroup">
                                <label>@currentGroup</label>
                                <label class="material-group-arrow">▾</label>
                            </div>
                        </div>
                    }

                    var accessor = Renderers[0].Materials;
                    @for (int i = 0; i < accessor.Count; i++)
                    {
                        var index = i;
                        var hasOverride = accessor.HasOverride(index);
                        var mat = hasOverride ? accessor.GetOverride(index) : accessor.GetOriginal(index);
                        var name = mat?.ResourceName ?? "Default";

                        <div class="material-row @(hasOverride ? "overridden" : "")">
                        <label>#spawnmenu.inspect.material</label><label> @(index + 1)</label>
                            <div class="material-button" @onclick=@(() => PickMaterial(index))>
                                <div class="material-preview" style="background-image: url( thumb:@(mat?.ResourcePath) )"></div>
                                <label>@name</label>
                            </div>
                            @if (hasOverride)
                            {
                                <div class="material-revert" @onclick=@(() => RevertMaterial(index))>x</div>
                            }
                        </div>
                    }
                }
            }
    </div>
</root>

@code
{
    public string Title => Target?.Count switch
    {
        null or < 2 => "📦 " + Game.Language.GetPhrase( "spawnmenu.inspect.object" ),
        _ => "📦 " + Game.Language.GetPhrase( "spawnmenu.inspect.object" ) + $" (+{Target.Count - 1})"
    };

    public List<GameObject> Target { get; private set; }

    public bool TrySetTarget(List<GameObject> selection)
    {
        var ids = selection.Select(x => x.Id);
        if (!ids.SequenceEqual(Target?.Select(x => x.Id) ?? []))
        {
            Target = selection.Any() ? selection.ToList() : null;
            RebuildFromTarget();
            StateHasChanged();
        }

        // Hide the tab when something is selected but there's nothing to show
        return Target == null || InspectorHasContent();
    }

    // Frozen rigidbodies report Mass = 0 from the live physics body, so fall back
    // to PhysicalProperties.Mass and MassOverride before giving up.
    static float ResolveMass( Rigidbody rb )
    {
        if ( !rb.IsValid() ) return 0f;
        var mo = rb.GetComponent<PhysicalProperties>();
        if ( mo.IsValid() && mo.Mass > 0f ) return mo.Mass;
        if ( rb.MassOverride > 0f ) return rb.MassOverride;
        return rb.Mass;
    }

    bool InspectorHasContent()
    {
        if ( Target == null ) return false;
        if ( Properties.Count > 0 || Renderers.Count > 0 ) return true;

        var totalMass = Target.SelectMany( go => go.GetComponentsInChildren<Rigidbody>() ).Sum( ResolveMass );
        if ( totalMass > 0 ) return true;

        var health = Target.Select( go => go.GetComponent<Prop>() ).FirstOrDefault( p => p.IsValid() );
        if ( health.IsValid() && health.Health != 0 ) return true;

        return false;
    }

    List<SerializedProperty> Properties = new();
    List<ModelRenderer> Renderers = new();
    List<string> MaterialGroups = new();

    protected override int BuildHash()
    {
        var hc = new HashCode();
        foreach ( var go in Target ?? [] )
        {
            hc.Add( go.Id );
            hc.Add( ResolveMass( go.GetComponent<Rigidbody>() ) );
            hc.Add( go.GetComponent<Prop>()?.Health ?? -1f );
            hc.Add( go.GetComponent<ModelRenderer>()?.MaterialGroup );
        }
        return hc.ToHashCode();
    }

    protected override void OnParametersSet()
    {
        base.OnParametersSet();
        RebuildFromTarget();
    }

    void RebuildFromTarget()
    {
        Properties = new();
        Renderers = new();
        MaterialGroups = new();

        if (Target == null) return;

        foreach (var c in Target.SelectMany(x => x.Components.GetAll()).Distinct().GroupBy(x => x is Collider ? typeof(Collider) : x.GetType()))
        {
            CollectProperties(c.ToArray());
        }
    }

    bool HasEditableProperties(Type type, PropertyDescription[] properties)
    {
        if (type.IsAssignableTo(typeof(ModelRenderer))) return true;
        if (type.IsAssignableTo(typeof(Collider))) return true;

        foreach (var prop in properties)
        {
            if (prop.HasAttribute<ClientEditableAttribute>())
                return true;
        }

        return false;
    }

    void CollectProperties(Component[] components)
    {
        var firstComponent = components.First();

        var tl = TypeLibrary.GetType(firstComponent.GetType());
        if (tl is null) return;

        if (!HasEditableProperties(firstComponent.GetType(), tl.Properties)) return;

        var so = new MultiSerializedObject();
        so.OnPropertyChanged = PropertyChanged;

        foreach (var component in components)
            so.Add(TypeLibrary.GetSerializedObject(component));

        so.Rebuild();

        foreach (var prop in tl.Properties)
        {
            if (!prop.HasAttribute<ClientEditableAttribute>()) continue;
            Properties.Add(so.GetProperty(prop.Name));
        }

        if (firstComponent is ModelRenderer mr)
        {
            Renderers.AddRange(components.OfType<ModelRenderer>());

            var model = mr.Model;
            if ( model is not null )
            {
                for ( int i = 0; i < model.MaterialGroupCount; i++ )
                    MaterialGroups.Add( model.GetMaterialGroupName( i ) );
            }

            var prop = mr.GetComponent<Prop>();
            if (prop is not null)
            {
                var propso = TypeLibrary.GetSerializedObject(prop);
                propso.OnPropertyChanged = PropertyChanged;
                Properties.Add(propso.GetProperty(nameof(ModelRenderer.Tint)));
            }
            else
            {
                Properties.Add(so.GetProperty(nameof(ModelRenderer.Tint)));
            }

            Properties.Add(so.GetProperty(nameof(ModelRenderer.RenderType)));
        }

        if (firstComponent is Collider)
            Properties.Add(so.GetProperty(nameof(Collider.Surface)));
    }

    void PropertyChanged(SerializedProperty prop)
    {
        foreach (var c in prop.Parent.Targets)
        {
            if (c is Component component)
                GameManager.ChangeProperty(component, prop.Name, prop.GetValue<object>());
        }
    }

    void PickMaterialGroup()
    {
        var menu = MenuPanel.Open( this );
        var current = Renderers[0].MaterialGroup ?? MaterialGroups.FirstOrDefault();
        foreach ( var group in MaterialGroups )
        {
            var g = group;
            menu.AddOption( current == g ? "check" : "", g, () => SetMaterialGroup( g ) );
        }
    }

    void SetMaterialGroup( string group )
    {
        foreach ( var renderer in Renderers )
            GameManager.ChangeProperty( renderer, nameof( ModelRenderer.MaterialGroup ), group );
    }

    void PickMaterial(int index)
    {
        var accessor = Renderers[0].Materials;
        var mat = accessor.HasOverride(index) ? accessor.GetOverride(index) : accessor.GetOriginal(index);

        var popup = new ResourceSelectPopup();
        popup.Extension = "material";
        popup.CurrentValue = mat?.ResourcePath;
        popup.AllowPackages = true;
        popup.Parent = FindPopupPanel();
        popup.OnSelectedFile = (path) => SetMaterialOverride(index, path);
    }

    void SetMaterialOverride(int index, string path)
    {
        foreach (var renderer in Renderers)
            GameManager.ChangeMaterialOverride(renderer, index, path);
    }

    void RevertMaterial(int index)
    {
        foreach (var renderer in Renderers)
            GameManager.ChangeMaterialOverride(renderer, index, null);
    }
}