Search the source of every open source package.
224 results
@namespace Sandbox
@using Sandbox.UI
@inherits Sandbox.UI.Panel
<root>
@if ( Developer )
{
<div class="developer-badge">🪲</div>
}
@if ( !string.IsNullOrWhiteSpace( Title ) && !HideText )
{
<div class="title">@Title</div>
}
</root>
@code
{
[Parameter] public string Ident { get; set; }
[Parameter] public string Icon { get; set; }
[Parameter] public string Title { get; set; }
[Parameter] public bool HideText { get; set; }
[Parameter] public string Metadata { get; set; }
[Parameter] public bool Developer { get; set; }
string Type;
string Path;
string Source;
/// <summary>
/// We want to drag from this panel
/// </summary>
public override bool WantsDrag => true;
protected override void OnParametersSet()
{
base.OnParametersSet();
(Type, Path, Source) = SpawnlistItem.ParseIdent( Ident );
if ( string.IsNullOrWhiteSpace( Icon ) )
{
Icon = $"thumb:{Path}";
}
Style.SetBackgroundImage(Icon);
}
protected override int BuildHash()
{
return HashCode.Combine( Ident, Icon, Title, HideText, Developer );
}
protected override async void OnDragStart( DragEvent e )
{
var dragData = new DragData
{
Type = Type,
Path = Path,
Icon = Icon ?? $"thumb:{Path}",
Title = Title,
Source = this
};
DragHandler.StartDragging( dragData );
if ( Type == "dupe" )
{
dragData.Data = await LoadDupeJson( Path, Source );
}
}
protected override void OnDragEnd( DragEvent e )
{
DragHandler.StopDragging();
}
protected override void OnClick( MousePanelEvent e )
{
GameManager.Spawn( Ident, Metadata );
}
protected override async void OnRightClick( MousePanelEvent e )
{
var menu = MenuPanel.Open( this );
// Let the spawner populate its own context menu options
var spawner = ISpawner.Create( Type, Path, metadata: Metadata );
if ( spawner is not null && await spawner.Loading )
{
spawner.PopulateContextMenu( menu, Ident, Metadata );
}
// If inside a spawnlist, offer removal
var spawnlistView = Ancestors.OfType<SpawnlistView>().FirstOrDefault();
if ( spawnlistView?.Entry is not null )
{
menu.AddOption( "delete", "Remove from List", () =>
{
var data = SpawnlistData.Load( spawnlistView.Entry );
var idx = data.Items.FindIndex( x => x.Ident == Ident );
if ( idx >= 0 )
{
SpawnlistData.RemoveItem( spawnlistView.Entry, idx );
spawnlistView.RefreshCache();
}
} );
menu.AddSpacer();
}
SpawnlistData.PopulateContextMenu( menu, new SpawnlistItem
{
Ident = Ident,
Title = Title,
Icon = Icon,
}, spawnlistView?.Entry );
e.StopPropagation();
}
async Task<string> LoadDupeJson( string id, string source )
{
if ( !ulong.TryParse( id, out var fileId ) )
return null;
if ( source == "workshop" )
{
var query = new Storage.Query { FileIds = [fileId] };
var result = await query.Run();
var item = result.Items?.FirstOrDefault();
if ( item is null ) return null;
var installed = await item.Install();
return installed?.Files.ReadAllText( "/dupe.json" );
}
var entry = Storage.GetAll( "dupe" ).FirstOrDefault( x => x.Id.ToString() == fileId.ToString() );
if ( entry is null ) return null;
return await entry.Files.ReadAllTextAsync( "/dupe.json" );
}
}
@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 UtilityPage
@namespace Sandbox
@attribute [Icon( "🔧" )]
@attribute [Title( "#spawnmenu.utility.utilities" )]
@attribute [Group( "#spawnmenu.utility.group.utilities" )]
@attribute [Order( 0 )]
<root class="page" style="flex-direction: column;">
<div class="control-row" @onclick=@(() => ConsoleSystem.Run( "kill" ))>
<div class="left"><label>🚿 </label><label>#spawnmenu.utility.kill_me</label></div>
</div>
</root>
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox
<root class="menu-header-bar">
@if ( Left != null )
{
<div class="left">@Left</div>
}
<div class="body">@Body</div>
@if ( Right != null )
{
<div class="right">@Right</div>
}
</root>
@code
{
[Parameter] public RenderFragment Left { get; set; }
[Parameter] public RenderFragment Body { get; set; }
[Parameter] public RenderFragment Right { get; set; }
}
@using Sandbox;
@using Sandbox.UI;
@inherits UtilityPage
@namespace Sandbox
@attribute [Icon( "🔫" )]
@attribute [Title( "#spawnmenu.utility.weapons" )]
@attribute [Group( "#spawnmenu.utility.group.world" )]
@attribute [Order( 2 )]
<root class="page" style="flex-direction: column;">
@if ( Connection.Local.HasPermission( "admin" ) )
{
<div class="control-row" Tooltip=@UnlimitedAmmoDescription @onclick=@ToggleUnlimitedAmmo>
<div class="left"><label>@(WeaponConVars.UnlimitedAmmo ? "🟢" : "⚪") </label><label>#spawnmenu.utility.unlimited_ammo</label></div>
<div style="flex-grow: 1;"></div>
<label class="right">@(WeaponConVars.UnlimitedAmmo ? "#spawnmenu.common.on" : "#spawnmenu.common.off")</label>
</div>
<div class="control-row" Tooltip=@InfiniteReservesDescription @onclick=@ToggleInfiniteReserves>
<div class="left"><label>@(WeaponConVars.InfiniteReserves ? "🟢" : "⚪") </label><label>#spawnmenu.utility.infinite_reserves</label></div>
<div style="flex-grow: 1;"></div>
<label class="right">@(WeaponConVars.InfiniteReserves ? "#spawnmenu.common.on" : "#spawnmenu.common.off")</label>
</div>
}
else
{
<div class="control-row">
<div class="left"><label>⚠️ </label><label>#spawnmenu.common.host_only</label></div>
</div>
}
</root>
@code
{
public override bool IsPageVisible() => Connection.Local.HasPermission( "admin" );
static readonly string UnlimitedAmmoDescription = "#spawnmenu.utility.unlimited_ammo_description";
static readonly string InfiniteReservesDescription = "#spawnmenu.utility.infinite_reserves_description";
void ToggleUnlimitedAmmo() => GameManager.SetConVar( "sb.weapon.unlimitedammo", !WeaponConVars.UnlimitedAmmo );
void ToggleInfiniteReserves() => GameManager.SetConVar( "sb.weapon.infinitereserves", !WeaponConVars.InfiniteReserves );
}
@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;
@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 PanelComponent
<root>
<div class="body">
<div class="title">
<label class="big">sandbox</label>
@if ( !string.IsNullOrEmpty( _mapTitle ) )
{
<label class="small">Playing on @(_mapTitle)</label>
}
</div>
<div class="table">
<div class="col">
<div class="header row">
<div class="avatar-spacer"></div>
<div class="name"></div>
<div class="stat">Kills</div>
<div class="stat">Deaths</div>
<div class="stat">Ping</div>
<div class="mute-spacer"></div>
</div>
@foreach ( var entry in Scene.GetAll<PlayerData>().OrderByDescending( x => x.Kills ).ThenBy( x => x.DisplayName ) )
{
<ScoreboardRow Entry="@entry" />
}
</div>
</div>
</div>
</root>
@code
{
string _mapTitle = null;
protected override void OnTreeFirstBuilt()
{
base.OnTreeFirstBuilt();
FetchMap();
}
async void FetchMap()
{
var ident = Networking.MapName;
var pkg = await Package.FetchAsync( ident, false );
if ( pkg is not null )
{
_mapTitle = pkg.Title;
}
}
protected override void OnUpdate()
{
var host = Game.ActiveScene.Get<SpawnMenuHost>();
if ( host?.Panel?.HasClass( "open" ) ?? false )
{
Input.Clear( "Score" );
}
SetClass( "visible", Input.Down( "Score" ) );
}
/// <summary>
/// update every second
/// </summary>
protected override int BuildHash() => System.HashCode.Combine( RealTime.Now.CeilToInt() );
}
@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 System
@using HitShapes
@using Sandbox.UI
@inherits PanelComponent
<root @onmouseout=@OnUnhover
@onmousemove=@OnHover
@onclick=@OnClicked>
@for ( int i = 0; i < 8; i++ )
{
int slot = i;
<div class=@SlotClass(slot) style=@SlotStyle(slot)>
<div class="num">@slot</div>
<div class="cnt">@_clicks[slot]</div>
</div>
}
</root>
@code
{
SlotDispatcher _dispatcher;
int[] _clicks = new int[8];
protected override void OnEnabled()
{
base.OnEnabled();
_dispatcher = new SlotDispatcher( HitShape.Radial( 8 ) )
{
OnSlotEnter = SlotChanged,
OnSlotLeave = SlotChanged,
OnSlotClick = ( slot, e ) => { _clicks[slot]++; StateHasChanged(); },
};
}
void SlotChanged( int slot ) => StateHasChanged();
void OnHover( PanelEvent e ) => _dispatcher?.HandleMouseMove( (MousePanelEvent)e, Panel );
void OnUnhover( PanelEvent e ) => _dispatcher?.HandleMouseLeave( (MousePanelEvent)e );
void OnClicked( PanelEvent e ) => _dispatcher?.HandleClick( (MousePanelEvent)e, Panel );
string SlotClass( int slot ) => "slot" + (_dispatcher?.CurrentSlot == slot ? " hot" : "");
string SlotStyle( int slot )
{
float theta = slot * MathF.Tau / 8f - MathF.PI / 2f;
float lx = 200f + MathF.Cos( theta ) * 140f - 30f;
float ly = 200f + MathF.Sin( theta ) * 140f - 30f;
return $"left: {lx}px; top: {ly}px";
}
}
@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 );
}
@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;
@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;
@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;
@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>() );
}
@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
);
}
}
@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>
<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.UI
@namespace sGBA
@inherits PanelComponent
<root class="@(IsVisible ? "visible" : "") @(_showConfirmDialog ? "confirm-open" : "") @(_showSavingDialog ? "saving-open" : "") @(InNetworkedSession ? "networked" : "")">
<div class="overlay">
<div class="menu-container">
<div class="menu-left @(_inSlotPanel ? "in-slots" : "")">
@for (int i = 0; i < ActiveMenuItems.Length; i++)
{
var idx = i;
<div class="menu-item @(idx == _selectedIndex ? "selected" : "")" onmouseenter=@(() => SelectItem(idx)) onclick=@(() => ActivateItemWithMouse(idx))>
@ActiveMenuItems[idx]
</div>
}
</div>
<div class="menu-divider @(HasSlotPanel ? "visible" : "")"></div>
<div class="menu-right @(HasSlotPanel ? "visible" : "")">
@for (int i = 1; i <= GbaSerialize.SlotCount; i++)
{
var slot = i;
<div class="slot @(slot == _highlightedSlot && HasSlotPanel ? "highlighted" : "")" onmouseenter=@(() => SelectSlotWithMouse(slot)) onclick=@(() => ActivateSlotWithMouse(slot))>
<div class="slot-preview">
@if (SlotTextures[slot - 1] != null)
{
<Image Texture=@SlotTextures[slot - 1] />
}
else
{
<span>#pause.slot.empty</span>
}
</div>
<div class="slot-info">
<div class="slot-label"><span>#pause.slot.label</span><span>@slot</span></div>
@if (SlotTimestamps[slot - 1] != null)
{
<div class="slot-timestamp">@SlotTimestamps[slot - 1].Value.ToString("dd/MM/yyyy - HH:mm")</div>
}
</div>
</div>
}
</div>
</div>
</div>
<div class="dialog-overlay @(_showConfirmDialog ? "visible" : "")">
<div class="confirm-box">
<div class="dialog-message">#pause.confirm.message</div>
<div class="confirm-buttons">
<div class="confirm-btn @(_confirmSelection == 0 ? "selected" : "")" onmouseenter=@(() => SelectConfirmWithMouse(0)) onclick=@(() => ConfirmOverwriteWithMouse())>#pause.confirm.yes</div>
<div class="confirm-btn @(_confirmSelection == 1 ? "selected" : "")" onmouseenter=@(() => SelectConfirmWithMouse(1)) onclick=@(() => CancelOverwriteWithMouse())>#pause.confirm.no</div>
</div>
</div>
</div>
<div class="dialog-overlay @(_showSavingDialog ? "visible" : "")">
<div class="dialog-box">
<div class="dialog-message">#pause.saving</div>
</div>
</div>
</root>
@code
{
private const int MenuContinue = 0;
private const int MenuLoad = 1;
private const int MenuCreate = 2;
private const int MenuControls = 3;
private const int MenuReset = 4;
private const int MenuGoToHome = 5;
private readonly string[] MenuItems =
[
"#pause.continue",
"#pause.load",
"#pause.create",
"#pause.controls",
"#pause.reset",
"#pause.home"
];
private static readonly string[] ClientMenuItems =
[
"#pause.continue",
"#pause.controls",
"#pause.home"
];
private static bool InNetworkedSession => NetworkManager.Current?.IsActive == true;
private string[] ActiveMenuItems => InNetworkedSession ? ClientMenuItems : MenuItems;
public bool IsVisible { get; private set; }
private int _selectedIndex;
private string SelectedKey => _selectedIndex >= 0 && _selectedIndex < ActiveMenuItems.Length ? ActiveMenuItems[_selectedIndex] : null;
private bool HasSlotPanel => SelectedKey is "#pause.load" or "#pause.create";
private bool IsLoadMode => SelectedKey == "#pause.load";
private bool _inSlotPanel;
private int _highlightedSlot = 1;
private readonly Texture[] SlotTextures = new Texture[GbaSerialize.SlotCount];
private readonly bool[] SlotOccupied = new bool[GbaSerialize.SlotCount];
private readonly DateTime?[] SlotTimestamps = new DateTime?[GbaSerialize.SlotCount];
private int _slotVersion;
private bool _showConfirmDialog;
private int _confirmSelection;
private int _pendingOverwriteSlot;
private bool _showSavingDialog;
private readonly FocusInput _input = new();
protected override void OnUpdate()
{
if (!EmulatorComponent.Current.IsValid() || !EmulatorComponent.Current.IsReady)
return;
if (Input.EscapePressed)
{
Input.EscapePressed = false;
if (IsVisible && !new Game.Overlay().IsOpen)
Resume();
else if (!IsVisible)
Pause();
return;
}
if (!IsVisible) return;
if (new Game.Overlay().IsOpen) return;
if (_showConfirmDialog)
{
HandleConfirmInput();
return;
}
if (_showSavingDialog) return;
var nav = _input.TickRepeating();
if (nav.Up) NavigateUp();
if (nav.Down) NavigateDown();
if (nav.Right)
{
if (!_inSlotPanel && HasSlotPanel)
EnterSlotPanel();
}
if (nav.Left)
{
if (_inSlotPanel)
ExitSlotPanel();
}
if (Input.Pressed("GBA_A"))
{
SetGamepadMode();
Sound.Play("ui.button.press");
if (_inSlotPanel)
ActivateSlot(_highlightedSlot);
else if (HasSlotPanel)
EnterSlotPanel();
else
ActivateItem(_selectedIndex);
}
if (Input.Pressed("GBA_B"))
{
SetGamepadMode();
Sound.Play("ui.button.press");
if (_inSlotPanel)
ExitSlotPanel();
else
Resume();
}
}
private void SetGamepadMode() => _input.ForceGamepadMode();
private void NavigateUp()
{
SetGamepadMode();
Sound.Play("ui.button.over");
if (_inSlotPanel)
_highlightedSlot = _highlightedSlot <= 1 ? GbaSerialize.SlotCount : _highlightedSlot - 1;
else
_selectedIndex = _selectedIndex <= 0 ? ActiveMenuItems.Length - 1 : _selectedIndex - 1;
}
private void NavigateDown()
{
SetGamepadMode();
Sound.Play("ui.button.over");
if (_inSlotPanel)
_highlightedSlot = _highlightedSlot >= GbaSerialize.SlotCount ? 1 : _highlightedSlot + 1;
else
_selectedIndex = _selectedIndex >= ActiveMenuItems.Length - 1 ? 0 : _selectedIndex + 1;
}
private void EnterSlotPanel()
{
SetGamepadMode();
Sound.Play("ui.button.press");
_inSlotPanel = true;
_highlightedSlot = 1;
}
private void ExitSlotPanel()
{
SetGamepadMode();
Sound.Play("ui.button.press");
_inSlotPanel = false;
_selectedIndex = MenuContinue;
}
private void Pause()
{
IsVisible = true;
_selectedIndex = 0;
_inSlotPanel = false;
_highlightedSlot = 1;
_input.Begin(useGamepad: true);
Sound.Play("ui.popup.message.open");
EmulatorComponent.Current.SetPaused(true);
RefreshSlotPreviews();
}
private void Resume()
{
IsVisible = false;
_selectedIndex = MenuContinue;
_inSlotPanel = false;
_input.End();
Mouse.Visibility = MouseVisibility.Hidden;
Sound.Play("ui.popup.message.close");
EmulatorComponent.Current.SetPaused(false);
}
private void SelectItem(int index)
{
_input.ForceMouseMode();
_selectedIndex = index;
_inSlotPanel = false;
if (HasSlotPanel)
_highlightedSlot = 1;
}
private void SelectSlotWithMouse(int slot)
{
_input.ForceMouseMode();
_highlightedSlot = slot;
}
private void ActivateSlotWithMouse(int slot)
{
_input.ForceMouseMode();
ActivateSlot(slot);
}
private void SelectConfirmWithMouse(int index)
{
_input.ForceMouseMode();
_confirmSelection = index;
}
private void ConfirmOverwriteWithMouse()
{
_input.ForceMouseMode();
ConfirmOverwrite();
}
private void CancelOverwriteWithMouse()
{
_input.ForceMouseMode();
CancelOverwrite();
}
private void ActivateItem(int index)
{
if (index < 0 || index >= ActiveMenuItems.Length) return;
switch (ActiveMenuItems[index])
{
case "#pause.continue":
Resume();
break;
case "#pause.load":
case "#pause.create":
SelectItem(index);
_inSlotPanel = true;
break;
case "#pause.controls":
Game.Overlay.ShowBinds();
break;
case "#pause.reset":
EmulatorComponent.Current.ResetEmulator();
Resume();
break;
case "#pause.home":
IsVisible = false;
_selectedIndex = MenuContinue;
_inSlotPanel = false;
Mouse.Visibility = MouseVisibility.Hidden;
Sound.Play("ui.popup.message.close");
NetworkManager.Current?.Leave();
HomeScreen.Current?.Show();
break;
}
}
private void ActivateItemWithMouse(int index)
{
_input.ForceMouseMode();
ActivateItem(index);
}
private void ActivateSlot(int slot)
{
if (IsLoadMode)
{
if (!SlotOccupied[slot - 1])
return;
EmulatorComponent.Current.LoadSuspendPoint(slot);
Resume();
}
else
{
if (SlotOccupied[slot - 1])
{
_pendingOverwriteSlot = slot;
_confirmSelection = 0;
_showConfirmDialog = true;
Sound.Play("ui.popup.message.open");
return;
}
DoSave(slot);
}
}
private void HandleConfirmInput()
{
var nav = _input.TickRepeating();
if (nav.Up || nav.Down)
{
SetGamepadMode();
Sound.Play("ui.button.over");
_confirmSelection = _confirmSelection == 0 ? 1 : 0;
}
if (Input.Pressed("GBA_A"))
{
SetGamepadMode();
Sound.Play("ui.button.press");
if (_confirmSelection == 0)
ConfirmOverwrite();
else
CancelOverwrite();
}
if (Input.Pressed("GBA_B"))
{
SetGamepadMode();
Sound.Play("ui.button.press");
CancelOverwrite();
}
}
private void ConfirmOverwrite()
{
_showConfirmDialog = false;
DoSave(_pendingOverwriteSlot);
}
private void CancelOverwrite()
{
_showConfirmDialog = false;
Sound.Play("ui.popup.message.close");
}
private async void DoSave(int slot)
{
_showSavingDialog = true;
await Task.Delay(100);
EmulatorComponent.Current.CreateSuspendPoint(slot);
RefreshSlotPreviews();
await Task.Delay(500);
_showSavingDialog = false;
}
private void RefreshSlotPreviews()
{
var emu = EmulatorComponent.Current;
if (!emu.IsValid()) return;
for (int i = 0; i < GbaSerialize.SlotCount; i++)
{
var path = emu.GetStatePath(i + 1);
if (!FileSystem.Data.FileExists(path))
{
ClearSlot(i);
continue;
}
var data = FileSystem.Data.ReadAllBytes(path).ToArray();
SlotOccupied[i] = true;
SlotTimestamps[i] = GbaSerialize.ReadTimestamp(data);
var screenshot = GbaSerialize.ReadScreenshot(data);
if (screenshot == null) continue;
if (SlotTextures[i] == null)
{
SlotTextures[i] = Texture.Create(GbaConstants.ScreenWidth, GbaConstants.ScreenHeight, ImageFormat.RGBA8888)
.WithDynamicUsage()
.WithName($"suspend-preview-{i}")
.Finish();
}
SlotTextures[i].Update(screenshot, 0, 0, GbaConstants.ScreenWidth, GbaConstants.ScreenHeight);
}
_slotVersion++;
}
private void ClearSlot(int index)
{
SlotOccupied[index] = false;
SlotTimestamps[index] = null;
SlotTextures[index]?.Dispose();
SlotTextures[index] = null;
}
protected override int BuildHash()
{
return HashCode.Combine(
IsVisible, _selectedIndex, _inSlotPanel, _highlightedSlot,
_input.UseGamepad, _slotVersion, _showConfirmDialog,
HashCode.Combine(_confirmSelection, _showSavingDialog, InNetworkedSession)
);
}
protected override void OnDestroy()
{
for (int i = 0; i < SlotTextures.Length; i++)
{
SlotTextures[i]?.Dispose();
SlotTextures[i] = null;
}
}
}
@using System.Threading.Tasks
@namespace sGBA
@inherits PanelComponent
<root class="@RootClass">
<div class="home-screen">
<HomeBackdrop Current=@_currentBackdrop Previous=@_previousBackdrop PreviousOpacity=@PreviousBackdropOpacity />
@if (ToastVisible)
{
<HomeToast Title=@_toast.Title Message=@_toast.Message Icon=@_toast.Icon ColorClass=@_toast.ColorClass InlineStyle=@ToastStyle />
}
@if (_allSoftwareOpen)
{
<div class="all-software-screen">
<div class="all-software-rule top" />
<div class="all-software-rule bottom" />
<div class="all-software-scroll" @ref=AllSoftwareScroll>
<div class="all-software-grid" style="@AllSoftwareGridStyle">
@foreach (var gridItem in AllSoftwareGridItems)
{
Texture gridCover = null;
if (gridItem.Game != null)
_boxArt.TryGetValue(gridItem.Game.Path, out gridCover);
<AllSoftwareCard @key="gridItem.Key"
[email protected]
Cover=@gridCover
Selected=@IsAllSoftwareSelected(gridItem.Index)
PositionStyle=@AllSoftwareGridItemStyle(gridItem.Row, gridItem.Column)
CardLeft=@(gridItem.Column * (AllSoftwareCardWidth + AllSoftwareColumnGap))
CardWidth=@AllSoftwareCardWidth
GridWidth=@AllSoftwareGridWidth
ShowTitleOnTop=@ShouldShowAllSoftwareTitleOnTop(gridItem.Index)
OnActivate=@(() => ActivateAllSoftwareItemWithMouse(gridItem.Index)) />
}
<div class="all-add-row" style="@AllSoftwareAddRowStyle">
<div class="@AllSoftwareAddButtonClass" onmouseenter=@UseMouseInAllSoftware onclick=@ActivateAllSoftwareAdd>
<SelectionRing Active=@IsAllSoftwareAddSelected StrokeWidth=@(8f) CornerRadius=@(10f) Gap=@(0f) class="all-add-button-ring" />
<div class="all-add-button-fill" />
<div class="all-add-button-label">#addgames.card.label</div>
</div>
</div>
</div>
</div>
@if (AllSoftwareShowsScrollbar)
{
<div class="all-scrollbar" @ref=AllSoftwareScrollbar onmousedown=@OnAllSoftwareScrollbarMouseDown>
<div class="all-scrollbar-thumb" @ref=AllSoftwareScrollbarThumb style="@AllSoftwareScrollbarThumbStyle" />
</div>
}
</div>
}
else
{
<div class="profile-avatar" style="background-image: url( avatar:@LocalSteamId )" />
@if (!IsHomeViewMoreSelected)
{
<HomeLogo Logo=@SelectedLogo Title=@SelectedTitle />
}
<HomeCarousel Items=@CarouselItems SelectedIndex=@_selectedIndex NavFocused=@_navFocused ShowSelection=@ShowHomeSelection RenderVersion=@_homeCarouselRenderVersion ArtworkVersion=@_artworkVersion MountedRange=@CarouselMountedRange Suppress=@_suppressHomeCarouselTransitions CoverFor=@((GameEntry g) => _boxArt.GetValueOrDefault(g.Path)) OnActivate=@((int index) => ActivateCarouselItem(index)) />
<HomeNavPill Items=@NavItems SelectedIndex=@_navSelection Focused=@_navFocused ShowSelection=@(ShowHomeSelection && _input.UseGamepad && _navFocused) OnHover=@((int i) => SelectNavItem(i, useGamepad: false)) OnActivate=@((int i) => ActivateNavItem(i, useGamepad: false)) />
}
</div>
</root>
@code
{
public static HomeScreen Current { get; private set; }
private List<GameEntry> _games = new();
private List<GameEntry> _allGames = new();
private List<GameEntry> _homeItems = new();
private HashSet<string> _knownPaths = new();
private float _pollTimer;
private const float PollInterval = 3f;
private Dictionary<string, Texture> _boxArt = new();
private Dictionary<string, Texture> _logos = new();
private Dictionary<string, Texture> _snaps = new();
private Dictionary<string, Texture> _titles = new();
private int _artworkVersion;
private int _artworkRequestId;
private int _selectedIndex;
private int _allSoftwareSelection;
private bool _homeOrderDirty;
private bool _suppressHomeCarouselTransitions;
private int _homeCarouselTransitionSuppressionFrames;
private int _homeCarouselRenderVersion;
private float _homeCarouselSettleRemaining;
private GameEntry _queuedHomeLaunchGame;
private string _pendingFrozenHomeLaunchPath;
private int _pendingFrozenHomeLaunchFrames;
private string _pendingHomeSelectionPath;
private int _navSelection = 2;
private bool _navFocused;
private bool _isVisible = true;
private bool _allSoftwareOpen;
private bool _addGamesModalOpen;
private readonly HomeToastState _toast = new();
private const int HomeGameLimit = 12;
private const int CarouselMountedRange = 9;
private const float BackdropRetainDuration = 0.32f;
private Texture _currentBackdrop;
private Texture _previousBackdrop;
private float _previousBackdropAge;
private bool _previousBackdropFading;
private int _backdropVersion;
private readonly List<PendingTextureDispose> _pendingTextureDisposals = new();
private const float HomeCarouselSettleDuration = 0.2f;
private sealed record ArtworkRequest(GameEntry Game, ThumbType Kind, Dictionary<string, Texture> Target);
private sealed record ArtworkLoad(ArtworkRequest Request, Texture Texture);
private readonly record struct PendingTextureDispose(string Key, Texture Texture, float Age);
private readonly NavItem[] NavItems =
[
new("group", "orange", "#nav.netplay"),
new("newspaper", "green", "#nav.news"),
new("cloud", "blue", "#nav.cloud"),
new("settings", "gray", "#nav.settings"),
new("power_settings_new", "gray", "#nav.quit")
];
private readonly FocusInput _input = new();
public bool IsVisible => _isVisible;
private string RootClass => (IsVisible ? "visible" : "") + (_suppressHomeCarouselTransitions ? " suppress-home-carousel-transitions" : "");
public bool AllSoftwareOpen => _allSoftwareOpen;
public bool NavigationFocused => _navFocused;
public bool HasSelectedGame => SelectedGame != null;
public bool CanOpenDetails => !_navFocused && SelectedGame != null;
public bool HasHomeAction => IsHomeViewMoreSelected;
public string PrimaryActionLabel => _allSoftwareOpen || IsHomeViewMoreSelected ? "#prompt.ok" : "#prompt.start";
private long LocalSteamId => Connection.Local is null ? 0L : Connection.Local.SteamId;
private int HomeGameCount => Math.Min(_homeItems.Count, HomeGameLimit);
private int HomeEntryCount => HomeGameCount + 1;
private int HomeViewMoreIndex => HomeGameCount;
private bool IsHomeViewMoreSelected => _selectedIndex == HomeViewMoreIndex;
private bool AddGamesModalOpen => _addGamesModalOpen || AddGamesModal.Current?.IsVisible == true;
private bool SettingsModalOpen => SettingsModal.Current?.IsVisible == true;
private bool ShowHomeSelection => !AddGamesModalOpen && !SettingsModalOpen;
private bool ToastBlocked => AddGamesModalOpen || SettingsModalOpen || DetailsModal.Current?.IsVisible == true;
private bool ToastVisible => _toast.IsVisible(ToastBlocked);
private string ToastStyle => _toast.GetStyle(ToastVisible);
private GameEntry SelectedGame => _allSoftwareOpen
? _allSoftwareSelection >= 0 && _allSoftwareSelection < _allGames.Count ? _allGames[_allSoftwareSelection] : null
: !IsHomeViewMoreSelected && _selectedIndex >= 0 && _selectedIndex < _homeItems.Count ? _homeItems[_selectedIndex] : null;
private string SelectedTitle => SelectedGame?.DisplayTitle ?? "sGBA";
private Texture SelectedLogo => SelectedGame != null && _logos.TryGetValue(SelectedGame.Path, out var logo) ? logo : null;
private Texture SelectedBackdrop => SelectedGame != null && _snaps.TryGetValue(SelectedGame.Path, out var snap) ? snap : SelectedGame != null && _boxArt.TryGetValue(SelectedGame.Path, out var cover) ? cover : null;
private float PreviousBackdropOpacity => _previousBackdrop == null ? 0f : _previousBackdropFading ? 0f : 1f;
private IEnumerable<HomeCarouselEntry> CarouselItems
{
get
{
if (HomeEntryCount == 0)
yield break;
int selected = _selectedIndex.Clamp(0, HomeEntryCount - 1);
int first = Math.Max(0, selected - CarouselMountedRange);
int last = Math.Min(HomeEntryCount - 1, selected + CarouselMountedRange);
for (int i = first; i <= last; i++)
{
bool isViewMore = i == HomeViewMoreIndex;
yield return new HomeCarouselEntry(isViewMore ? null : _homeItems[i], i, i - selected, isViewMore, isViewMore ? "view-more" : _homeItems[i].Path);
}
}
}
protected override void OnTreeFirstBuilt()
{
Current = this;
if (NetworkManager.Current != null)
NetworkManager.Current.JoinRejected += OnJoinRejected;
EnsureInputHintsPanel();
_games = GameEntry.Discover();
_knownPaths = GameEntry.GetInstalledPaths();
RebuildGameLists(preserveSelection: false);
SyncBackdropTexture();
QueueArtworkRefresh();
}
private void QueueArtworkRefresh()
{
var requestId = ++_artworkRequestId;
_ = LoadArtworkAsync(requestId);
}
private async Task LoadArtworkAsync(int requestId)
{
var visibleTask = LoadVisibleCarouselThumbnailsAsync();
var selectedTask = LoadSelectedFeatureArtAsync();
await visibleTask;
await selectedTask;
if (requestId != _artworkRequestId)
return;
var artworkVersion = _artworkVersion;
PruneArtworkTextures();
if (_artworkVersion != artworkVersion)
StateHasChanged();
}
private Task<bool> LoadSelectedFeatureArtAsync()
{
var game = SelectedGame;
if (game == null)
return Task.FromResult(false);
return LoadArtworkBatchAsync([
new ArtworkRequest(game, ThumbType.Snap, _snaps),
new ArtworkRequest(game, ThumbType.Logo, _logos),
new ArtworkRequest(game, ThumbType.Title, _titles)
]);
}
private Task<bool> LoadVisibleCarouselThumbnailsAsync()
{
var homeRequests = CarouselItems
.Where(item => item.Game != null)
.Select(item => new ArtworkRequest(item.Game, ThumbType.BoxArt, _boxArt));
var allSoftwareRequests = _allSoftwareOpen
? AllSoftwareVisibleItems.Select(item => new ArtworkRequest(item.Game, ThumbType.BoxArt, _boxArt))
: [];
var requests = homeRequests
.Concat(allSoftwareRequests)
.ToList();
return LoadArtworkBatchAsync(requests);
}
private async Task<bool> LoadArtworkBatchAsync(IReadOnlyList<ArtworkRequest> requests)
{
var pending = requests
.Where(request => request.Game != null && !request.Target.ContainsKey(request.Game.Path))
.ToList();
if (pending.Count == 0)
return false;
foreach (var request in pending)
CancelPendingTextureDispose(GetArtworkKey(request.Kind, request.Game.Path));
var changed = false;
var loadTasks = pending.Select(LoadArtworkAsync).ToList();
while (loadTasks.Count > 0)
{
var completedTask = await Task.WhenAny(loadTasks);
loadTasks.Remove(completedTask);
if (!ApplyArtworkLoad(await completedTask))
continue;
changed = true;
_artworkVersion++;
StateHasChanged();
}
return changed;
}
private static async Task<ArtworkLoad> LoadArtworkAsync(ArtworkRequest request)
{
var texture = await Thumbnails.LoadAsync(request.Game, request.Kind);
return new ArtworkLoad(request, texture);
}
private bool ApplyArtworkLoad(ArtworkLoad load)
{
var game = load.Request.Game;
var kind = load.Request.Kind;
var target = load.Request.Target;
var texture = load.Texture;
if (texture == null || game == null)
return false;
CancelPendingTextureDispose(GetArtworkKey(kind, game.Path));
if (target.ContainsKey(game.Path))
{
return false;
}
if (!ShouldRetainArtwork(game, kind))
{
ScheduleTextureDispose(GetArtworkKey(kind, game.Path), texture);
return false;
}
target[game.Path] = texture;
if (game == SelectedGame)
SyncBackdropTexture();
return true;
}
private bool ShouldRetainArtwork(GameEntry game, ThumbType kind)
{
if (game == null)
return false;
if (game == SelectedGame)
return true;
return kind == ThumbType.BoxArt && RetainedBoxArtPaths().Contains(game.Path);
}
private HashSet<string> RetainedBoxArtPaths()
{
var retained = CarouselItems
.Where(item => IsCarouselOffsetMounted(item.Offset))
.Where(item => item.Game != null)
.Select(item => item.Game.Path)
.ToHashSet();
if (_allSoftwareOpen)
{
foreach (var item in AllSoftwareVisibleItems)
{
if (item.Game != null)
retained.Add(item.Game.Path);
}
}
return retained;
}
private HashSet<string> RetainedFeatureArtworkPaths()
{
var retained = new HashSet<string>();
if (SelectedGame != null)
retained.Add(SelectedGame.Path);
return retained;
}
private void PruneArtworkTextures()
{
PruneArtworkDictionary(_boxArt, ThumbType.BoxArt, RetainedBoxArtPaths());
var retainedFeatureArtwork = RetainedFeatureArtworkPaths();
PruneArtworkDictionary(_logos, ThumbType.Logo, retainedFeatureArtwork);
PruneArtworkDictionary(_snaps, ThumbType.Snap, retainedFeatureArtwork);
PruneArtworkDictionary(_titles, ThumbType.Title, retainedFeatureArtwork);
}
private void PruneArtworkDictionary(Dictionary<string, Texture> textures, ThumbType kind, HashSet<string> retainedPaths)
{
foreach (var (path, texture) in textures.ToList())
{
if (retainedPaths.Contains(path))
continue;
if (texture == _currentBackdrop || texture == _previousBackdrop)
continue;
textures.Remove(path);
ScheduleTextureDispose(GetArtworkKey(kind, path), texture);
_artworkVersion++;
}
}
private static string GetArtworkKey(ThumbType kind, string path)
{
return $"{kind}:{path}";
}
private void ScheduleTextureDispose(string key, Texture texture)
{
if (texture == null)
return;
_pendingTextureDisposals.RemoveAll(item => item.Key == key || item.Texture == texture);
_pendingTextureDisposals.Add(new PendingTextureDispose(key, texture, 0f));
}
private void CancelPendingTextureDispose(string key)
{
_pendingTextureDisposals.RemoveAll(item => item.Key == key);
}
private void UpdatePendingTextureDisposals()
{
if (_pendingTextureDisposals.Count == 0)
return;
for (int i = _pendingTextureDisposals.Count - 1; i >= 0; i--)
{
var pending = _pendingTextureDisposals[i];
var age = pending.Age + Time.Delta;
if (age < 0.5f)
{
_pendingTextureDisposals[i] = pending with { Age = age };
continue;
}
if (!IsArtworkTextureStillReferenced(pending.Texture))
Thumbnails.ReleaseTexture(pending.Texture);
_pendingTextureDisposals.RemoveAt(i);
}
}
private bool IsArtworkTextureStillReferenced(Texture texture)
{
if (texture == null)
return false;
if (texture == _currentBackdrop || texture == _previousBackdrop)
return true;
return _boxArt.ContainsValue(texture) || _logos.ContainsValue(texture) || _snaps.ContainsValue(texture);
}
public void Show()
{
if (_homeOrderDirty)
{
RebuildGameLists(preserveSelection: false);
SelectPendingHomeGame();
_homeOrderDirty = false;
_pendingHomeSelectionPath = null;
_suppressHomeCarouselTransitions = true;
_homeCarouselTransitionSuppressionFrames = 2;
Panel?.SkipTransitions();
SyncBackdropTexture();
QueueArtworkRefresh();
}
_isVisible = true;
_navFocused = false;
_input.Begin(useGamepad: false);
Sound.Play("ui.popup.message.open");
EmulatorComponent.Current?.Unload();
StateHasChanged();
}
public void Hide(bool update = true)
{
_isVisible = false;
Mouse.Visibility = MouseVisibility.Hidden;
if (update)
StateHasChanged();
}
private void ActivateCarouselItem(int index)
{
if (IsHomeViewMoreIndex(index))
{
SelectCarouselItem(index, _input.UseGamepad);
OpenAllSoftware();
return;
}
bool shouldLaunch = !_navFocused && index == _selectedIndex;
SelectCarouselItem(index, _input.UseGamepad);
if (shouldLaunch && SelectedGame != null)
{
if (_homeCarouselSettleRemaining > 0f)
{
_queuedHomeLaunchGame = SelectedGame;
return;
}
LaunchGame(SelectedGame);
}
}
private void OpenAddGamesModal(bool useGamepad = false)
{
Sound.Play("ui.button.press");
AddGamesModal addGamesPanel = EnsureAddGamesPanel();
if (addGamesPanel == null)
return;
_addGamesModalOpen = true;
StateHasChanged();
addGamesPanel.Open(useGamepad);
}
public void OnAddGamesModalClosed(bool useGamepad)
{
_addGamesModalOpen = false;
if (useGamepad)
_input.ForceGamepadMode();
else
_input.ForceMouseMode();
EnsureAllSoftwareSelectionVisible();
StateHasChanged();
}
private AddGamesModal EnsureAddGamesPanel()
{
if (AddGamesModal.Current != null && AddGamesModal.Current.IsValid())
return AddGamesModal.Current;
AddGamesModal addGamesPanel = Scene.GetAllComponents<AddGamesModal>().FirstOrDefault(panel => panel.IsValid());
if (addGamesPanel != null)
return addGamesPanel;
return AddComponent<AddGamesModal>();
}
private void LaunchGame(GameEntry game)
{
var emulator = EmulatorComponent.Current;
if (!emulator.IsValid())
return;
GamePlayHistory.MarkPlayed(game.Path);
AchievementManager.OnGameLaunched(game.Path);
_homeOrderDirty = true;
_suppressHomeCarouselTransitions = true;
_pendingHomeSelectionPath = game.Path;
if (_allSoftwareOpen)
{
_allSoftwareOpen = false;
Sound.Play("ui.button.press");
Hide();
emulator.Restart(game.Path);
return;
}
Sound.Play("ui.button.press");
_suppressHomeCarouselTransitions = true;
_homeCarouselTransitionSuppressionFrames = 4;
Panel?.SkipTransitions();
_homeCarouselRenderVersion++;
_pendingFrozenHomeLaunchPath = game.Path;
_pendingFrozenHomeLaunchFrames = 1;
StateHasChanged();
}
private void CompleteFrozenHomeLaunch()
{
if (string.IsNullOrWhiteSpace(_pendingFrozenHomeLaunchPath))
return;
var path = _pendingFrozenHomeLaunchPath;
_pendingFrozenHomeLaunchPath = null;
_pendingFrozenHomeLaunchFrames = 0;
var emulator = EmulatorComponent.Current;
if (!emulator.IsValid())
return;
Hide(update: false);
emulator.Restart(path);
}
private void RebuildGameLists(bool preserveSelection = true)
{
bool hadHomeGames = _homeItems.Count > 0;
var selectedGame = preserveSelection ? SelectedGame : null;
bool selectedViewMore = preserveSelection && hadHomeGames && IsHomeViewMoreSelected;
_allGames = [.._games];
_homeItems = [.._games
.OrderByDescending(game => GamePlayHistory.LastPlayedAt(game.Path))
.ThenBy(game => game.DisplayTitle, StringComparer.OrdinalIgnoreCase)];
if (!preserveSelection)
{
_selectedIndex = 0;
}
else if (selectedGame != null && !selectedViewMore)
{
int homeIndex = _homeItems.IndexOf(selectedGame);
if (homeIndex >= 0)
_selectedIndex = homeIndex;
}
else if (selectedViewMore)
{
_selectedIndex = HomeViewMoreIndex;
}
ClampSelectedIndex();
}
private void SelectPendingHomeGame()
{
if (string.IsNullOrWhiteSpace(_pendingHomeSelectionPath))
return;
int homeIndex = _homeItems.FindIndex(game => string.Equals(game.Path, _pendingHomeSelectionPath, StringComparison.OrdinalIgnoreCase));
_selectedIndex = homeIndex >= 0 && homeIndex < HomeGameCount ? homeIndex : HomeViewMoreIndex;
}
private void ClampSelectedIndex()
{
if (HomeEntryCount == 0)
{
_selectedIndex = 0;
}
else
{
_selectedIndex = _selectedIndex.Clamp(0, HomeEntryCount - 1);
}
_allSoftwareSelection = _allSoftwareSelection.Clamp(0, AllSoftwareAddIndex);
EnsureAllSoftwareSelectionVisible();
}
private void SetGamepadMode()
{
_input.ForceGamepadMode();
}
private void SyncVisibilityWithEmulator()
{
var emu = EmulatorComponent.Current;
var net = NetworkManager.Current;
bool linkClient = net != null && net.IsClient && net.Mode == SessionMode.LinkCable;
bool running = emu.IsValid() && (emu.IsLinked || linkClient || (emu.IsReady && !string.IsNullOrEmpty(emu.RomPath)));
if (running && _isVisible) Hide();
else if (!running && !_isVisible) Show();
}
private SettingsModal EnsureSettingsPanel()
{
if (SettingsModal.Current != null && SettingsModal.Current.IsValid())
return SettingsModal.Current;
SettingsModal settingsPanel = Scene.GetAllComponents<SettingsModal>().FirstOrDefault(panel => panel.IsValid());
if (settingsPanel != null)
return settingsPanel;
return AddComponent<SettingsModal>();
}
private InputHints EnsureInputHintsPanel()
{
if (InputHints.Current != null && InputHints.Current.IsValid())
return InputHints.Current;
InputHints inputHintsPanel = Scene.GetAllComponents<InputHints>().FirstOrDefault(panel => panel.IsValid());
if (inputHintsPanel != null)
return inputHintsPanel;
return AddComponent<InputHints>();
}
private void OpenSettings(bool useGamepad = false)
{
if (useGamepad)
SetGamepadMode();
else
_input.End();
_navFocused = true;
_navSelection = 3;
Sound.Play("ui.button.press");
SettingsModal settingsPanel = EnsureSettingsPanel();
settingsPanel?.Show(useGamepad);
}
public void FocusHeaderAction(int headerSelection, bool useGamepad)
{
_navFocused = true;
_navSelection = headerSelection.Clamp(0, NavItems.Length - 1);
if (useGamepad) _input.ForceGamepadMode();
else _input.End();
StateHasChanged();
}
public void RestoreInputMode(bool useGamepad)
{
if (useGamepad) _input.ForceGamepadMode();
else _input.ForceMouseMode();
StateHasChanged();
}
public void FocusSettingsAction(bool useGamepad)
{
FocusHeaderAction(3, useGamepad);
}
protected override void OnUpdate()
{
SyncVisibilityWithEmulator();
if (!string.IsNullOrWhiteSpace(_pendingFrozenHomeLaunchPath))
{
if (_pendingFrozenHomeLaunchFrames > 0)
{
_pendingFrozenHomeLaunchFrames--;
return;
}
CompleteFrozenHomeLaunch();
return;
}
if (_homeCarouselSettleRemaining > 0f)
{
_homeCarouselSettleRemaining = MathF.Max(0f, _homeCarouselSettleRemaining - Time.Delta);
if (_homeCarouselSettleRemaining <= 0f && _queuedHomeLaunchGame != null)
{
var queuedLaunch = _queuedHomeLaunchGame;
_queuedHomeLaunchGame = null;
LaunchGame(queuedLaunch);
return;
}
}
if (_isVisible && _suppressHomeCarouselTransitions)
{
if (_homeCarouselTransitionSuppressionFrames > 0)
{
_homeCarouselTransitionSuppressionFrames--;
}
else
{
_suppressHomeCarouselTransitions = false;
StateHasChanged();
}
}
UpdateBackdropTransition();
UpdatePendingTextureDisposals();
UpdateToast();
_pollTimer += Time.Delta;
if (_pollTimer >= PollInterval)
{
_pollTimer = 0f;
var current = GameEntry.GetInstalledPaths();
if (!current.SetEquals(_knownPaths))
{
_knownPaths = current;
_games = GameEntry.Discover();
RebuildGameLists();
QueueArtworkRefresh();
StateHasChanged();
}
}
if (!_isVisible) return;
if (SettingsModal.Current?.IsVisible == true) return;
if (DetailsModal.Current?.IsVisible == true) return;
if (AddGamesModal.Current?.IsVisible == true) return;
if (new Game.Overlay().IsOpen) return;
UpdateAllSoftwareScrollState();
var nav = _input.TickRepeating();
if (nav.Up) { SetGamepadMode(); NavigateUp(); }
if (nav.Down) { SetGamepadMode(); NavigateDown(); }
if (nav.Left) { SetGamepadMode(); NavigateLeft(); }
if (nav.Right) { SetGamepadMode(); NavigateRight(); }
if (Input.Pressed("GBA_A") || Input.Pressed("GBA_Start"))
{
SetGamepadMode();
if (_allSoftwareOpen) ActivateAllSoftwareItem(_allSoftwareSelection);
else if (_navFocused) ActivateNavItem(_navSelection, useGamepad: true);
else ActivateCarouselItem(_selectedIndex);
}
if (Input.Pressed("GBA_B") && _allSoftwareOpen)
{
SetGamepadMode();
CloseAllSoftware();
}
if (Input.Pressed("GBA_B") && _navFocused)
{
SetGamepadMode();
_navFocused = false;
Sound.Play("ui.button.over");
}
if (Input.Pressed("GBA_Select") && CanOpenDetails)
{
SetGamepadMode();
OpenSelectedDetails();
}
}
private void NavigateUp()
{
if (_allSoftwareOpen)
{
NavigateAllSoftwareVertical(-1);
return;
}
if (_navFocused)
{
_navFocused = false;
Sound.Play("ui.button.over");
}
}
private void NavigateDown()
{
if (_allSoftwareOpen)
{
NavigateAllSoftwareVertical(1);
return;
}
if (!_navFocused)
{
_navFocused = true;
Sound.Play("ui.button.over");
}
}
private void NavigateLeft()
{
if (_allSoftwareOpen)
{
NavigateAllSoftwareHorizontal(-1);
return;
}
if (_navFocused)
{
if (_navSelection > 0)
{
_navSelection--;
Sound.Play("ui.button.over");
}
return;
}
SelectCarouselItem(_selectedIndex - 1, useGamepad: true);
}
private void NavigateRight()
{
if (_allSoftwareOpen)
{
NavigateAllSoftwareHorizontal(1);
return;
}
if (_navFocused)
{
if (_navSelection < NavItems.Length - 1)
{
_navSelection++;
Sound.Play("ui.button.over");
}
return;
}
SelectCarouselItem(_selectedIndex + 1, useGamepad: true);
}
private void SelectCarouselItem(int index, bool useGamepad)
{
if (HomeEntryCount == 0)
return;
var next = index.Clamp(0, HomeEntryCount - 1);
if (_selectedIndex == next && !_navFocused)
return;
_selectedIndex = next;
_navFocused = false;
_queuedHomeLaunchGame = null;
_homeCarouselSettleRemaining = HomeCarouselSettleDuration;
if (useGamepad)
SetGamepadMode();
Sound.Play("ui.button.over");
QueueArtworkRefresh();
SyncBackdropTexture();
StateHasChanged();
}
private bool IsHomeViewMoreIndex(int index)
{
return index == HomeViewMoreIndex;
}
private void UpdateBackdropTransition()
{
SyncBackdropTexture();
if (_previousBackdrop == null)
return;
if (_currentBackdrop == null && !_previousBackdropFading)
{
_previousBackdropFading = true;
_backdropVersion++;
StateHasChanged();
return;
}
_previousBackdropAge += Time.Delta;
if (_previousBackdropAge < BackdropRetainDuration)
return;
_previousBackdrop = null;
PruneArtworkTextures();
_backdropVersion++;
StateHasChanged();
}
private void SyncBackdropTexture()
{
var next = SelectedBackdrop;
if (next == _currentBackdrop)
return;
if (_currentBackdrop != null)
_previousBackdrop = _currentBackdrop;
_currentBackdrop = next;
_previousBackdropAge = 0f;
_previousBackdropFading = false;
_backdropVersion++;
}
private void SelectNavItem(int index, bool useGamepad)
{
_navSelection = index.Clamp(0, NavItems.Length - 1);
_navFocused = true;
if (useGamepad)
SetGamepadMode();
else
_input.ForceMouseMode();
StateHasChanged();
}
private void ActivateNavItem(int index, bool useGamepad)
{
SelectNavItem(index, useGamepad);
switch (index)
{
case 0:
ShowUnavailableToast("#toast.netplay.title", "#toast.netplay.message", "group", NavItems[index].ColorClass);
break;
case 1:
ShowUnavailableToast("#toast.news.title", "#toast.news.message", "newspaper", NavItems[index].ColorClass);
break;
case 2:
ShowUnavailableToast("#toast.cloud.title", "#toast.cloud.message", "cloud", NavItems[index].ColorClass);
break;
case 3:
OpenSettings(useGamepad);
break;
case 4:
Sound.Play("ui.button.press");
Game.Close();
break;
}
}
private void ShowUnavailableToast(string title, string message, string icon, string colorClass)
{
_toast.Show(title, message, icon, colorClass);
Sound.Play("ui.button.press");
StateHasChanged();
}
public void ShowToast(string title, string message, string icon = "info", string colorClass = "blue")
{
_toast.Show(title, message, icon, colorClass);
StateHasChanged();
}
private void UpdateToast()
{
if (!_toast.IsRunning)
return;
_toast.Tick(Time.Delta);
StateHasChanged();
}
private void OpenSelectedDetails()
{
if (!CanOpenDetails)
return;
DetailsModal detailsPanel = EnsureDetailsPanel();
detailsPanel?.Open(SelectedGame, _input.UseGamepad);
StateHasChanged();
}
public void RefreshControllerHints()
{
InputHints.Current?.Refresh();
}
private DetailsModal EnsureDetailsPanel()
{
if (DetailsModal.Current != null && DetailsModal.Current.IsValid())
return DetailsModal.Current;
DetailsModal detailsPanel = Scene.GetAllComponents<DetailsModal>().FirstOrDefault(panel => panel.IsValid());
if (detailsPanel != null)
return detailsPanel;
return AddComponent<DetailsModal>();
}
private static bool IsCarouselOffsetMounted(int offset)
{
return offset >= -CarouselMountedRange && offset <= CarouselMountedRange;
}
protected override int BuildHash()
{
if (_isVisible)
return HashCode.Combine(
HashCode.Combine(_selectedIndex, _allSoftwareSelection, (int)AllSoftwareScrollY, _allSoftwareOpen),
HashCode.Combine(AddGamesModalOpen, SettingsModalOpen, _allGames.Count, _homeItems.Count, _suppressHomeCarouselTransitions),
HashCode.Combine(_homeCarouselRenderVersion, _input.UseGamepad, _artworkVersion, _backdropVersion),
_allSoftwareTitleDirection,
HashCode.Combine(_navFocused, _navSelection, _toast.RenderHash));
return HashCode.Combine(_isVisible);
}
private void OnJoinRejected()
{
Show();
ShowToast("#toast.missingrom.title", "#toast.missingrom.message", "warning", "orange");
}
protected override void OnDestroy()
{
if (NetworkManager.Current != null)
NetworkManager.Current.JoinRejected -= OnJoinRejected;
if (Current == this)
Current = null;
DisposeArtworkTextures();
_currentBackdrop = null;
_previousBackdrop = null;
}
private void DisposeArtworkTextures()
{
var textures = _boxArt.Values.Concat(_logos.Values).Concat(_snaps.Values).Concat(_titles.Values).Concat(_pendingTextureDisposals.Select(item => item.Texture)).Where(texture => texture != null).ToHashSet();
foreach (var texture in textures)
Thumbnails.ReleaseTexture(texture);
_boxArt.Clear();
_logos.Clear();
_snaps.Clear();
_titles.Clear();
_pendingTextureDisposals.Clear();
}
}
@using Sandbox;
@using Sandbox.UI;
@using System.Linq;
@inherits PanelComponent
<root>
<div class="bg">
<!-- Blob 1: Cyan - 5 children (рухливі клякси) -->
<div class="blob-group b1">
<div class="c c1a"></div>
<div class="c c1b"></div>
<div class="c c1c"></div>
<div class="c c1d"></div>
<div class="c c1e"></div>
</div>
<!-- Blob 2: Purple - 4 children (рухливі клякси) -->
<div class="blob-group b2">
<div class="c c2a"></div>
<div class="c c2b"></div>
<div class="c c2c"></div>
<div class="c c2d"></div>
</div>
<!-- Blob 3: Green - 3 children (рухливі клякси) -->
<div class="blob-group b3">
<div class="c c3a"></div>
<div class="c c3b"></div>
<div class="c c3c"></div>
</div>
<!-- Blob 4: Pink - 4 children (рухливі клякси) -->
<div class="blob-group b4">
<div class="c c4a"></div>
<div class="c c4b"></div>
<div class="c c4c"></div>
<div class="c c4d"></div>
</div>
<!-- Blob 5: Deep Blue - 2 children (рухливі клякси) -->
<div class="blob-group b5">
<div class="c c5a"></div>
<div class="c c5b"></div>
</div>
<div class="noise"></div>
</div>
<div class="layout">
<div class="left-panel">
<div class="logo">PAINT<span>SLIME</span></div>
<div class="tagline">3v3 Multiplayer Paint Arena</div>
</div>
<div class="right-panel">
<div class="btn btn-multi" onclick=@ShowMultiplayer>Multiplayer</div>
<div class="btn btn-exit" onclick=@QuitGame>Quit</div>
</div>
</div>
<!-- Matchmaking / Lobby Overlay -->
@if ( Matchmaking.IsValid() && Matchmaking.IsSearching )
{
<div class="lobby-overlay">
<div class="lobby-card">
<div class="spinner-container">
<div class="spinner-ring"></div>
<div class="spinner-core"></div>
</div>
<h2>MATCHMAKING</h2>
<div class="status-msg">@Matchmaking.StatusMessage</div>
<div class="player-slots">
@for ( int i = 0; i < Matchmaking.RequiredPlayers; i++ )
{
var slotActive = i < Matchmaking.ConnectedPlayers;
var slotReady = i < Matchmaking.ReadyPlayers.Count;
<div class="slot @(slotActive ? "active" : "") @(slotReady ? "ready" : "")">
<div class="slot-dot"></div>
</div>
}
</div>
<div class="lobby-actions">
@{
bool amIReady = Connection.Local != null && Matchmaking.ReadyPlayers.Contains( Connection.Local.Id );
bool isHost = Sandbox.Networking.IsHost;
}
<div class="btn-lobby btn-ready @(amIReady ? "is-ready" : "")" onclick=@ToggleReadyStatus>
@(amIReady ? "READY!" : "READY")
</div>
<div class="btn-lobby btn-cancel" onclick=@CancelSearch>Cancel</div>
</div>
</div>
</div>
}
</root>
@code {
[Property] public MatchmakingManager Matchmaking { get; set; }
protected override void OnStart()
{
// Шукаємо компонент на сцені, якщо не призначено вручну
if ( !Matchmaking.IsValid() )
{
Matchmaking = Scene.GetAllComponents<MatchmakingManager>().FirstOrDefault();
}
// Автоматично створюємо MatchmakingManager, якщо його немає на сцені,
// щоб пошук працював одразу «з коробки» без додаткових ручних налаштувань
if ( !Matchmaking.IsValid() )
{
Matchmaking = GameObject.Components.Create<MatchmakingManager>();
}
}
void ShowMultiplayer()
{
if ( Matchmaking.IsValid() )
{
Matchmaking.StartMatchmaking();
}
else
{
Log.Error( "MatchmakingManager not found in the scene! Try adding it manually." );
}
}
void CancelSearch()
{
if ( Matchmaking.IsValid() )
{
Matchmaking.CancelMatchmaking();
}
}
void ToggleReadyStatus()
{
if ( !Matchmaking.IsValid() ) return;
if ( Connection.Local == null ) return;
Matchmaking.ToggleReady( Connection.Local.Id );
}
void ForceStartGame()
{
if ( Matchmaking.IsValid() )
{
Matchmaking.ForceStartGame();
}
}
void QuitGame() { Game.Close(); }
protected override int BuildHash()
{
var isSearching = Matchmaking.IsValid() && Matchmaking.IsSearching;
var players = Matchmaking.IsValid() ? Matchmaking.ConnectedPlayers : 0;
var ready = Matchmaking.IsValid() ? Matchmaking.ReadyPlayers.Count : 0;
var msg = Matchmaking.IsValid() ? Matchmaking.StatusMessage : "";
var isHost = Sandbox.Networking.IsHost;
return System.HashCode.Combine( isSearching, players, msg, isHost, ready );
}
}
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace Sandbox
@if ( Player.IsValid() && Player.WantsHideHud )
return;
@if ( !HasAnyAction )
return;
<root>
<div class="panel">
<div class="name">@Info?.Name</div>
@if ( !string.IsNullOrEmpty( Info?.Description ) )
{
<div class="description">@Info.Description</div>
}
<div class="divider"></div>
<div class="actions">
@if (!string.IsNullOrEmpty(Info?.PrimaryAction))
{
<div class="action">
<InputHint Action="attack1" class="key" />
<span class="text">@Info.PrimaryAction</span>
</div>
}
@if (!string.IsNullOrEmpty(Info?.SecondaryAction))
{
<div class="action">
<InputHint Action="attack2" class="key" />
<span class="text">@Info.SecondaryAction</span>
</div>
}
@if (!string.IsNullOrEmpty(Info?.ReloadAction))
{
<div class="action">
<InputHint Action="reload" class="key" />
<span class="text">@Info.ReloadAction</span>
</div>
}
</div>
</div>
</root>
@code
{
IToolInfo CurrentToolInfo
{
get
{
var inv = Player?.GetComponent<PlayerInventory>();
if ( !inv.IsValid() || !inv.ActiveWeapon.IsValid() )
return null;
if ( inv.ActiveWeapon.GetComponentInChildren<IToolInfo>() is { IsValid: true } toolInfo )
return toolInfo;
return inv?.ActiveWeapon as IToolInfo;
}
}
Player Player => Player.FindLocalPlayer();
IToolInfo Info => CurrentToolInfo;
bool HasAnyAction => !string.IsNullOrEmpty( Info?.PrimaryAction )
|| !string.IsNullOrEmpty( Info?.SecondaryAction )
|| !string.IsNullOrEmpty( Info?.ReloadAction );
protected override void OnUpdate()
{
SetClass( "visible", CurrentToolInfo is not null );
}
protected override int BuildHash() => System.HashCode.Combine( Info?.Name, Info?.Description, Info?.PrimaryAction, Info?.SecondaryAction, Info?.ReloadAction, Player?.WantsHideHud );
}
@using Sandbox;
@using Sandbox.UI;
@using Sandbox.Mounting;
@namespace Sandbox
@inherits Panel
@if ( mount is null )
{
<root>
#spawnmenu.mounts.error
</root>
return;
}
<SpawnMenuContent>
<Header>
<SpawnMenuToolbar>
<Left>
@*
<Button Icon="home" @onclick="@GoHome"></Button>
<Button Icon="arrow_upward" Disabled=@( currentDir?.Parent == null ) @onclick="@GoUp"></Button>
*@
</left>
<Body>
@*
<SpawnMenuPath Path=@CurrentPath></SpawnMenuPath>
*@
<label>@( $"{mount.Title}, {GetData().Count()}" ) </label><label>#spawnmenu.common.items</label>
</Body>
<Right>
<SpawnMenuFilter Query:bind="@Filter"></SpawnMenuFilter>
<SpawnMenuIconOptions Size:bind="@ItemSize"></SpawnMenuIconOptions>
</Right>
</SpawnMenuToolbar>
</Header>
<Body>
<VirtualGrid Items=@( GetData() ) ItemSize=@ItemSize>
<Item Context="item">
@if (item is ResourceFolder dir)
{
<div class="folder" @onclick=@( () => currentDir = dir )>
<div><i>folder</i></div>
<div class="name">@dir.Name</div>
</div>
}
@if (item is ResourceLoader loader)
{
var nameWithoutExt = System.IO.Path.GetFileNameWithoutExtension(loader.Name);
<SpawnMenuIcon HideText=@(ItemSize <= 60) Ident=@($"mount:{loader.Path}") Title="@nameWithoutExt" [email protected]( mount.Title )></SpawnMenuIcon>
}
</Item>
</VirtualGrid>
</Body>
</SpawnMenuContent>
@code
{
public string Ident { get; set; }
public string Filter { get; set; }
public static int ItemSize { get; set; } = 120;
ResourceFolder currentDir;
BaseGameMount mount;
protected override int BuildHash() => HashCode.Combine(Ident, ItemSize, Filter);
protected override async Task OnParametersSetAsync()
{
mount = await Sandbox.Mounting.Directory.Mount( Ident );
}
void GoHome()
{
currentDir = mount.RootFolder;
}
void GoUp()
{
if ( currentDir.Parent != null )
{
currentDir = currentDir.Parent;
}
}
string CurrentPath
{
get => currentDir?.Path ?? "/";
}
IEnumerable<object> GetData()
{
var q = mount.Resources
.Where( x => x.Type == ResourceType.Model )
.Where( x => !x.Flags.Contains( Sandbox.Mounting.ResourceFlags.DeveloperOnly ) ); // Hide dev-only junk models
if ( !string.IsNullOrWhiteSpace( Filter ) )
{
q = q.Where( x => x.Path.Contains( Filter, StringComparison.OrdinalIgnoreCase ) );
}
return q;
}
}
@using Sandbox;
@using Sandbox.UI;
@namespace Sandbox
@inherits BasePopup
<root class="popup">
<div class="inner" onclick:preventDefault=@true>
<div class="popup-header">
<h2>@Title</h2>
@if (AllowPackages)
{
<div class="sort-buttons">
@foreach ( var mode in Enum.GetValues<PackageSortMode>() )
{
var m = mode;
<div class="sort-button @(SortMode == m ? "active" : "")" @onclick=@(() => SetSort(m))>@m.ToString()</div>
}
</div>
}
</div>
<VirtualGrid Items=@(_filteredItems) ItemSize=@(new Vector2( 180, 200 )) class="grid">
<Item Context="item">
@if (item is Resource res)
{
bool selected = res.ResourcePath.Equals(_pendingValue ?? CurrentValue, StringComparison.OrdinalIgnoreCase);
string itemClass = selected ? "item active" : "item";
string thumbUrl = $"thumb:{res.ResourcePath}";
<div @[email protected] class="@itemClass" @onclick=@(() => { OnItemClicked(res); })>
<div class="icon" style="background-image: url( @thumbUrl )"></div>
@if ( res is IDefinitionResource definitionResource )
{
<div class="title">@definitionResource.Title</div>
<div class="desc">@definitionResource.Description</div>
}
else
{
<div class="title">@res.ResourceName</div>
<div class="desc"></div>
}
</div>
}
else if ( item is Package pkg )
{
bool selected = pkg.FullIdent.Equals(CurrentValue, StringComparison.OrdinalIgnoreCase);
string itemClass = selected ? "item active" : "item";
<div @[email protected] class="@itemClass" @onclick=@(() => { SelectPackage(pkg); })>
<div class="icon" style="background-image: url( @pkg.Thumb )"></div>
<div class="title">@pkg.Title</div>
<div class="desc">@pkg.Org.Title</div>
</div>
}
</Item>
</VirtualGrid>
<div class="popup-footer">
<div class="clear-button" @onclick=@ClearSelection>Clear</div>
<div class="select-button" @onclick=@ConfirmSelection>Select</div>
</div>
</div>
</root>
@code
{
public string Title { get; set; } = "Select Resource";
public string Extension { get; set; } = "vmdl";
public string CurrentValue { get; set; }
public bool AllowPackages { get; set; }
public Action<string> OnSelectedFile;
string _pendingValue;
IResourcePreview _previewingResource;
public string Category { get; set; }
PackageSortMode SortMode { get; set; } = PackageSortMode.Popular;
void SetSort( PackageSortMode mode )
{
if ( SortMode == mode ) return;
SortMode = mode;
if ( AllowPackages )
_ = LoadCloudPackages();
}
readonly List<Resource> _localResources = new();
readonly List<Package> _cloudPackages = new();
List<object> _filteredItems = new();
protected override void OnParametersSet()
{
_localResources.Clear();
_cloudPackages.Clear();
_localResources.AddRange( ResourceLibrary.GetAll<Resource>().Where( FilterExtension ).Where( FilterCategory ) );
RebuildFilteredItems();
if ( AllowPackages )
{
_ = LoadCloudPackages();
}
}
void RebuildFilteredItems()
{
var items = new List<object>();
items.AddRange( _localResources );
items.AddRange( _cloudPackages );
_filteredItems = items;
}
async Task LoadCloudPackages()
{
var query = $"sort:{SortMode.ToIdentifier()} type:{Extension}";
var result = await Package.FindAsync( query );
_cloudPackages.Clear();
if ( result?.Packages != null )
{
_cloudPackages.AddRange( result.Packages );
}
RebuildFilteredItems();
StateHasChanged();
}
bool FilterExtension(Resource res)
{
if (res == null) return false;
if (string.IsNullOrEmpty(Extension)) return true;
var ext = System.IO.Path.GetExtension(res.ResourcePath);
if (string.IsNullOrEmpty(ext)) return false;
return ext.TrimStart('.').Equals(Extension, StringComparison.OrdinalIgnoreCase);
}
bool FilterCategory(Resource res)
{
if (string.IsNullOrEmpty(Category)) return true;
if (res is not SoundDefinition soundDef) return true;
return string.Equals(soundDef.Category, Category, StringComparison.OrdinalIgnoreCase);
}
void OnItemClicked( Resource res )
{
StopPreview();
_pendingValue = res.ResourcePath;
var gameRes = ResourceLibrary.Get<GameResource>( res.ResourcePath );
if ( gameRes is IResourcePreview preview )
{
_previewingResource = preview;
preview.OnPreview();
}
StateHasChanged();
}
void StopPreview()
{
_previewingResource?.OnPreviewStop();
_previewingResource = null;
}
public override void OnDeleted()
{
StopPreview();
base.OnDeleted();
}
void ClearSelection()
{
StopPreview();
_pendingValue = null;
OnSelectedFile?.Invoke( null );
StateHasChanged();
}
void ConfirmSelection()
{
StopPreview();
if ( _pendingValue != null )
{
OnSelectedFile?.Invoke( _pendingValue );
Delete();
}
}
protected override void OnMouseDown(MousePanelEvent e)
{
base.OnMouseDown(e);
if (e.Target == this)
{
StopPreview();
Delete();
}
}
void SelectResource(Resource res)
{
OnSelectedFile?.Invoke(res.ResourcePath);
Delete();
}
void SelectPackage(Package pkg)
{
OnSelectedFile?.Invoke(pkg.FullIdent);
Delete();
}
}
@using Sandbox;
@using Sandbox.UI;
@attribute [InspectorEditor(typeof(Dresser))]
@inherits Panel
@namespace Sandbox
@implements IInspectorEditor
<root>
<div class="body">
<Label>todo: add / remove clothing resources</Label>
</div>
<div class="footer">
<Button class="menu-action primary" Text="Randomize" Icon="🎲" onclick=@DoRandomize></Button>
<Button class="menu-action primary" Text="Clear" Icon="🧹" onclick=@DoClear></Button>
</div>
</root>
@code
{
public string Title => "👗 Dresser";
public Dresser Target { get; private set; }
public bool TrySetTarget(List<GameObject> selection)
{
Dresser found = null;
foreach (var go in selection)
{
if ( go.Tags.Has( "player" ) ) continue;
found = go.GetComponentInChildren<Dresser>();
if (found != null) break;
}
if (found == Target) return Target != null;
Target = found;
StateHasChanged();
return Target != null;
}
void DoRandomize() => TryBroadcastRandomize( Target.GameObject );
void DoClear() => TryBroadcastClear( Target.GameObject );
[Rpc.Host]
private static void TryBroadcastRandomize(GameObject go)
{
if ( !go.IsValid() ) return;
var dresser = go.GetComponentInChildren<Dresser>();
if ( dresser is null ) return;
dresser.Randomize();
// Refresh the object to update the clothing items for everyone
go.Network.Refresh();
}
[Rpc.Host]
private static async void TryBroadcastClear(GameObject go)
{
if ( !go.IsValid() ) return;
var dresser = go.GetComponentInChildren<Dresser>();
if ( dresser is null ) return;
dresser.Clothing.Clear();
dresser.Clear();
await dresser.Apply();
// Refresh the object to update the clothing items for everyone
go.Network.Refresh();
}
}
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@*
PeachSplatOverlay — full-screen blit overlay driven by PeachOverlayBridge.
Add this component to the HUD / camera GameObject in the scene (one instance).
Evening = regular peach, Midnight = giant peach.
*@
<root>
<div class="peach-overlay @(PeachOverlayBridge.IsGiant ? "midnight" : "evening")" style="opacity: @OpacityStr;">
</div>
</root>
@code {
private float _current = 0f;
public string OpacityStr => _current.ToString( "0.00" );
protected override void OnUpdate()
{
PeachOverlayBridge.Tick( Time.Delta );
_current = _current.LerpTo( PeachOverlayBridge.Target, Time.Delta * PeachOverlayBridge.Speed * 3f );
if ( PeachOverlayBridge.Exiting && _current < 0.005f )
{
_current = 0f;
PeachOverlayBridge.OnFullyFaded();
}
StateHasChanged();
}
protected override int BuildHash()
{
return System.HashCode.Combine( PeachOverlayBridge.IsGiant, (int)(_current * 1000) );
}
}
<style>
.peach-overlay {
position: absolute;
inset: 0;
pointer-events: none;
}
.peach-overlay.evening {
background: radial-gradient(
ellipse at center,
rgba(30, 15, 60, 0) 30%,
rgba(80, 30, 120, 220) 100%
);
}
.peach-overlay.midnight {
background: radial-gradient(
ellipse at center,
rgba(5, 5, 30, 40) 20%,
rgba(10, 5, 60, 242) 100%
);
}
</style>
@inherits PanelComponent
<root> <div class="ynal2p"> @t </div> </root>
@code
{
[Property] public game Game {get;set;}
string t = "You need at least 2 players.";
protected override void OnUpdate()
{
if (Game.IsActive == true && Game.Players.Count >= 2)
d(this);
}
[Rpc.Broadcast]
public void d(PanelComponent pc)
{pc.Destroy();}
}@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;
@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;
}
}
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox
<root>
@Path
</root>
@code
{
public string Path { get; set; }
}
@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 );
}
}
@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("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;
@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 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}" );
}
}
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
<root class="row player">
@if ( Entry is not null && Entry.Connection is not null )
{
var steamId = Entry.Connection.SteamId;
bool isMuted = SandboxVoice.IsMuted( steamId );
<img class="avatar" src="avatar:@steamId" />
<div class="name">@Entry.DisplayName</div>
<div class="stat">@Entry.Kills</div>
<div class="stat">@Entry.Deaths</div>
<div class="stat">@(Entry.Ping.CeilToInt())</div>
@if ( !Entry.IsMe )
{
<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 PlayerData Entry { get; set; }
public override void Tick()
{
if ( Entry is null ) return;
SetClass( "me", Entry.IsMe );
if ( Entry.Connection is not null )
SetClass( "friend", new Friend( Entry.Connection.SteamId ).IsFriend );
}
protected override int BuildHash()
{
if (Entry is null) return 0;
return System.HashCode.Combine( Entry.DisplayName, Entry.Kills, Entry.Deaths, Entry.Ping );
}
protected override void OnRightClick( MousePanelEvent e )
{
if ( Entry is null || Entry.Connection is null ) return;
var steamId = Entry.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 {Entry.Connection.DisplayName}'s SteamID to your clipboard", 5 );
} );
if ( !Entry.IsMe )
{
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( Entry ) );
menu.AddOption( "gavel", "Ban", () => OpenBanConfirm( Entry ) );
}
}
e.StopPropagation();
}
void OpenKickConfirm( PlayerData entry )
{
var popup = new StringQueryPopup
{
Title = "Kick Player",
Prompt = $"Why do you want to kick {entry.DisplayName}?",
ConfirmLabel = "Kick",
OnConfirm = x =>
{
GameManager.RpcKickPlayer( entry.Connection, x );
}
};
popup.Parent = FindPopupPanel();
}
void OpenBanConfirm( PlayerData entry )
{
var popup = new StringQueryPopup
{
Title = "Ban Player",
Prompt = $"Why do you want to ban {entry.DisplayName}?",
ConfirmLabel = "Ban",
OnConfirm = x => BanSystem.RpcBanPlayer( entry.Connection, x )
};
popup.Parent = FindPopupPanel();
}
}
@using Sandbox;
@using Sandbox.UI;
@using Sandbox.Mounting;
@inherits Panel
@namespace Sandbox
<root>
<div class="name">@(Content.GetMeta<string>( "name" ) ?? "Dupe")</div>
</root>
@code
{
public Storage.Entry Content;
protected override void OnParametersSet()
{
Style.SetBackgroundImage( Content.Thumbnail );
}
protected override void OnMouseDown( MousePanelEvent e )
{
if ( e.MouseButton == MouseButtons.Right )
{
var menu = MenuPanel.Open( this );
menu.AddOption( "delete", "#spawnmenu.common.delete", () => Content.Delete() );
menu.AddOption( "publish", "#spawnmenu.common.publish", Publish );
SpawnlistData.PopulateContextMenu( menu, new SpawnlistItem
{
Ident = SpawnlistItem.MakeIdent( "dupe", Content.Id.ToString(), "local" ),
Title = Content.GetMeta<string>( "name" ) ?? "Dupe",
Icon = null,
} );
return;
}
base.OnMouseDown( e );
}
void Publish()
{
var options = new Modals.WorkshopPublishOptions();
options.Title = "My Dupe";
options.AddCategory<DupeCategory>( "Category" );
options.AddCategory<DupeMovement>( "Movement" );
Content.Publish(options);
}
}
@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>
</SpawnMenuToolbar>
</Header>
<Body>
<VirtualGrid Items=@Entries ItemSize=@(120)>
<Item Context="item">
@if ( item is LocalProps.Entry entry )
{
<SpawnMenuIcon Ident=@($"prop:{entry.Path}") [email protected] />
}
</Item>
</VirtualGrid>
</Body>
</SpawnMenuContent>
@code
{
public string Category { get; set; } = "";
private string Filter
{
get;
set { field = value; Rebuild(); }
}
private List<LocalProps.Entry> Entries = new();
protected override void OnParametersSet()
{
Rebuild();
}
void Rebuild()
{
var source = string.IsNullOrEmpty( Category )
? LocalProps.All
: LocalProps.All.Where( x => x.Category == Category ).ToList();
if ( !string.IsNullOrWhiteSpace( Filter ) )
source = source.Where( x => x.DisplayName.Contains( Filter, StringComparison.OrdinalIgnoreCase )
|| x.Path.Contains( Filter, StringComparison.OrdinalIgnoreCase ) ).ToList();
Entries = source;
StateHasChanged();
}
}
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox
<root></root>
@code
{
Panel _active;
public Panel ActivePanel
{
get => _active;
set => SwitchToPanel(value );
}
protected override void OnChildAdded(Panel child)
{
base.OnChildAdded(child);
if ( ChildrenCount == 1 )
{
SwitchToPanel(child);
}
else
{
child.AddClass("hidden");
}
}
public void SwitchToPanel( Panel panel )
{
_active = panel;
foreach ( var child in Children )
{
child.SetClass("hidden", child != panel);
}
}
}
@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 PanelComponent
@implements Global.IPlayerEvents
@if ( Player.IsValid() && Player.WantsHideHud )
return;
@if ( !inventory.IsValid() )
return;
<root>
@for (int i = 0; i < inventory.MaxSlots; i++)
{
<InventorySlot Index=@i Inventory=@inventory Active=@(activeSlot == i)></InventorySlot>
}
<HotbarPresetsButton Inventory=@inventory></HotbarPresetsButton>
</root>
@code
{
[Property, Group("Sound")] public SoundEvent SwitchSound { get; set; }
Player Player => Player.FindLocalPlayer();
PlayerInventory inventory;
int activeSlot = -1;
BaseCarryable prev;
protected override int BuildHash() => HashCode.Combine( inventory, activeSlot, Player?.WantsHideHud );
void Global.IPlayerEvents.OnPlayerPickup(PlayerPickupEvent e)
{
StateHasChanged();
}
bool IsSpawnMenuOpen()
{
var host = Game.ActiveScene.Get<SpawnMenuHost>();
return host?.Panel?.HasClass("open") ?? false;
}
protected override void OnUpdate()
{
inventory = Game.ActiveScene.GetAllComponents<PlayerInventory>().Where(x => x.Network.IsOwner).FirstOrDefault();
activeSlot = inventory?.ActiveWeapon?.InventorySlot ?? -1;
Panel.SetClass( "spawnmenu-open", IsSpawnMenuOpen() );
}
public void HandleInput()
{
if (inventory is null)
return;
MoveSlot(-(int)Input.MouseWheel.y);
if ( Input.Pressed( "invprev" ) && prev.IsValid() )
{
var weapon = prev;
prev = inventory.ActiveWeapon;
inventory.SwitchWeapon( weapon );
}
if (Input.Pressed("SlotNext")) MoveSlot(1);
if (Input.Pressed("SlotPrev")) MoveSlot(-1);
if (Input.Pressed("Slot1")) SelectSlot(0);
if (Input.Pressed("Slot2")) SelectSlot(1);
if (Input.Pressed("Slot3")) SelectSlot(2);
if (Input.Pressed("Slot4")) SelectSlot(3);
if (Input.Pressed("Slot5")) SelectSlot(4);
if (Input.Pressed("Slot6")) SelectSlot(5);
if (Input.Pressed("Slot7")) SelectSlot(6);
if (Input.Pressed("Slot8")) SelectSlot(7);
if (Input.Pressed("Slot9")) SelectSlot(8);
}
/// <summary>
/// Pressing a number key directly switches to that slot's weapon.
/// If the slot is empty or already active, holsters the current weapon.
/// </summary>
public void SelectSlot( int slot )
{
var weapon = inventory.GetSlot( slot );
if ( weapon.IsValid() && !weapon.CanSwitch() )
return;
prev = inventory.ActiveWeapon;
inventory.SwitchWeapon( weapon, allowHolster: true );
Sound.Play( SwitchSound );
}
public void MoveSlot( int delta )
{
if ( delta == 0 )
return;
var weapons = inventory.Weapons.Where( x => x.CanSwitch() ).ToList();
if ( weapons.Count == 0 )
return;
// Find current position in the ordered weapon list
BaseCarryable current = inventory.ActiveWeapon ?? weapons.FirstOrDefault();
int currentIndex = weapons.IndexOf( current );
currentIndex += delta;
currentIndex %= weapons.Count;
if ( currentIndex < 0 )
currentIndex = weapons.Count + currentIndex;
var target = weapons[currentIndex];
prev = inventory.ActiveWeapon;
inventory.SwitchWeapon( target );
Sound.Play( SwitchSound );
}
}
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@inherits Panel
@attribute [StyleSheet("GameOverScreen.razor.scss")]
<root>
@if(Manager.Instance.IsBossDead)
{
<div class="title_win"></div>
}
else
{
<div class="title_lose"></div>
}
@* <div class="title">@(Manager.Instance.IsBossDead ? "Victory!" : "Game Over")</div> *@
<div class="content">
@if(Manager.Instance.PlayerProfileToShow != null)
{
<PlayerProfilePanel />
}
else
{
<div class="col left">
<GameOverResultPanel />
</div>
<div class="col right">
<LeaderboardPanel [email protected] IsOnMainMenu=@false />
</div>
}
</div>
<div class="row controls">
<button class="restart_button @(Input.UsingController && Networking.IsHost ? "controller" : "") @(_restartActivated ? "ctrl-activated" : _restartHeld ? "ctrl-held" : "")" style="opacity:@(Networking.IsHost ? 1f : 0.1f);" onclick=@(() => Restart())>
</button>
<button class="@(Networking.IsHost ? "lobby_button" : "leave_button") @(Input.UsingController ? "controller" : "") @(_lobbyActivated ? "ctrl-activated" : _lobbyHeld ? "ctrl-held" : "")" onclick=@(() => ReturnToLobby())>
</button>
@* <button class="lobby_button" style="opacity:@(Networking.IsHost ? 1f : 0.1f);" onclick=@(() => ReturnToLobby())></button> *@
</div>
@if(AreYouSureAction is not null)
{
<div class="are-you-sure" onclick=@(() => { Manager.Instance.PlaySfxUI("click", pitch: 0.75f, volume: 0.6f); AreYouSureAction = null; })>
<div class="panel">
<label class="title">@AreYouSureText</label>
<div class="row">
<button class="confirm_no_button" style="width: 25%;" onclick=@(() => { AreYouSureAction = null; })>
@if(Input.UsingController) { <InputHint class="inputbutton" Button="Back" /> }
</button>
<button class="confirm_yes_button" style="width: 25%;" onclick=@(() => { AreYouSureAction?.Invoke(); AreYouSureAction = null; })>
@if(Input.UsingController) { <InputHint class="inputbutton" Button="Dash" /> }
</button>
</div>
</div>
</div>
}
</root>
@code
{
Action AreYouSureAction = null;
string AreYouSureText = "Are you sure?";
bool _restartHeld;
bool _restartActivated;
bool _lobbyHeld;
bool _lobbyActivated;
public void Restart()
{
if (!Networking.IsHost)
return;
Manager.Instance.Restart();
Manager.Instance.SetEscMenuOpen(false);
}
public void ReturnToLobby()
{
if (!Networking.IsHost)
{
AreYouSureText = "Quit the party?";
AreYouSureAction = () =>
{
DoClientLeave();
};
return;
}
Manager.Instance.SetGameState(GameState.Lobby);
Manager.Instance.SetEscMenuOpen(false);
}
public override void Tick()
{
base.Tick();
if (!Input.UsingController)
{
_restartHeld = false;
_restartActivated = false;
_lobbyHeld = false;
_lobbyActivated = false;
return;
}
if (AreYouSureAction is not null)
{
if (Input.Pressed("Dash")) { Manager.Instance.PlaySfxUI("click", pitch: 1.15f, volume: 0.75f); AreYouSureAction?.Invoke(); AreYouSureAction = null; }
else if (Input.Pressed("Back")) { Manager.Instance.PlaySfxUI("click", pitch: 0.75f, volume: 0.6f); AreYouSureAction = null; }
return;
}
if (_restartActivated)
{
_restartActivated = false;
Restart();
return;
}
if (_lobbyActivated)
{
_lobbyActivated = false;
ReturnToLobby();
return;
}
bool restartDown = Input.Down("R");
if (_restartHeld && !restartDown) { _restartHeld = false; _restartActivated = true; Manager.Instance.PlaySfxUI("click", pitch: 1.15f, volume: 0.75f); }
else { if (!_restartHeld && restartDown) Manager.Instance.PlaySfxUI("click", pitch: 0.85f, volume: 0.6f); _restartHeld = restartDown; }
bool lobbyDown = Input.Down("banish");
if (_lobbyHeld && !lobbyDown) { _lobbyHeld = false; _lobbyActivated = true; Manager.Instance.PlaySfxUI("click", pitch: 1.15f, volume: 0.75f); }
else { if (!_lobbyHeld && lobbyDown) Manager.Instance.PlaySfxUI("click", pitch: 0.85f, volume: 0.6f); _lobbyHeld = lobbyDown; }
}
protected override int BuildHash()
{
int buttonHash = System.HashCode.Combine(
_restartHeld,
_restartActivated,
_lobbyHeld,
_lobbyActivated
);
return System.HashCode.Combine(
Manager.Instance.IsEscMenuOpen,
Input.UsingController,
AreYouSureAction is not null,
RealTime.Now, // todo:
buttonHash
);
}
void DoClientLeave()
{
if(!Networking.IsHost)
{
Networking.Disconnect();
Scene.LoadFromFile("scenes/game.scene");
}
Manager.Instance.SetEscMenuOpen(false);
DoClientLeaveAsync();
}
async void DoClientLeaveAsync()
{
Manager.Instance.FadeRpc(fadeIn: false);
await Task.Frame();
Manager.Instance.SetGameState(GameState.Lobby);
}
}
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@inherits Panel
@attribute [StyleSheet("LoadoutItemPanel.razor.scss")]
@{
var isGem = Item.Category == ShopItemCategory.Gem;
var owned = ProgressManager.IsItemOwned( Item.Id );
var coins = ProgressManager.GetCoins();
var canAfford = coins >= Item.Price;
var isLocked = !ProgressManager.IsItemUnlocked( Item.Id );
string stateClass;
if ( IsShopMode )
{
if ( isLocked )
stateClass = "locked";
else if ( isGem )
{
if ( !owned )
stateClass = canAfford ? "affordable" : "too_expensive";
else if ( ProgressManager.IsGemMaxed( Item.Id ) )
stateClass = "gem_maxed";
else
{
var upgCost = ProgressManager.GetGemUpgradeCost( Item.Id );
stateClass = (upgCost.HasValue && coins >= upgCost) ? "gem_upgradeable" : "gem_upgrade_expensive";
}
}
else
stateClass = owned ? "owned" : canAfford ? "affordable" : "too_expensive";
}
else
{
stateClass = IsSelected ? "selected" : IsSlotFull ? "slot_full" : "";
}
// Compute description text
string descText = null;
if ( isGem && (Item.GemDescription != null || Item.GemUpgradeDescription != null) )
{
var displayLevel = ProgressManager.GetGemDisplayLevel( Item.Id );
if ( !owned )
descText = Item.GemDescription?.Invoke( 1 );
else if ( IsShopMode && !ProgressManager.IsGemMaxed( Item.Id ) )
descText = Item.GemUpgradeDescription?.Invoke( displayLevel + 1 ) ?? Item.GemDescription?.Invoke( displayLevel );
else
descText = Item.GemDescription?.Invoke( displayLevel );
}
else if ( Item.ItemDescription != null )
{
descText = Item.ItemDescription();
}
}
<root class="@stateClass" onclick=@OnClick>
@{
var frameBg = Item.Category switch
{
ShopItemCategory.Gun => "/textures/ui/panel/item_gun.png",
ShopItemCategory.Charm => "/textures/ui/panel/item_charm.png",
ShopItemCategory.Gem => "/textures/ui/panel/item_gem.png",
_ => "/textures/ui/panel/panel_02.png",
};
}
<div class="frame" style="background-image: url(@frameBg);"></div>
<div class="top_row">
<div class="item_icon" style="background-image:url(@Item.IconPath);"></div>
<div class="top_text">
<label class="item_name">@Item.Name</label>
@if ( IsShopMode )
{
<div class="item_price">
@if ( isGem && owned )
{
var upgCost = ProgressManager.GetGemUpgradeCost( Item.Id );
@if ( upgCost.HasValue )
{
<div class="price_coin_icon"></div>
<label class="price_amount">@upgCost</label>
}
else
{
<label class="owned_label">Max</label>
}
}
else if ( !owned )
{
<div class="price_coin_icon"></div>
<label class="price_amount">@Item.Price</label>
}
</div>
}
</div>
</div>
<div class="middle">
@if ( descText != null )
{
<RichText class="item_desc" Text=@descText />
}
@if ( IsShopMode && isLocked )
{
var needed = ProgressManager.GetPurchasesNeeded( Item.Id );
var catWord = Item.Category == ShopItemCategory.Gun ? (needed == 1 ? "gun" : "guns")
: Item.Category == ShopItemCategory.Charm ? (needed == 1 ? "charm" : "charms")
: (needed == 1 ? "gem" : "gems");
<label class="lock_label">Buy @needed more @catWord</label>
}
</div>
@if ( isGem )
{
var maxLevel = (Item.UpgradePrices?.Length ?? 0) + 1;
var gemDL = ProgressManager.GetGemDisplayLevel( Item.Id );
// In shop mode: show the level being purchased next (matches the "Lv X" price label).
// In loadout mode: show current level.
var displayLevel = !IsShopMode ? gemDL
: !owned ? 1
: ProgressManager.GetGemUpgradeCost( Item.Id ).HasValue ? gemDL + 1 : gemDL;
<div class="level_indicator">
<label>@displayLevel</label><label class="level_sep">/</label><label>@maxLevel</label>
</div>
}
</root>
@code {
public ShopItemDef Item { get; set; }
public Action OnClick { get; set; }
/// <summary>True when used in the shop — shows price and lock info, computes shop state classes.</summary>
public bool IsShopMode { get; set; }
/// <summary>Loadout mode only — highlights the card as the active selection.</summary>
public bool IsSelected { get; set; }
/// <summary>Loadout mode only — dims the card when the slot is full and this item is not selected.</summary>
public bool IsSlotFull { get; set; }
protected override int BuildHash()
{
return System.HashCode.Combine(
Item.Id,
IsShopMode,
IsSelected,
IsSlotFull,
ProgressManager.StateVersion
);
}
}