Search the source of every open source package.
246 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);
}
}